三 使用XML配置SQL映射器(映射文件)
关系型数据库和SQL是经受时间考验和验证的数据存储机制。和其他的ORM 框架如Hibernate不同,【MyBatis鼓励】开发者可以直接【使用数据库】,而不是将其对开发者隐藏,因为这样可以充分发挥数据库服务器所提供的SQL语句的巨大威力。
与此同时,MyBaits【消除】了书写大量【冗余代码】的痛苦,它让使用SQL更容易。在代码里直接嵌套
很差的编码实践,并且维护起来困难。MyBaits使用了映射文件或注解来配置SQL语句。

3.1 映射器文件和映射器接口
我们已经看见了一些在映射器配置文件中配置基本的映射语句,以及怎样使用SqlSession对象调用它们的例子。现在让我们看一下在com.briup.mappers包中的StudentMapper.xml 配置文件内,是如何配置id为”findStudentById”的SQL语句的,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.briup.mappers.StudentMapper">
<select id="findStudentById" parameterType="int" resultType="Student">
select stud_id as studId, name, email, dob
from students where stud_id=#{studId}
</select>
</mapper>

我们可以通过下列代码调用findStudentById映射的SQL语句:
public Student findStudentById(Integer studId) {
SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();
try{
Student student = sqlSession.selectOne("com.briup.mappers.StudentMapper.findStudentById", studId);
return student;
}
finally {
sql Session.close();
}
}

调用映射方法的方式一:
我们可以通过字符串(字符串形式为:映射器配置文件所在的包名的【namespace + sql语句id值】,如上,即包名com.briup.mappers.StudentMapper和语句id的值findStudentById组成)调用映射的SQL语句。
但是这种方式【容易出错】,我们【不推荐】使用。你需要检查映射器配置文件中的定义,以保证你的输入参数类型和结果返回类型是有效的。

调用映射方法的方式二:
也就是我们一直在使用的方式,通过session获取mapper对象,然后由mapper对象直接调用映射方法实现功能。
【重点部分:】
MyBatis通过使用映射器Mapper接口提供了更好的调用映射语句的方法。一旦我们通过映射器配置文件配置了映射语句,我们可以创建一个完全对应的一个映射器接口,xml映射文件中的【namespace属性值】和映射接口的【全限定名】需要保持【一致】。
映射器接口中的【方法名】也跟映射器配置文件中【id值】完全对应;
映射方法的【参数类型】和【parameterType属性值】对应;
映射方法【返回值类型】和【returnType属性值】一致。

上述的StudentMapper.xml文件,我们可以创建一个映射器接口StudentMapper.java如下:

package com.briup.mappers;
public interface StudentMapper{
Student findStudentById(Integer id);
}
在Student Mapper.xml映射器配置文件中,其名空间namespace应该跟StudentMapper接口的全限定名保持一致。另外,StudentMapper.xml中语句id, parameterType,returnType 应该分别和StudentMapper接口中的方法名,参数类型,返回值相对应。

使用映射器接口我们可以以类型安全的形式调用调用映射语句。如下所示:

public Student findStudentById(Integer studId){
SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();
try {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

return student Mapper.findStudentById(studId);
}
finally {
sqlSession.close();
}
}

结论:
namespace关键字的用法
情形1: 任何值都可以
使用session对象执行sql语句;

情形2: 必须映射接口的全包名
使用映射接口对象 执行sql语句。

推荐第2种。

3.2 映射语句
MyBatis提供了多种元素来配置不同类型的语句,如SELECT,INSERT,UPDATE,DELETE。让我们看看如何具体配置映射语句

