【精选必读】MyBatis关联查询及注解开发
文章目录
MyBatis关联查询
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
}
数据库设计如下:
MyBatis一对一关联查询
查询学生时,将关联的一个班级对象查询出来,就是一对一关联查询。
创建持久层接口
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>
<!-- 一对一对象列 property:属性名 column:关联列名 javaType:对象类型-->
<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);
}
MyBatis一对多关联查询
查询班级时,将关联的学生集合查询出来,就是一对多关联查询。
创建持久层接口
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>
<!-- 集合列 property:属性名 column:关联列名 ofType:集合的泛型 -->
<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多对多关联查询
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>
即可:
<resultMap id="classesMapper" type="com.mybatis.pojo.Classes">
<id property="cid" column="cid"></id>
<result property="className" column="className"></result>
<!-- 集合列 property:属性名 column:关联列名 ofType:集合的泛型 -->
<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分解式查询_一对多
在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>
<!-- select:从表查询调用的方法 column:调用方法时传入的参数字段 -->
<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语句
MyBatis分解式查询_一对一
查询学生时关联查询出班级也可以使用分解式查询,首先将查询语句分开:
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);
}
MyBatis延迟加载
分解式查询又分为两种加载方式:
- 立即加载:在查询主表时就执行所有的Sql语句。
- 延迟加载:又叫懒加载,首先执行主表的查询语句,使用从表数据时才触发从表的查询语句。
延迟加载在获取关联数据时速度较慢,但可以节约资源,即用即取。
开启延迟加载
-
设置所有的N+1查询都为延迟加载:
<settings> <setting name="lazyLoadingEnabled" value="true"/> </settings>
-
设置某个方法为延迟加载:
在
<association>
、<collection>
中添加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注解开发
环境搭建
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);
动态Sql
MyBatis注解开发中有两种方式构建动态Sql:
使用脚本标签
将Sql嵌套在<script>
内即可使用动态Sql标签:
// 根据任意条件查询
@Select("<script>" +
" select * from user\n" +
" <where>\n" +
" <if test=\"username != null and username.length() != 0\">\n" +
" username like #{username}\n" +
" </if>\n" +
" <if test=\"sex != null and sex.length() != 0\">\n" +
" and sex = #{sex}\n" +
" </if>\n" +
" <if test=\"address != null and address.length() != 0\">\n" +
" and address = #{address}\n" +
" </if>\n" +
" </where>" +
"</script>")
List<User> findByCondition(User user);
在方法中构建动态Sql
在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语句集中,利于快速了解和维护项目。
- 级联查询支持连接查询和分解查询两种方式,注解开发只支持分解查询。
注解:
- 配置简单,开发效率高。
- 类型安全,在编译期即可进行校验,不用等到运行时才发现错误。