MyBatis的关联查询分为一对一关联查询和一对多关联查询。
- 查询对象时,将关联的另一个对象查询出来,就是一对一关联查询。
- 查询对象时,将关联的另一个对象的集合查询出来,就是一对多关联查询。
例如有学生类和班级类:
一个学生对应一个班级,也就是学生类中有一个班级属性,这就是一对一关系。
一个班级对应多个学生,也就是班级类中有一个学生集合属性,这就是一对多关系。
实体类设计如下:
public class Student {
private int sid;
private String name;
private int age;
private String sex;
private Classes classes;
// 省略getter/setter/toString
}
public class Classes {
private int cid;
private String className;
private List<Student> studentList;
// 省略getter/setter/toString
}
数据库设计如下:
查询学生时,将关联的一个班级对象查询出来,就是一对一关联查询。
public interface StudentMapper {
List<Student> findAll();
}
<resultMap id="studentMapper" type="com.mybatis.pojo.Student">
<id property="sid" column="sid">id>
<result property="name" column="name">result>
<result property="age" column="age">result>
<result property="sex" column="sex">result>
<association property="classes" column="classId" javaType="com.mybatis.pojo.Classes">
<id property="cid" column="cid">id>
<result property="className" column="className">result>
association>
resultMap>
<select id="findAll" resultMap="studentMapper">
select * from student left join classes on student.classId = classes.cid;
select>
<mappers>
<package name="com.mybatis.mapper"/>
mappers>
InputStream is = null;
SqlSession session = null;
@Before
public void before() throws IOException {
is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
session = factory.openSession();
}
@After
public void after() throws IOException {
session.close();
is.close();
}
@Test
public void testFindAllStudent(){
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
List<Student> all = studentMapper.findAll();
all.forEach(System.out::println);
}
查询班级时,将关联的学生集合查询出来,就是一对多关联查询。
public interface ClassesMapper {
List<Classes> findAll();
}
<resultMap id="classesMapper" type="com.mybatis.pojo.Classes">
<id property="cid" column="cid">id>
<result property="className" column="className">result>
<collection property="studentList" column="classId" ofType="com.mybatis.pojo.Student">
<id property="sid" column="sid">id>
<result property="name" column="name">result>
<result property="age" column="age">result>
<result property="sex" column="sex">result>
collection>
resultMap>
<select id="findAll" resultMap="classesMapper">
select * from classes left join student on classes.cid = student.classId;
select>
@Test
public void testFindAllClasses() {
ClassesMapper classesMapper = session.getMapper(ClassesMapper.class);
List<Classes> all = classesMapper.findAll();
all.forEach(System.out::println);
}
MyBatis多对多关联查询本质就是两个一对多关联查询。
例如有老师类和班级类:
一个老师对应多个班级,也就是老师类中有一个班级集合属性。
一个班级对应多个老师,也就是班级类中有一个老师集合属性。
实体类设计如下:
public class Teacher {
private Integer tid;
private String tname;
private List<Classes> classes;
// 省略getter/setter/toString
}
public class Classes {
private Integer cid;
private String className;
private List<Student> studentList;
private List<Teacher> teacherList;
// 省略getter/setter/toString
}
在数据库设计中,需要建立中间表,双方与中间表均为一对多关系。
接下来测试查询老师时,将关联的班级集合查询出来。
public interface TeacherMapper {
List<Teacher> findAll();
}
<resultMap id="teacherMapper" type="com.mybatis.pojo.Teacher">
<id column="tid" property="tid">id>
<result column="tname" property="tname">result>
<collection property="classes" column="tid" ofType="com.mybatis.pojo.Classes">
<id column="cid" property="cid">id>
<result column="className" property="className">result>
collection>
resultMap>
<select id="findAll" resultMap="teacherMapper">
select *
from teacher
left join classes_teacher
on teacher.tid = classes_teacher.tid
left join classes
on classes_teacher.cid = classes.cid
select>
@Test
public void testFindAllTeacher() {
TeacherMapper teacherMapper = session.getMapper(TeacherMapper.class);
List<Teacher> all = teacherMapper.findAll();
all.forEach(System.out::println);
}
如果想查询班级时,将关联的老师集合查询出来,只需要修改班级映射文件的Sql语句和
即可:
<resultMap id="classesMapper" type="com.mybatis.pojo.Classes">
<id property="cid" column="cid">id>
<result property="className" column="className">result>
<collection property="studentList" column="classId" ofType="com.mybatis.pojo.Student">
<id property="sid" column="sid">id>
<result property="name" column="name">result>
<result property="age" column="age">result>
<result property="sex" column="sex">result>
collection>
<collection property="teacherList" column="cid" ofType="com.mybatis.pojo.Teacher">
<id property="tid" column="tid">id>
<result property="tname" column="tname">result>
collection>
resultMap>
<select id="findAll" resultMap="classesMapper">
select *
from classes
left join student
on classes.cid = student.classId
left join classes_teacher
on classes.cid = classes_teacher.cid
left join teacher
on classes_teacher.tid = teacher.tid;
select>
在MyBatis多表查询中,使用连接查询时一个Sql语句就可以查询出所有的数据。如:
# 查询班级时关联查询出学生
select *
from classes
left join student
on student.classId = classes.cid
也可以使用分解式查询,即将一个连接Sql语句分解为多条Sql语句,如:
# 查询班级时关联查询出学生
select * from classes;
select * from student where classId = 1;
select * from student where classId = 2;
这种写法也叫N+1查询。
连接查询:
- 优点:降低查询次数,从而提高查询效率。
- 缺点:如果查询返回的结果集较多会消耗内存空间。
N+1查询:
- 优点:结果集分步获取,节省内存空间。
- 缺点:由于需要执行多次查询,相比连接查询效率低。
我们以查询班级时关联查询出学生为例,使用N+1查询:
public interface ClassesMapper {
// 查询所有班级
List<Classes> findAll();
}
public interface StudentMapper {
// 根据班级Id查询学生
List<Student> findByClassId(int classId);
}
<select id="findAll" resultType="com.mybatis.pojo.Classes">
select * from classes
select>
<select id="findByClassId" resultType="com.mybatis.pojo.Student" parameterType="int">
select * from student where classId = ${classId}
select>
<resultMap id="MyClassesMapper" type="com.mybatis.pojo.Classes">
<id property="cid" column="cid">id>
<result property="className" column="className">result>
<collection property="studentList"
ofType="com.mybatis.pojo.Student" select="com.mybatis.mapper2.StudentMapper2.findByClassId"
column="cid">
collection>
resultMap>
<select id="findAll" resultMap="MyClassesMapper">
select * from classes
select>
@Test
public void testFindAllClasses2(){
ClassesMapper2 classesMapper2 = session.getMapper(ClassesMapper2.class);
List<Classes> all = classesMapper2.findAll();
all.forEach(System.out::println);
}
我们可以看到在控制台打印出了多条Sql语句
查询学生时关联查询出班级也可以使用分解式查询,首先将查询语句分开:
select * from student;
select * from classes where cid = ?;
public interface StudentMapper {
// 查询所有学生
List<Student> findAll();
}
public interface ClassesMapper {
// 根据ID查询班级
Classes findByCid(int cid);
}
<select id="findAll" resultType="com.mybatis.pojo.Student">
select *
from student
select>
<select id="findByCid" resultType="com.mybatis.pojo.Classes" parameterType="int">
select * from classes where cid = ${cid}
select>
<resultMap id="MyStudentMapper" type="com.mybatis.pojo.Student">
<id property="sid" column="sid">id>
<result property="name" column="name">result>
<result property="age" column="age">result>
<result property="sex" column="sex">result>
<association property="classes"
javaType="com.mybatis.pojo.Classes"
select="com.mybatis.mapper2.ClassesMapper2.findByCid"
column="classId">
association>
resultMap>
<select id="findAll" resultMap="MyStudentMapper">
select *
from student
select>
@Test
public void testFindAllStudent2(){
StudentMapper2 studentMapper2 = session.getMapper(StudentMapper2.class);
List<Student> all = studentMapper2.findAll();
all.forEach(System.out::println);
}
分解式查询又分为两种加载方式:
延迟加载在获取关联数据时速度较慢,但可以节约资源,即用即取。
设置所有的N+1查询都为延迟加载:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
settings>
设置某个方法为延迟加载:
在
、
中添加fetchType
属性设置加载方式。lazy
:延迟加载;eager
:立即加载。
@Test
public void testFindAllClasses2(){
ClassesMapper2 classesMapper2 = session.getMapper(ClassesMapper2.class);
List<Classes> all = classesMapper2.findAll();
all.forEach(System.out::println);
System.out.println("-------------------------");
System.out.println(all.get(0).getStudentList());
}
由于打印对象时会调用对象的toString
方法,toString
方法默认会触发延迟加载的查询,所以我们无法测试出延迟加载的效果。
我们在配置文件设置lazyLoadTriggerMethods属性,该属性指定对象的什么方法触发延迟加载,设置为空字符串即可。
<settings>
<setting name="lazyLoadTriggerMethods" value=""/>
settings>
一般情况下,一对多查询使用延迟加载,一对一查询使用立即加载。
MyBatis可以使用注解替代映射文件。映射文件的作用就是定义Sql语句,可以在持久层接口上使用@Select
/@Delete
/@Insert
/@Update
定义Sql语句,这样就不需要使用映射文件了。
创建maven工程,引入依赖
创建mybatis核心配置文件SqlMapConfig.xml
将log4j.properties文件放入resources中,让控制台打印SQL语句。
创建实体类
创建持久层接口,并在接口方法上定义Sql语句
public interface UserMapper {
@Select("select * from user")
List<User> findAll();
}
由于注解在方法上方,而方法中就有参数类型和返回值类型,所以使用注解开发不需要定义参数类型和返回值类型
在核心配置文件注册持久层接口,由于没有映射文件,所以只能采用注册接口或注册包的方法。
<mappers>
<package name="com.mybatis.mapper"/>
mappers>
测试方法
InputStream is = null;
SqlSession session = null;
UserMapper userMapper = null;
@Before
public void before() throws IOException {
is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
session = factory.openSession();
userMapper = session.getMapper(UserMapper.class);
}
@After
public void after() throws IOException {
session.close();
is.close();
}
@Test
public void testFindAll(){
List<User> all = userMapper.findAll();
all.forEach(System.out::println);
}
接下来写一套基于MyBatis注解的增删改查方法:
@SelectKey(keyColumn = "id", keyProperty = "id", resultType = int.class,before = false, statement = "SELECT LAST_INSERT_ID()")
@Insert("insert into user(username,sex,address) values(#{username},#{sex},#{address})")
void add(User user);
@Update("update user set username = #{username},sex=#{sex},address=#{address} where id = #{id}")
void update(User user);
@Delete("delete from user where id = #{id}")
void delete(int id);
@Select("select * from user where username like #{username}")
List<User> findByUsernameLike(String username);
MyBatis注解开发中有两种方式构建动态Sql:
将Sql嵌套在内即可使用动态Sql标签:
// 根据任意条件查询
@Select("")
List<User> findByCondition(User user);
在MyBatis中有@SelectProvider
、@UpdateProvider
、@DeleteProvider
、@InsertProvider
注解。当使用这些注解时将不在注解中直接编写SQL,而是调用某个类的方法来生成SQL。
// 生成根据任意条件查询的Sql语句
public String findByConditionSql(User user){
StringBuffer sb = new StringBuffer("select * from user where 1=1 ");
if (user.getUsername() != null && user.getUsername().length() != 0){
sb.append(" and username like #{username} ");
}
if (user.getSex() != null && user.getSex().length() != 0){
sb.append(" and sex = #{sex} ");
}
if (user.getAddress() != null && user.getAddress().length() != 0){
sb.append(" and address = #{address} ");
}
return sb.toString();
}
当POJO属性名与数据库列名不一致时,需要自定义实体类和结果集的映射关系,在MyBatis注解开发中,使用@Results
定义并使用自定义映射,使用@ResultMap
使用自定义映射,用法如下:
// 查询所有用户
@Results(id = "userDiyMapper" ,value = {
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username1"),
@Result(property = "sex",column = "sex1"),
@Result(property = "address",column = "address1"),
})
@Select("select * from user")
List<User> findAll();
// 根据id查询
@ResultMap("userDiyMapper")
@Select("select * from user where id = #{id}")
User findById(int id);
MyBatis默认开启一级缓存,接下来我们学习如何在注解开发时使用二级缓存:
POJO类实现Serializable接口。
在MyBatis配置文件添加如下设置:
<settings>
<setting name="cacheEnabled" value="true"/>
settings>
在持久层接口上方加注解@CacheNamespace(blocking=true)
,该接口的所有方法都支持二级缓存。
测试二级缓存
@Test
public void testCache() throws IOException {
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
SqlSession session1 = factory.openSession();
SqlSession session2 = factory.openSession();
User user1 = session1.getMapper(UserMapper.class).findById(1);
System.out.println(user1);
System.out.println(user1.hashCode());
session1.commit(); // 清空一次缓存,将数据存到二级缓存
User user2 = session2.getMapper(UserMapper.class).findById(1);
System.out.println(user2);
System.out.println(user2.hashCode());
}
在MyBatis的注解开发中对于多表查询只支持分解查询,不支持连接查询。
创建实体类
public class Student {
private int sid;
private String name;
private int age;
private String sex;
private Classes classes;
// 省略getter/setter/toString
}
public class Classes {
private int cid;
private String className;
private List<Student> students;
// 省略getter/setter/toString
}
创建分解后的查询方法
public interface StudentMapper {
@Select("select * from student")
List<Student> findAll();
}
public interface ClassesMapper {
// 根据id查询班级
@Select("select * from classes where cid = #{cid}")
Classes findByCid(Integer cid);
}
主表的查询配置自定义映射关系
@Select("select * from student")
// 自定义映射关系
@Results(id = "studentMapper",value = {
@Result(id = true,property = "sid",column = "sid"),
@Result(property = "name",column = "name"),
@Result(property = "age",column = "age"),
@Result(property = "sex",column = "sex"),
/**
* property:属性名
* column:调用从表方法时传入的参数列
* one:表示该属性是一个对象
* select:调用的从表方法
* fetchType:加载方式
*/
@Result(property = "classes",column = "classId",
one = @One(select = "com.mybatis.mapper.ClassesMapper.findByCid",
fetchType = FetchType.EAGER))
})
List<Student> findAll();
测试
@Test
public void findAllStudent(){
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
List<Student> all = studentMapper.findAll();
all.forEach(System.out::println);
}
创建分解后的查询方法
public interface ClassesMapper {
// 查询所有班级
@Select("select * from classes")
List<Classes> findAll();
}
public interface StudentMapper {
// 根据班级id查询学生
@Select("select * from student where classId = #{classId}")
List<Student> findByClassId(int classId);
}
主表的查询配置自定义映射关系
// 查询所有班级
@Select("select * from classes")
@Results(id = "classMapper", value = {
@Result(id = true, property = "cid", column = "cid"),
@Result(property = "className", column = "className"),
// many:表示该属性是一个集合
@Result(property = "studentList", column = "cid",
many = @Many(select = "com.mybatis.mapper.StudentMapper.findByClassId",
fetchType = FetchType.LAZY))
})
List<Classes> findAll();
测试
@Test
public void findAllClasses(){
ClassesMapper classesMapper = session.getMapper(ClassesMapper.class);
List<Classes> all = classesMapper.findAll();
all.forEach(System.out::println);
}
MyBatis中更推荐使用映射文件开发,Spring、SpringBoot更推荐注解方式。具体使用要视项目情况而定。它们的优点对比如下:
映射文件:
- 代码与Sql语句是解耦的,修改时只需修改配置文件,无需修改源码。
- Sql语句集中,利于快速了解和维护项目。
- 级联查询支持连接查询和分解查询两种方式,注解开发只支持分解查询。
注解:
- 配置简单,开发效率高。
- 类型安全,在编译期即可进行校验,不用等到运行时才发现错误。