3.2.1 INSERT 插入语句
一个INSERT语句可以在<insert>标签元素在映射器XML配置文件中配置,如下所示:
<insert id="insertStudent" parameterType="Student">
INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE) VALUES(#{studId},#{name},#{email},#{phone})
</insert>
这里我们设置一个ID属性为insertStudent,可以在命名空间 com.briup.mappers.StudentMapper.insertStudent中唯一标识该sql语句。
parameterType 属性是一个完全限定类名或者是一个类型别名(alias)。

我们可以如下调用这个语句:
int count = sqlSession.insert("com.briup.mappers.StudentMapper.insertStudent", student);
sqlSession.insert() 方法返回执行 INSERT 语句后所影响的行数。

如果不使用命名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器Mapper 接口,并以类型安全的方式调用方法,如下所示:
package com.briup.mappers;
public interface StudentMapper{
int insertStudent(Student student);
}
你可以如下调用insertStudent映射语句:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int count = mapper.insertStudent(student);

【自动生成主键】
情形1:
数据库默认支持自动生成主键列的值,例如mysql。

在上述的INSERT语句中,我们为可以自动生成(auto-generated)主键的列 STUD_ID 插入值。我们可以使用useGeneratedKeys和keyProperty属性让数据库生成auto_increment列的值,并将生成的值设置到其中一个输入对象属性内,如下所示:

<insert id="insertStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="studId">
INSERT INTO STUDENTS(NAME, EMAIL, PHONE) VALUES(#{name},#{email},#{phone})
</insert>
这里STUD_ID列值将会被数据库自动生成(如mysql),并且生成的值会被设置到student对象的studId属性上。

情形2:
数据库不支持自动生成主键列的值,例如oracle。
但是有些数据库如Oracle并不支持AUTO_INCREMENT列,其使用序列【SEQUENCE】来生成主键值。假设我们有一个名为my_seq的序列来生成SUTD_ID主键值。使用如下代码来生成主键:
drop sequence my_seq;
create sequence my_seq;

<insert id="insertStudent" parameterType="Student">
<selectKey keyProperty="studId" resultType="int" order="BEFORE">
SELECT my_seq.nextval FROM DUAL
</selectKey>
INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE)
VALUES(#{studId},#{name},#{email},#{phone})
</insert>
这里我们使用了<selectKey>子元素来生成主键值,并将值保存到Student对象的studId 属性上。属性order=“before”表示MyBatis将取得序列的下一个值作为主键值,并且在执行INSERT语句之前将值设置到studId属性上。

【注意】
SelectKey需要注意order属性,像MySQL、SQLServer等一类支持自动增长类型的数据库中,order需要设置为after才会取到正确的值。
像Oracle这样取序列的情况,需要设置为before,否则会报错。

3.2.2 UPDATE 更新语句
一个UPDATE SQL语句可以在<update>元素在映射器XML配置文件中配置,如下所示:
<update id="updateStudent" parameterType="Student">
UPDATE STUDENTS SET NAME=#{name}, EMAIL=#{email}, PHONE=#{phone}
WHERE STUD_ID=#{studId}
</update>

我们可以如下调用此语句:
int noOfRowsUpdated = sqlSession.update("com.briup.mappers.StudentMapper.updateStudent", student);
sqlSession.update()方法返回执行UPDATE语句之后影响的行数。

如果不使用名空间(namespace)和语句id来调用映射语句,你可以通过创建一个映射器Mapper接口,并以类型安全的方式调用方法,如下所示:

package com.briup.mappers;
public interface StudentMapper{
int updateStudent(Student student);
}

你可以使用映射器Mapper接口来调用updateStudent语句,如下所示:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int noOfRowsUpdated = mapper.updateStudent(student);

3.2.3 DELETE 删除语句
一个UPDATE SQL语句可以在<update>元素在映射器XML配置文件中配置,如下所示
<delete id="deleteStudent" parameterType="int">
DELETE FROM STUDENTS WHERE STUD_ID=#{id}
</delete>

我们可以如下调用此语句:
int studId = 1;
//delete("命名空间名.方法名",参数);
int noOfRowsDeleted = sqlSession.delete("com.briup.mappers.StudentMapper.deleteStudent", studId);
sqlSession.delete()方法返回 delete 语句执行后影响的行数。

如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安全的方式调用方法,如下所示:
package com.briup.mappers;
public interface StudentMapper{
int deleteStudent(int studId);
}

你可以使用映射器Mapper接口来调用deleteStudent语句,如下所示:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int noOfRowsDeleted = mapper.deleteStudent(studId);

3.2.4 SELECT 查询语句
MyBatis真正【强大】的功能,在于【映射SELECT】查询结果到java的【各种类型】。
让我们看看一个简单的select查询是如何(在MyBatis中)配置的,如下所示:
<select id="findStudentById" parameterType="int"
resultType="Student">
SELECT STUD_ID, NAME, EMAIL, PHONE
FROM STUDENTS
WHERE STUD_ID=#{studId}
</select>
我们可以如下调用此语句:
int studId = 1;
Student student = sqlSession.selectOne("com.briup.mappers. StudentMapper.findStudentById", studId);

如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安全的方式调用方法,如下所示:
package com.briup.mappers;
public interface StudentMapper{
Student findStudentById(Integer studId);
}
你可以使用映射器Mapper接口来调用 findStudentById 语句,如下所示:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.findStudentById(studId);

如果你检查Student对象的属性值,你会发现studId属性值并没有被stud_id列值填充。这是因为MyBatis自动对java对象中和列名匹配的属性进行填充。这就是为什么name,email和 phone属性被填充而studId属性没有被填充。
解决这一问题,我们可以为列名起一个可以与JavaBean中属性名匹配的别名,如下所示:
<select id="findStudentById" parameterType="int"
resultType="Student">
SELECT STUD_ID AS studId, NAME,EMAIL, PHONE
FROM STUDENTS
WHERE STUD_ID=#{studId}
</select>

MyBatis执行返回多条结果的SELECT语句查询,如下所示:
<select id="findAllStudents" resultType="Student">
SELECT STUD_ID AS studId, NAME, EMAIL, PHONE
FROM STUDENTS
</select>

List<Student> students =
sqlSession.selectList("com.briup.mappers.StudentMapper.findAllStudents");

映射器 Mapper 接口 StudentMapper 可以如下定义:
package com.briup.mappers;
public interface StudentMapper{
List<Student> findAllStudents();
}

使用上述代码,我们可以如下调用
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.findAllStudents();

如果你注意到上述的SELECT映射定义,你可以看到,我们为所有的映射语句中的stud_id 起了别名。我们可以使用ResultMaps,来避免上述的到处重复别名。我们稍后会继续讨论。

除了java.util.List,你也可以使用其他类型的集合类,如Set,Map,以及(SortedSet)。MyBatis 根据集合的类型,会采用适当的集合实现,如下所示:
对于List,Collection,Iterable类型,MyBatis将返回java.util.ArrayList
对于Map类型,MyBatis 将返回java.util.HashMap 
对于Set类型,MyBatis 将返回java.util.HashSet
对于SortedSet类型,MyBatis将返回java.util.TreeSet

3.3 结果集映射 ResultMaps
ResultMaps被用来将SELECT语句的结果集映射到java对象的属性中。我们可以定义结果集映射ResultMaps并且在一些SELECT语句上引用resultMap。MyBatis的结果集映射 ResultMaps特性非常强大,你可以使用它将简单的SELECT语句映射到复杂的一对一、一对多关系的SELECT语句上。

3.3.1 简单ResultMap
一个映射了查询结果为Student类型的resultMap定义如下:
<resultMap id="StudentResult" type="com.briup.pojo.Student">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>

<select id="findAllStudents" resultMap="StudentResult">
SELECT * FROM STUDENTS
</select>
<select id="findStudentById" parameterType="int" resultMap="StudentResult">
SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}
</select>

resultMap的id值应该在此名空间内是唯一的,并且type属性是完全限定类名或者是返回类型的别名。
<result>子元素被用来将一个resultset列映射到对象的一个属性中。
<id>元素和<result>元素功能相同,不过<id>它被用来映射到唯一标识属性,用来区分和比较对象(一般和主键列相对应)。
在<select>语句中,我们使用了resultMap属性,而不是resultType属性。当<select>语句中配置了resutlMap属性,MyBatis会使用表中的列名与对象属性 【映射关系】 来填充对象中的属性值。

【注意】:
【resultType和resultMap】二者只能用其一,】不能同时使用】。

【问题升级1】:
<select>映射语句中如何将查询【一条】数据填充到Map中?
<select id="findStudentById" parameterType="int" resultType="map">
SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}
</select>
在上述的<select>语句中,我们将resultType配置成【map,即java.util.HashMap】的别名。在这种情况下,结果集的列名将会作为Map中的key值,而列值将作为Map的value值。

HashMap<String,Object> studentMap = sqlSession.selectOne("com.briup.mappers.StudentMapper.findStudentById", studId);
System.out.println("stud_id :"+studentMap.get("stud_id"));
System.out.println("name :"+studentMap.get("name"));
System.out.println("email :"+studentMap.get("email"));
System.out.println("phone :"+studentMap.get("phone"));

【问题升级2】:
<select>映射语句中如何将查询【多条】数据填充到Map中?
<select id="findAllStudents" resultType="map">
SELECT STUD_ID, NAME, EMAIL, PHONE FROM STUDENTS
</select>

由于resultType=”map”和语句返回多行,则最终返回的数据类型应该是List<Map<String,Object>>,如下所示:

List<Map<String, Object>> studentMapList = sqlSession.selectList("com.briup.mappers.StudentMapper.findAllStudents");
for(Map<String, Object> studentMap : studentMapList) {
System.out.println("studId :" + studentMap.get("stud_id"));
System.out.println("name :" + studentMap.get("name"));
System.out.println("email :" + studentMap.get("email"));
System.out.println("phone :" + studentMap.get("phone"));
}

其他实例1:【结果为对象】
<select id="findAllStudents_student" resultType="Student">
SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB
FROM STUDENTS
</select>
对应的接口中的方法,你写什么类型的集合,Mybatis就给你返回什么类型的集合,但是要注意使用SortedSet的时候,Student类需要实现Comparable接口,否则是不能进行排序的。
例如:
public List<Student> findAllStudents_List();
或者
public Set<Student> findAllStudents_Set();
或者
public SortedSet<Student> findAllStudents_SortedSet();
SortedSet是Set的子接口,TreeSet是其实现子类【需要实现比较接口才可以成功】。

其他实例2:【查询指定列】
<select id="findAllName_list" resultType="String">
SELECT NAME
FROM STUDENTS
</select>
对应的接口中的方法: 把查询到所有名字都放到List集合中并返回
public List<String> findAllName_list();

其他实例3:【查询组函数】
<select id="findCount_int" resultType="int">
SELECT count(*)
FROM STUDENTS
</select>
对应的接口中的方法: 把查询到的这个值直接返回
public int findCount_int();

3.3.2 拓展/继承 ResultMap 【后面知识点,暂不介绍】
(注:这个例子在下面的一对一映射的知识点中进行测试,因为这里需要建立一对一关系的表结构)
我们可以从从另外一个<resultMap>,【拓展】出一个新的<resultMap>,这样,原先的属性映射可以【继承】过来,以实现:
<resultMap type="Student" id="StudentResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>

<!-- Student类中又新增加了一个属性,该属性的类型是Address -->
<!-- 自定义类Address,类中也有多个属性,同时数据库中ADDRESSES表与其对应 -->
<resultMap type="Student" id="StudentWithAddressResult" extends="StudentResult">
<result property="address.addrId" column="addr_id" />
<result property="address.street" column="street" />
<result property="address.city" column="city" />
<result property="address.state" column="state" />
<result property="address.zip" column="zip" />
<result property="address.country" column="country" />
</resultMap>

其中id为StudentWithAddressResult的resultMap拓展了id为StudentResult的resultMap

如果你只想映射Student数据,你可以使用id为StudentResult的resultMap,如下所示:

<select id="findStudentById" parameterType="int"
resultMap="StudentResult">
SELECT * FROM STUDENTS WHERE STUD_ID=#{stud Id}
</select>

如果你想将映射Student数据和Address数据,你可以使用id为StudentWithAddressResult的 resultMap:
<select id="selectStudentWithAddress" parameterType="int"
resultMap="StudentWithAddressResult">
SELECT STUD_ID, NAME, EMAIL, PHONE, A.ADDR_ID, STREET, CITY, STATE, ZIP, COUNTRY
FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON
S.ADDR_ID=A.ADDR_ID
WHERE STUD_ID=#{studId}
</select>
注:该sql语句使用了连接查询中的左外连接,也可以使用等值连接

3.4 一对一映射
Student和Address是一个【一对一】关系
建表语言:
drop table students;
drop table addresses;
如果需要可以使用 cascade constraints;

create table addresses(
addr_id number primary key,
street varchar2(50) not null,
city varchar2(50) not null,
state varchar2(50) not null,
zip varchar2(10),
country varchar2(50)
);

create table students(
stud_id number primary key,
name varchar2(50) not null,
email varchar2(50),
phone varchar2(15),
dob date ,
addr_id number references addresses(addr_id)
);

java类:
public class PhoneNumber {
private String countryCode;
private String stateCode;
private String number;
get/set
}
public class Address{
private Integer addrId;
private String street;
private String city;
private String state;
private String zip;
private String country;
get/set
}
public class Student {
private Integer studId;
private String name;
private String email;
private Date dob;
private PhoneNumber phone;
private Address address;
get/set
}

addresses 表的样例输入如下所示:
addr_id streetcity statezip country
1 redStkunshan W12345 china
2 blueSTkunshan W 12345 china

insert into addresses(addr_id,street,city,state,zip,country) values(1,'redSt','kunshan','W','12345','china');
insert into addresses(addr_id,street,city,state,zip,country) values(2,'blueST','kunshan','W','12345','china');

students 表的样例数据如下所示:
stud_id name emailphoneaddr_id
1 John [email protected] 123-456-7890 1
2 Paul [email protected] 111-222-3333 2

insert into students(stud_id,name,email,phone,addr_id) values(1,'John','[email protected]','123-456-7890',1);
insert into students(stud_id,name,email,phone,addr_id) values(2,'Paul','[email protected]','111-222-3333',2);

【执行select操作】
mapper XML:

<resultMap type="Student" id="StudentWithAddressResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="dob" column="dob" />
<result property="phone" column="phone" />
<result property="address.addrId" column="addr_id" />
<result property="address.street" column="street" />
<result property="address.city" column="city" />
<result property="address.state" column="state" />
<result property="address.zip" column="zip" />
<result property="address.country" column="country" />
</resultMap>

//【多表查询】实现功能
<select id="findStudentByIdWithAddress" parameterType="int" resultMap="StudentWithAddressResult">
select stud_id, name, email, dob, phone, a.addr_id, street, city, state, zip, country
from students s left outer join addresses a on
s.addr_id=a.addr_id
where stud_id=#{id}
</select>
//注意,有歧义的一定要有【别名】,两个表中都有【addr_id】

我们可以使用(【对象.属性名】)的方式为【内嵌的对象】的属性赋值。在上述的resultMap中,Student的address属性使用该方式被赋上了 address 对应列的值。同样地,我们可以访问【任意深度】的内嵌对象的属性。

//接口定义
public interface StudentMapper{
Student selectStudentWithAddress(int studId);
}

//方法调用
int studId = 1;
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.selectStudentWithAddress(studId);
System.out.println("Student :" + student);
System.out.println("Address :" + student.getAddress());

【执行insert操作】
1.映射文件:
<insert id="insertAddress" parameterType="Address">
<selectKey keyProperty="addrId" resultType="int" order="BEFORE">
select my_seq.nextval from dual
</selectKey>
insert into addresses(addr_id,street,city,state,zip,country)
values(#{addrId},#{street},#{city},#{state},#{zip},#{country})
</insert>

<insert id="insertStudent" parameterType="Student">
<selectKey keyProperty="studId" resultType="int" order="BEFORE">
select my_seq.nextval from dual
</selectKey>
insert into students(stud_id,name,email,phone,dob,addr_id)
values(#{studId},#{name},#{email},#{phone},#{dob},#{address.addrId})
</insert>

2.映射接口:
public void insertAddress(Address addr);

public void insertStudent(Student stu);

3.测试代码:
One2OneMapper mapper = session.getMapper(One2OneMapper.class);
Address addr = new Address("xyl", "ks", "sz", "js", "china");
mapper.insertAddress(addr);

Student stu = new Student("zs", "email", new Date(), new PhoneNumber("110-120-119"), addr);
mapper.insertStudent(stu);

session.commit();

上面展示了一对一关联映射的一种方法。然而,使用这种方式映射,如果address结果需要在其他的SELECT映射语句中映射成Address对象,我们需要为每一个语句重复这种映射关系。
MyBatis提供了更好地实现一对一关联映射的方法:【嵌套结果】ResultMap和【嵌套查询】select语句。接下来,我们将讨论这两种方式。

3.4.1 使用【嵌套结果ResultMap】实现一对一关系映射
我们可以使用一个嵌套结果ResultMap方式来获取Student及其Address信息。

映射文件 【映射接口和测试代码 使用之前的即可】
<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>

<resultMap type="Student" id="StudentWithAddressResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="dob" column="dob" />
<result property="phone" column="phone" />
<!-- 【关联】的意思 -->
<association property="address" resultMap="AddressResult" />
</resultMap>

<select id="findStudentByIdWithAddress" parameterType="int" resultMap="StudentWithAddressResult">
select stud_id, name, email, dob, phone, a.addr_id, street, city, state, zip, country
from students s left outer join addresses a
on s.addr_id=a.addr_id
where stud_id=#{studid}
<!-- 也可以不使用左外连接,而使用多表查询【等值连接】实现功能 -->
</select>

【等值连接】
select stud_id, name, email, dob, phone, a.addr_id, street, city, state, zip, country
from students s, addresses a
where s.addr_id=a.addr_id
and stud_id=#{studid}

注:【association是关联】的意思
元素<association>被用来导入“有一个”(has-one)类型的关联。在上述的例子中,我们使用了<association>元素引用了另外的在同一个XML文件中定义的<resultMap>。

同时我们也可以使用<association> 定义【内联的resultMap】,代码如下所示:
<resultMap type="Student" id="StudentWithAddressResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="dob" column="dob" />
<result property="phone" column="phone" />
<association property="address" javaType="Address">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</association>
</resultMap>

3.4.2 使用【嵌套查询select】实现一对一关系映射
嵌套查询本质:
一个select语句 转化成 多条select语句去实现功能。
我们可以通过使用嵌套select查询来获取Student及其Address信息,代码如下:
1.【先根据addr_id去查找 地址对象】
<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>

<select id="findAddressById" parameterType="int" resultMap="AddressResult">
select * from addresses where addr_id=#{id}
</select>

2.【根据学号 查询 学生对象】
<resultMap type="Student" id="StudentWithAddress">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="dob" column="dob" />
<result property="phone" column="phone" />
<!-- 【嵌套查询实现】 -->
<association property="address" column="addr_id" select="findAddressById" />
</resultMap>

<select id="findStudentByIdWithAddress" parameterType="int" resultMap="StudentWithAddress">
select * from students where stud_id=#{id}
</select>

在此方式中,<association>元素的select属性被设置成了id为findAddressById的语句。这里,两个分开的SQL语句将会在数据库中分别执行,第一个调用findStudentById加载student信息,而第二个调用findAddressById来加载address信息。

addr_id列的值将会被作为输入参数传递给selectAddressById语句。

3.【映射接口】
public Address findAddressById(int id);
另外一个映射方法 之前已经实现,不需要再次定义。

4.【测试代码】
测试代码同上。

我们可以如下调用findStudentWithAddress映射语句:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectStudentWithAddress(studId);
System.out.println(student);
System.out.println(student.getAddress());

总结:
嵌套结果映射 本质上是一条sql语句查询多张表;
嵌套查询 本质上是每条sql语句查询一张表,组合在一起查询多张表。
效率上,嵌套结果更快。

结论:
如果是 嵌套结果,通过 association标签中的 resultMap属性 实现;
如果是 嵌套查询,通过 association标签中的 select属性 实现。

*****************************

3.5 一对多映射
【一个讲师】tutors可以教授一个或者【多个课程】course。这意味着讲师和课程之间存在一对多的映射关系。

第一步:建立表结构,并插入数据
注意:在一对多关系中,数据库建表的时候外键一定是在多的那一方建立.
建表语句:
drop table tutors;
drop table courses;
如果需要可以使用 cascade constraints;

create table tutors(
tutor_id number primary key,
name varchar2(50) not null,
email varchar2(50) ,
phone varchar2(15) ,
addr_id number(11) references addresses(addr_id)
);

create table courses(
course_id number primary key,
name varchar2(100) not null,
description varchar2(512),
start_date date ,
end_date date ,
tutor_id number references tutors(tutor_id)
);

tutors 表的样例数据如下:
tutor_id name email phoneaddr_id
1zs [email protected] 123-456-7890 1
2ls [email protected] 111-222-3333 2

插入数据:
insert into tutors(tutor_id,name,email,phone,addr_id)
values(1,'zs','[email protected]','123-456-7890',1);
insert into tutors(tutor_id,name,email,phone,addr_id)
values(2,'ls','[email protected]','111-222-3333',2);

course 表的样例数据如下:
course_id name description start_date end_date tutor_id
1 JavaSE JavaSE 2015-09-10 2016-02-10 1
2 JavaEE JavaEE 2015-09-10 2016-03-10 2
3 MyBatis MyBatis 2015-09-10 2016-02-20 2

插入数据:
insert intocourses(course_id,name,description,start_date,end_date,tutor_id)
values(1,'JavaSE','JavaSE',to_date('2015-09-10','yyyy-mm-dd'),to_date('2016-02-10','yyyy-mm-dd'),1);

insert into courses(course_id,name,description,start_date,end_date,tutor_id)
values(2,'JavaEE','JavaEE',to_date('2015-09-10','yyyy-mm-dd'),to_date('2016-03-10','yyyy-mm-dd'),2);

insert into courses(course_id,name,description,start_date,end_date,tutor_id)
values(3,'MyBatis','MyBatis',to_date('2015-09-10','yyyy-mm-dd'),to_date('2016-02-20','yyyy-mm-dd'),1);

在上述的表数据中,zs 讲师教授一个课程,而 ls 讲师教授两个课程

第二步:建立对应Java类
java代码:
public class Tutor{
private Integer tutorId;
private String name;
private String email;
private PhoneNumber phone;
private Address address;
//【此处注意,一个老师可以教多门课,所以用集合】
private List<Course> courses;

get/set
}

public class Course{
private Integer courseId;
private String name;
private String description;
private Date startDate;
private Date endDate;

get/set
}

【<collection>元素】被用来将多行课程结果映射成一个课程Course对象的一个集合。和一对一映射一样,我们可以使用【嵌套结果ResultMap】和【嵌套查询Select】语句两种方式映射实现一对多映射。

第三步
建立One2MoreMapper.xml映射文件
建立One2MoreMapper.java映射接口

在mybatis-config.xml文件中配置mapper标签,使映射文件生效。

3.5.1 使用嵌套结果 ResultMap 实现一对多映射

第四步:在One2MoreMapper.xml文件中添加以下配置
使用嵌套结果resultMap方式获得讲师及其课程信息,
注意,一个讲师类Tutor 中包含一个地址address 和多门课程 courses。
所以表示讲师的ResultMap里面可以包含一个address和多个course;

代码如下:
<!-- 只要遇到Address对象,就按照AddressResult封装 -->
<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>

<!-- 只要遇到Course对象,就按照CourseResult封装 -->
<resultMap type="Course" id="CourseResult">
<id column="course_id" property="courseId" />
<result column="name" property="name" />
<result column="description" property="description" />
<result column="start_date" property="startDate" />
<result column="end_date" property="endDate" />
</resultMap>

<!-- 只要遇到Tutor类型对象,就按照TutorResult封装 -->
<resultMap type="Tutor" id="TutorResult">
<id column="tutor_id" property="tutorId" />
<result column="name" property="name" />
<result column="email" property="email" />
<result column="phone" property="phone" />
<!-- association表一对一关系,address属性用AddressResult处理 -->
<association property="address" resultMap="AddressResult" />
<!-- collection表一对多关系,遇到courses集合成员 -->
<collection property="courses" resultMap="CourseResult" />
</resultMap>

<!-- 以下为【select语句】通过【左外连接】实现 -->
<select id="findTutorById" parameterType="int" resultMap="TutorResult">
select t.tutor_id, t.name, t.email,t.phone, c.course_id, c.name, description, start_date, end_date , a.addr_id, a.street, a.state, a.zip, a.country
from tutors t left outer join addresses a on t.addr_id=a.addr_id
left outer join courses c on t.tutor_id=c.tutor_id
where t.tutor_id=#{tutorid}
</select>

select语句也可以通过【多表查询】 【等值连接】实现
<select id="findTutorById" parameterType="int" resultMap="TutorResult">
select t.tutor_id,t.name,t.email,t.phone,
a.addr_id,a.street,a.city,a.state,a.zip,a.country,
c.course_id,c.name,c.description,c.start_date,c.end_date
from tutors t , addresses a, courses c
where t.addr_id=a.addr_id
and c.tutor_id=t.tutor_id
and t.tutor_id=#{id}
</select>

【注意】
此时查询出来的结果中,课程name值和讲师name值相同,原因是select中有多个name,产生了歧义,可以使用别名解决,即c.name as 【cname】。

这里我们使用了一个简单的使用了JOINS连接的Select语句获取讲师及其所教课程信息。<collection>元素的resultMap属性设置成了CourseResult,CourseResult包含了Course对象属性与表列名之间的映射。
要查询到Address相关信息,按照上面一对一的方式,在配置中加入<association>。

第五步:添加测试文件,获得session对象,再有session获得mapper对象,然后调用findTutorById(1)方法去获得1号讲师,然后输出。

3.5.2 使用嵌套Select语句实现一对多映射
也可以使用【嵌套Select语句】方式获得讲师及其课程信息
嵌套查询可以理解成子查询,但稍微不同,子查询是先查子句然后再查主句;但此处是先查主句讲师 tutor表信息,得到讲师id后,再据此去查找课程course表信息,同时得到讲师地址id后,再据此查找讲师地址address信息。

第四步:在映射文件One2MoreMapper.xml文件中添加如下代码
<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>
<resultMap type="Course" id="CourseResult">
<id column="course_id" property="courseId" />
<result column="name" property="name" />
<result column="description" property="description" />
<result column="start_date" property="startDate" />
<result column="end_date" property="endDate" />
</resultMap>

<resultMap type="Tutor" id="TutorResult">
<id column="tutor_id" property="tutorId" />
<result column="tutor_name" property="name" />
<result column="email" property="email" />
<result column="phone" property="phone" />
<!--根据表中addr_id去findAddressById,获得address成员信息 -->
<association property="address" column="addr_id" select="findAddressById"></association>
<!-- 【第3执行子句】,把当前tutor_id表中列的值当做参数传递给findCoursesByTutor去查询,得到的结果封装到Tutor类中的courses属性中 -->
<collection property="courses" column="tutor_id" select="findCoursesByTutor" />
</resultMap>

<!-- 此处是select主句,【第1执行】 -->
<select id="findTutorById" parameterType="int" resultMap="TutorResult">
select *
from tutors
where tutor_id=#{tutor_id}
</select>
<!-- 此处是select子句,【第2执行】-->
<select id="findAddressById" parameterType="int" resultMap="AddressResult">
select *
from addresses
where addr_id = #{addr_id}
</select>
<!-- 此处是select子句,【第3执行】-->
<select id="findCoursesByTutor" parameterType="int" resultMap="CourseResult">
select *
from courses
where tutor_id=#{tutor_id}
</select>

注意:
在这种方式中,<aossication>元素的select属性被设置为id为findCourseByTutor的语句,用来触发单独的SQL查询加载课程信息。tutor_id这一列值将会作为输入参数传递给 findCouresByTutor语句。

第五步:去One2MoreMapper.java接口中添加成员方法
public interface TutorMapper{
Tutor findTutorById(int tutorId);
}

第六步:在测试文件中调用方法
TutorMapper mapper = sqlSession.getMapper(TutorMapper.class);
Tutor tutor = mapper.findTutorById(tutor Id);
System.out.println(tutor);
List<Course> courses = tutor.getCourses();
for (Course course : courses){
System.out.println(course);
}
【注意】嵌套查询Select语句查询会导致1+N选择问题。首先,主查询将会执行(1 次),对于主查询返回的每一行,另外一个查询将会被执行(主查询 N 行,则此查询 N 次)。对于大量数据而言,这会导致很差的性能问题。

总之,我们还是推荐【嵌套结果】方式。

3.5 多对多映射
对于在mybatis中的多对多的处理,其实我们可以参照一对多来解决
【注意】在这个例子中有三个字段都是一样的:id,这种情况一定要小心,要给列起别名的(上面的一对一和一对多中如果出现这种情况也是一样的处理方式)

多对多的关系,需要建立第三张桥表 来帮助实现功能。

第一步:建立表结构【此处不用插入数据,通过mybatis编码实现】
建表语句:
drop table student_course;
drop table course;
drop table student;
如果需要可以使用 cascade constraints;

create table course (
id number primary key,
course_code varchar2(30) not null,
course_name varchar2(30) not null
);
create table student (
id number primary key,
name varchar2(10) not null,
gender varchar2(10) ,
major varchar2(10) ,
grade varchar2(10)
);
//学生课程表 就是 桥表
create table student_course (
id number primary key,
student_id number references student(id),
course_id number references course(id)
);

第二步:建立对应Java类 com.briup.many2many
java代码:
public class Course {
private Integer id;
private String courseCode; // 课程编号
private String courseName;// 课程名称
private List<Student> students;// 选课学生
get/set
}
public class Student {
private Integer id;
private String name; // 姓名
private String gender; // 性别
private String major; // 专业
private String grade; // 年级
private List<Course> courses;// 所选的课程
get/set
}

第三步:建立Many2ManyMapper.java接口
public interface Many2ManyMapper {
//插入student数据
public void insertStudent(Student student);
//插入course数据
public void insertCourse(Course course);
//通过id查询学生
public Student getStudentById(Integer id);
//通过id查询课程
public Course getCourseById(Integer id);

//学生x选课y
public void studentSelectCourse(Student student, Course course);
//查询比指定id值小的学生信息
public List<Student> getStudentByIdOnCondition(Integer id);
//查询student级联查询出所选的course并且组装成完整的对象
public Student getStudentByIdWithCourses(Integer id);
}

第四步:在映射文件Many2ManyMapper.xml中进行配置
a.插入学生、课程信息【基本操作】
<insert id="insertStudent" parameterType="Student">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select my_seq.nextval from dual
</selectKey>
insert intostudent(id,name,gender,major,grade)
values(#{id},#{name},#{gender},#{major},#{grade})
</insert>

<insert id="insertCourse" parameterType="Course">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select my_seq.nextval from dual
</selectKey>
insert intocourse(id,course_code,course_name)
values(#{id},#{courseCode},#{courseName})
</insert>

b.Many2ManyMapperTest.java文件中添加测试代码
【插入基本信息到数据库,注意 两个表先不相互包含】
//插入学生
Student s = new Student("lisi", "男", "计算机", "大四");
mapper.insertStudent(s);
session.commit();

//插入课程
mapper.insertCourse(new Course("002","Oracle"));
session.commit();

*****************************************

c.在Many2ManyMapper.xml中配置查询操作
<select id="getStudentById" parameterType="int" resultType="Student">
select id,name,gender,major,grade
from student
where id=#{id}
</select>

<select id="getCourseById" parameterType="int" resultType="Course">
select id,course_code as courseCode,course_name as courseName
from course
where id=#{id}
</select>

d.Many2ManyMapperTest.java文件中添加测试代码
//查学生
Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
Student stu = mapper.getStudentById(22);
System.out.println(stu);

//查课程
Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
Course course = mapper.getCourseById(24);
System.out.println(course);
***************************
在前面所有功能实现完成以后,再做下面的工作

【核心功能】,实现学生选课功能

第五步:映射文件中配置【学生选课】 多对多映射
a.添加xml配置文件
<!-- param1代表方法中第一个参数 以此类推 -->
<insert id="studentSelectCourse">
insert intostudent_course(id,student_id,course_id)
values(my_seq.nextval,#{param1.id},#{param2.id})
</insert>

b.添加测试代码
Student student = mapper.getStudentById(22);
Course course = mapper.getCourseById(24);
mapper.studentSelectCourse(student,course);
session.commit();

************************************

c. 根据条件查找学生信息
<!-- 【注意】如果有特殊符号的话 需要用 <![CDATA[ 特殊符号 ]]> 例如 < & 等等 -->
<select id="getStudentByIdOnCondition" parameterType="int" resultType="Student">
select *
from student
where id <![CDATA[ < ]]> #{id}
</select>
d. 添加测试代码
Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
List<Student> list = mapper.getStudentByIdOnCondition(30);
for (Student student : list) {
System.out.println(student);
}

*************************
第六步:核心功能,查询student级联查询出所选的course并且组装成完整的对象
a.封装基本Student查询结果,不包含Course
<!--
这里使用了嵌套结果ResultMap的方式进行级联查询
当然也可以使用嵌套查询select
-->
<!-- 映射一个基本的Student查询结果 -->
<resultMap id="StudentResult" type="Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="gender" column="gender"/>
<result property="major" column="major"/>
<result property="grade" column="grade"/>
</resultMap>
b.在以上ResultMap基础上,封装完整Student查询结果
<!-- 继承上面那个基本的映射,再扩展出级联查询 -->
<resultMap id="StudentResultWithCourses" type="Student" extends="StudentResult">
<collection property="courses" resultMap="CourseResult"></collection>
</resultMap>
c.单独再封装遇到的Course对象为CourseResult
<!-- 这里特别要是的是column="cid" 这是和select语句中的 c.id as cid对应的 一定一定一定要对应起来 -->
<resultMap id="CourseResult" type="Course">
<id property="id" column="cid"/>
<result property="courseCode" column="course_code"/>
<result property="courseName" column="course_name"/>
</resultMap>
d.书写select查询语句
<!--
注意:查询语句的中的c.id as cid这个地方,避免名字相同出现查询结果不正确的情况
同时在id="CourseResult"的resultMap中也有与这里对应的设置要特别特别注意
-->
<select id="getStudentByIdWithCourses" parameterType="int" resultMap="StudentResultWithCourses">
select s.id,s.name,s.gender,s.major,s.grade,c.id as cid,c.course_code,c.course_name,sc.id,sc.student_id,sc.course_id
from student s,course c,student_course sc
where s.id=#{id}
and s.id=sc.student_id
and sc.course_id=c.id
</select>
e.添加测试代码
Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
Student stu = mapper.getStudentByIdWithCourses(22);
System.out.println(stu);

*************************************************
Many2ManyMapperTest.java中完整测试代码如下:
@Test
public void test_insertStudent(){

SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);

mapper.insertStudent(new Student("张三","男","计算机","大四"));

session.commit();

} catch (Exception e) {
e.printStackTrace();
session.rollback();
}finally {
if(session!=null)session.close();
}

}

@Test
public void test_insertCourse(){

SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);

mapper.insertCourse(new Course("001","corejava"));
mapper.insertCourse(new Course("002","oracle"));

session.commit();

} catch (Exception e) {
e.printStackTrace();
session.rollback();
}finally {
if(session!=null)session.close();
}

}

@Test
public void test_getStudentById() {
SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);

Student stu = mapper.getStudentById(61);
System.out.println(stu);

session.commit();

} catch (Exception e) {
e.printStackTrace();
session.rollback();
}finally {
if(session!=null)session.close();
}
}

@Test
public void test_getCourseById() {
SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);

Course c = mapper.getCourseById(62);
System.out.println(c);

session.commit();

} catch (Exception e) {
e.printStackTrace();
session.rollback();
}finally {
if(session!=null)session.close();
}

}

@Test
public void test_studentSelectCourse(){

SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);

//【注意】一定是 获取已经插入表中的学生 和 课程,然后进行选课
Student student = mapper.getStudentById(58);
Course course = mapper.getCourseById(59);

mapper.studentSelectCourse(student, course);

session.commit();

} catch (Exception e) {
e.printStackTrace();
session.rollback();
}finally {
if(session!=null)session.close();
}

}

@Test
public void test_getStudentByIdOnCondition(){

SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);

List<Student> list = mapper.getStudentByIdOnCondition(100);

for(Student s:list){
System.out.println(s);
}

} catch (Exception e) {
e.printStackTrace();
}finally {
if(session!=null)session.close();
}

}

@Test
public void test_getStudentByIdWithCourses(){

SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);

Student student = mapper.getStudentByIdWithCourses(58);

System.out.println(student);

} catch (Exception e) {
e.printStackTrace();
}finally {
if(session!=null)session.close();
}

}

注:这是从student这边出发所做的一些操作,从course一边开始操作是一样的,因为俩者的关系是多对多(对称的).
同时不论是一对一还是一对多还是多对多,都不能在mybatis中进行级联保存、更新、删除,我们需要使用sql语句控制每一步操作

05-17 20:07