备注：本文修订于2022年6月29日。1、MyBatis trim 标签Mybatis具有实现动态SQL的能力，但是在拼凑SQL语句的时候，稍有不注意则会画蛇添足，此时可以通过 trim 标签来进行修剪。trim 是“修剪”的意思，其基本格式如下：<trim prefix="" suffix="" suffixOverrides="" prefixOverrides=""></trim>trim 标签的主要属性有四个，如下表所示： 属性名 作用 prefix 给sql增加前缀 suffix 给sql增加后缀 prefixOverrides 去掉sql前面多余的关键字或者字符 suffixOverrides 去掉sql后面多余的关键字或者字符 2、prefix 使用场景介绍 select * from t_users <trim prefix="where"> name = #{name} and age = #{age} and phone = #{phone} </trim> 在动态生成SQL的过程中，会将prefix前缀拼接到trim标签的外侧，最终得到的SQL如下：select * from t_users where name= ? and age= ? and phone = ?3、prefixOverrides 使用场景介绍 select * from t_users <trim prefix="where"> <if test="name != null"> name=#{name} </if> <if test="age != null"> and age=#{age} </if> <if test="phone != null"> and phone=#{phone} </if> </trim> 在动态 SQL 的拼接过程中，如果 name 为 null，则第一个 if 不成立，里面的 SQL 语句不拼接，第二个 if 里面的 and 会紧跟在 where 后面，造成语法错误，最终动态生成的 SQL 如下：select * from t_users where and age = ? and phone = ?为了解决这个问题，只要加上 prefixOverrides 即可： select * from t_users <trim prefix="where" prefixOverrides="and"> <if test="name != null"> name=#{name} </if> <if test="age != null"> and age=#{age} </if> <if test="phone != null"> and phone=#{phone} </if> </trim> 当然，prefix 和 prefixOverrides 可以放在一起综合使用，如下所示：select * from t_users <trim prefix="WHERE" prefixOverrides="AND|OR"> <if test="name != null and name.length() > 0"> AND name=#{name}</if> <if test="gender != null and gender.length() > 0"> AND gender=#{gender}</if> </trim>假如name和gender的值都不为null，打印出的SQL是这样的：select * from t_users where name = ? and gender = ?where后不存在and，这是因为prefixOverrides="AND|OR"代表去掉第一个and或者是or。4、suffix 和 suffixOverrides 使用场景介绍update t_users <trim prefix="set" suffixOverrides="," suffix=" where id = #{id} "> <if test="name != null and name.length() > 0"> name=#{name}, </if> <if test="gender != null and gender.length() > 0"> gender=#{gender}, </if> </trim>假如name和gender的值都不为null，打印出的SQL为：update t_users set name = ? , gender = ? where id = ?trim 标签的作用是：在 gender = ? 后面裁剪了逗号，而且自动加了一个 set 前缀和 where 后缀。5、提醒prefix 是给整个字符串增加一个前缀，而 prefixOverrides 则是去掉整个字符串前面多余的字符。suffix 是给整个字符串增加一个后缀，而 suffixOverrides 则是去掉整个字符串后面多余的字符。其实，上述的处理过程与 set 标签类似，针对的都是整个字符串后面的多余字符。
1、MyBatis bind 标签的作用bind 标签可以用来在映射文件中定义变量，然后将输入参数中的值拼接其他字符串后组成新的字符串赋值给该变量。2、MyBatis bind 标签的应用场景bind 标签最常用的场景是模糊查询。3、MyBatis bind 标签的使用过程第一步：编写接口。public List<Employee> selectEmployeeLikeName(Employee e){}第二步：设置变量。bind 标签可以从 OGNL 表达式中创建一个变量并将其绑定到上下文。如下所示： <bind name="pattern" value="'%'+_parameter.getName() + '%'" /> SELECT * FROM tb_employee WHERE loginname LIKE #{pattern} 备注：参数名称必须是_parameter，表示传入的查询对象Employee，否则会提示异常。第三步：设置查询参数。// 根据传入的参数进行模糊查询 List<Employee> selectEmployeeLikeName(Employee employee); public void testSelectEmployeeLikeName(SqlSession session) { EmployeeMapper em = session.getMapper(EmployeeMapper.class); Employee employee = new Employee(); // 设置模糊查询的参数 employee.setName("o"); List<Employee> list = em.selectEmployeeLikeName(employee); System.out.println(list); }测试selectEmployeeLikeNtme方法，控制台显示如下:DEBUG [main]==> Preparing: SELECT * FROM tb_ employee WHERE loginname LIKE ? DEBUG [main]==> Parameters :%o%(String) DEBUG [main]==> Total :2 Employee [id=2,loginname=rose,password=123456,name=露丝,sex=女,age=21,phone=13902018888,sal=6800.0,state=ACTIVE] Employee [id=3,loginname=tom,password=123456,name=汤姆,sex=男,age=25,phone=13902017777,sal=8800.0,state=ACTIVE]可以看到，执行的sql语句是一个like模糊查询语句，返回的是loginname中包含“o”的所有数据。
备注：本文修改于2022年6月28日。1、MyBatis foreach 标签介绍foreach 标签主要用在构建 in 条件中，它可以在 SQL 语句中遍历一个集合。2、MyBatis foreach 标签属性foreach 标签的属性主要有 collection，item，index，open，separator，close。其含义如下所示：collection：遍历的对象。当遍历对象是List、Array对象时，collection属性值分别默认用"list"、"array"代替，而Map对象没有默认的属性值。注意：可以使用 @Param(“keyName”) 注解来自定义collection属性值，设置keyName后，list、array会失效。item：集合元素遍历时的别名称。该参数为必选项。index：在list、array中，index为元素的序号索引，但是在Map中，index为遍历元素的key值。该参数为可选项。open：遍历集合时的开始符号，通常与 close=")" 搭配使用。使用场景为 IN()、values()时。该参数为可选项。separator：元素之间的分隔符，类比在 IN() 的时候，separator=","，最终所有遍历的元素将会以设定的逗号符号隔开。该参数为可选项。close：遍历集合时的结束符号，通常与 open="(" 搭配使用。该参数为可选项。3、collection 属性值的三种情况如果参数类型为List时，collection的默认属性值为list，同样可以使用@Param注解自定义;如果参数类型为Array时，collection的默认属性值为array，同样可以使用@Param注解自定义;如果传入的参数类型为Map时，collection的属性值可为下面三种情况：遍历 map.keys遍历 map.values遍历 map.entrySet()3.1 参数类型为 List，collection 属性值为 listMapper文件：public List<User> selectByIds(List<Integer> userIds);SQL片段： select * from t_users where id in <foreach collection="list" index="index" item="item" open="(" separator="," close=")"> #{item} </foreach> 3.2 参数类型为 List，使用 @Param 注解自定义 collection 属性值Mapper文件：List<UserList> getUserInfo(@Param("userName") List<String> userName);SQL片段： SELECT * FROM t_users where <if test="userName!= null and userName.size() >0"> USERNAME IN <foreach collection="userName" item="value" separator="," open="(" close=")"> #{value} </foreach> </if> 3.3 参数类型为 Array，collection 属性值为 arrayMapper文件：public List<User> selectByIds(int[] userIds);SQL片段： select * from t_user where id in <foreach collection="array" index="index" item="item" open="(" separator="," close=")"> #{item} </foreach> 3.3 参数类型为 Array，使用 @Param 注解自定义 collection 属性值Mapper文件：List<UserList> getUserInfo(@Param("userName") String[] userName);SQL片段： SELECT * FROM t_users where <if test="userName!= null and userName.length() >0"> USERNAME IN <foreach collection="userName" item="value" separator="," open="(" close=")"> #{value} </foreach> </if> 3.3 参数类型为 Map，collection 属性值分为三种情况Mapper文件：List<UserList> getUserInfo(@Param("user") Map<String,String> user);第一种情况：获取Map的键值对多字段组合条件情况下，一定要注意书写格式：括号() SELECT * FROM t_users where <if test="user!= null and user.size() >0"> (USERNAME,AGE) IN <foreach collection="user.entrySet()" item="value" index="key" separator="," open="(" close=")"> (#{key},#{value}) </foreach> </if> 第二种：参数Map类型，只需要获取key值或者value值 SELECT * FROM t_users where <if test="user!= null and user.size() >0"> (USERNAME) IN <foreach collection="user.keys" item="key" separator="," open="(" close=")"> #{key} </foreach> </if> 第三种：参数Map类型，只需要获取value值 SELECT * FROM t_users where <if test="user!= null and user.size() >0"> (USERNAME) IN <foreach collection="user.values" item="value" separator="," open="(" close=")"> #{key} </foreach> </if> 4、补充介绍关于动态SQL另外一个常用的操作就是需要对一个集合进行遍历，通常发生在构建IN条件语句时。 SELECT * FROM tb_employee WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=") "> #{item} </foreach> foreach元素的功能非常强大，它允许指定一个集合，声明可以用在元素体内的集合项和索引变量。它也允许指定开闭匹配的字符串以及在迭代中间放置分隔符。这个元素是很智能的，因此它不会随机地附加多余的分隔符。// 根据传入的id集合查询员工 List<Employee> selectEmployeeIn(List<Integer> ids);selectEmployeeIn方法传入的参数是一个List集合，该集合中的每一个Integer元素表示需要查询的员工的id。public void testSelectEmployeeIn(SqlSession session) { EmployeeMapper em = session.getMapper(EmployeeMapper.class); // 创建List集合 List<Integer> ids = new Arraylist<Integer>(); // 往List集合中添加两个测试数据 ids.add(1); ids.add(2); List<Employee> list = em.selectEmployeeIn(ids); list.forEach(employee -> System.out.println(employee)); }测试selectEmployeeln方法，控制台显示如下:DEBUG [main]==> Preparing: SELECT * FROM tb employee WHERE ID in( ? , ? ) DEBUG [main]==> Parameters: 1(Integer) , 2 (Integer) DEBUG [main]<== Total: 2 Employee [id=1,loginname=jack,password=123456,name=杰克,sex=男,age=26,phone=13902019999,sal=9800.0,state=ACTIVE] Employee [id=2,loginname=rose,password=123456,namem露丝,sex=女,age=21,phone=13902018888,sal=6800.0,state=ACTIVE]可以看到，执行的sql语句是一个in条件语句，返回的是List集合中的id的员工数据。
MyBatis set 标签set 标签可以被用于动态包含需要更新的列，而舍去其他的，例如多余的逗号。 SELECT * FROM tb_employee where id = #{id} <update id="updateEmployeeIfNecessary" parameterType="cn.mybatis.domain.Employee"> update tb_employee <set> <if test="loginname != null">loginname=#{loginname} ,</if> <if test="password != null">password=#{password} ,</if> <if test="name != null">name=#{name},</if> <if test="sex != null">sex=#{sex},</if> <if test="age != null">age=#{age},</if> <if test="phone != null">phone=#{phone} ,</if> <if test="sal != null">sal=#{sal},</if> <if test="state != null">state=#{state}</if> </set> where id=#{id} </update>set 标签会动态前置SET关键字，同时也会消除无关的逗号，因为使用了条件语句之后很可能就会在生成的赋值语句的后面留下这些逗号。// 根据id查询员工 Employee selectEmployeeWithId(Integer id); // 动态更新员工 void updateEmployeeIfNecessary (Employee employee);需要注意的是，updateEmployeelfNecessar方法传递的参数不是之前使用的HashMap，而是一个Employee对象，因为通常更新操作时都是先查询出一个实体对象再进行更新操作。public void testUpdateEmployeeIfNecessary(qlSession session) { EmployeeMapper em = session.getMapper(EmployeeMapper.class); // 查询id为4的员工信息 Employee employee = em.selectEmployeewithId(4); // 设置需要修改的属性 employee.setLoginname("mary"); employee.setPassword("123"); employee.setName("玛丽"); em.updateEmployeeIfNecessary(employee); }测试updateEmployeelfNecessa方法，控制台显示如下:DEBUG [main]==> Preparing: SELECT * FROM tb_ employee where id= ? DEBUG [main]==> Parameters :4 (Integer) DEBUG [main]<== Total :1 DEBUG [main]==> Preparing :update tb_employee SET loginname=?,password=?,name=?,sex=?,age=?,phone=?,sal=?,state=? where id=? DEBUG [main]==> Parameters: mary(String),123(String),玛丽(String),女(String),20(Integer),13902016666(String),5800.0(Double),ACTIVE(String),4 (Integer) DEBUG [main]<== Updates :1可以看到，首先执行了一条查询语句，查询id为4的员工，之后执行了一条update语句，根据传入的Employee对象更新员工信息。切换到数据库，可以看到id为4的员工信息已经更新。
MyBatis where 标签前面几个例子已经很好地解决了动态SQL问题。现在回到之前的if示例，这次我们将state="ACTIVE"也设置成动态的条件，看看会发生什么。 SELECT * FROM tb_employee WHERE <if test="state != null "> state= #{state} </if> <if test="id != null "> and id = #{id} </if> 如果传入state参数，则执行正常。如果没有传入参数，则会执行sql语句:SELECT * EROM tb_employee WHERE如果只是传入id，则会执行sql语句:SELECT * FROM tb_ employee WHERE and id = ?也就是说，如果没有传入state参数，会导致执行失败。这个问题不能简单地用条件语句来解决。MyBatis有一个简单的处理方法，只要简单地修改就能得到想要的效果： SELECT * EROM tb_employee <where> <if test="state != null "> state = #{state} </if> <if test="id != null "> and id = #{id} </if> <if test="loginname != null and password != null"> and loginname = #{loginname} and password = #{password} </if> </where> where 标签知道只有在一个以上的if条件有值的情况下才去插入WHERE子句。而且，若最后的内容是“AND”或“OR”开头，则where元素也知道如何将它们去除。List<Employee> selectEmployeeLike (HashMap<string,object> params) ; public void testSelectEmployeeLike(SqlSession session) { EmployeeMapper em = session.getMapper(EmployeeMapper.class); HashMap<string, Object> params = new HashMap<string, Object>(); // 设置id,loginname和password属性 params.put("id", 1); params.put("loginname", "jack"); params.put("password", "123456"); List<Employee> list = em.selectEmployeelike(params); list.forEach(employee -> System.out.printIn(employee)); } 测试selectEmployeeLike方法，控制台显示如下:DEBUG [main]==> Preparing: SELECT * FROM tb_employee WHERE id = ? and loginname= ? and password = ? DEBUG [main]==> Parameters: 1(Integer),jack(String),123456(string) DEBUG [main]==> Total: 1 Employee [id=1,paasword=12345,name=杰克,sex=男,age=26] loginnamejack, phone=13902019999,sal=9800.0,state-ACTIVE]可以看到，当没有传入 state 参数时，MyBatis 自动过滤掉 id 前面的 and 关键字。MyBatis where 标签的局限性where 标签只能去除第一个条件中出现的前置 and 关键字。像以下情况，where 就无能为力了: SELECT * EROM tb_employee <where> <if test="state != null "> state = #{state} and </if> <if test="id != null "> id = #{id} and </if> <if test="loginname != null and password != null"> loginname = #{loginname} and password = #{password} </if> </where> where 标签无法去除掉后面的 and 关键字，此时 SQL 语句出现语法错误。要想解决以上 where 标签无法处理的问题，可以考虑使用 trim 标签。
MyBatis choose、when、otherwise 标签有些时候，我们不想使用所有的条件语句，而只想从中择其一二。针对这种情况，MyBatis提供了choose标签，它有点像Java中的switch语句。还是上面的例子，但是这次变为：如果客户端提供了 id 就按 id 查找，提供了 loginname 和 password 就按 loginnam 和password 查找，若两者都没有提供，就返回所有sex = '男'的 Employee。 SELECT * FROM tb employee WHERE state = 'ACTIVE' <choose> <when test="id != null"> and id= #{id} </when> <when test="loginname != null and password != null"> and loginname = #{loginname} and password = #{password} </when> <otherwise> and sex = '男' </otherwise> </choose> List<Employee> selectEmployeeChoose(HashMap<String,Object> params); public void testSelectEployeeChoose(SqlSession session) { EmployeeMapper em = session.getMapper(EmployeeMapper.class); HashMap<String, Object> params = new HashMap<String, object>(); // 设置id属性 params.put("id", 1); params.put("loginnamc", "jack"); params.put("password", "123456"); List<employee> list = em.selectEmployeechoose(params); list.forEach(employee -> Systen.out.printIn(employee)); } 运行main 方法，尝试selectEmployeeChoose()方法，控制台显示如下:DEBUG [main]==> Preparing: STTR.CT FROM tb_employee WHERE state = 'ACTIVE' and loginname = ? and password = ? DEBUG [main]==> Parameters: jack(String),123456(String) DEBUG [main]==> Total :1 [Employee [id=1,loginname=jack,passwiord=123456,name=马云,sex=男,age=26,phone=13902019999,sal=9800.0,state=ACTIVE] ]可以看到，HashMap里面传递的参数包括了id、loginname和password，执行的sql语句中因为使用了choose标签，所以sql语句执行的是按照id查询，查询返回的Emplyee对象就是id为1的对象。接下来注释以下代码:// params.put("id",1) ;再次执行main方法，控制台显示如下:DEBUG [main]==> Prcparing: SEIECT * FROM tb_employee WHERE state = 'ACTIVE' and login name= ? and password = ? DEBUG [main]==> Parameters: jack(String) ,123456(String) DEBUG [main]<== Total :1 Employee [id=1,loginname=jack,passwiord=123456,name=马云,sex=男,age=26,phone=13902019999,sal=9800.0,state=ACTIVE]可以看到，HashMap里面传递的参数只包括loginname和password，所以sql语句是按照loginname和password查找，查询返回的Emplyee对象就是loginname，即jack，并且password是123456的对象。接下来注释以下代码:// params.put("id",1); // params.put("loginname","jack"); // params.put("password","123456");再次执行main 方法，控制台显示如下:DEBUG [main]==> Preparing: SELECT * FROM tb_employee WHERE state = 'ACTIVE' and sex = '男' DEBUG [main]==> Parameters: DEBUG [main]==> Total :2 Employee [id=l,name= 杰克,sex=男,age=26,password=123456,loginname=jack,phone=13902019999,sal=9800.0,state=ACTIVE] Employee [id=3,name=汤姆,sex=男,age=25,loginname=tom,password=123456,phone=13902017777,sal=8800.0,state=ACTIVE]可以看到，由于没有传递任何参数，故sql语句执行的是<otherwise>元素里面的查询条"sex='男'"的对象。
MyBatis if 标签if 标签在mybatis的开发工作中主要用于where查询、insert插入和update更新三种操作中，本文接下来会对每种操作中的 if 标签做详细讲述.where 查询中使用<if>标签:<mapper namespace="cn.mybatis.mapper.EmployeeMapper"> SELECT * FROM tb_employee WHERE state = 'ACTIVE' <if test="id != null"> and id = #{id} </if> </mapper>以上语句提供了一个可选的根据id查找Employee的功能。如果没有传入id，那么所有处于"ACTIVE"状态的Employee都会被返回。反之，若传入了id，那么就会把查找id内容的Employee结果返回。public Interface EmployeeMapper { List<Employee> selectEmployeeByIdLike (HashMap<String,Object> params); }以上代码提供了一个和EmployeeMapper.xml中的select元素的id同名的方法，需要注意的是，selectEmployeeByIdLike 接收一个 HashMap 作为参数。public static void main(string[] args) throws Exception { // 读取mybatis-config.xml 文件 Inputstream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 初始化mybatis,创建SqlSessionFactory 类的实例 sqlsessionFactory sqlSessionEactory = new sqlsessionFactoryBullder().build(inputStream); // 创建Session实例 sqlSesaion seasion = sqlsessionFactory.openSession(); DynamicsQlTest t = new DynamicSQITest(); t.testSelectEmployeeByIdLike(sesion); // 提交事务 sesion.commit(); // 关闭Session session.close(); } // 测试 public void testSelectemployeeByIdLike(Sqlsession session) { // 获得EmployeeMapper接口的代理对象 EmployeeMapper em = session.getMapper(EmployeeMapper.class); // 创建一个HashMap存储参数 HashMap<String, Object> params = new HashMap<String, Object>(); // 设置id属性 params.put("id", 1); // 调用EmployeeMapper接口的selectEmployeeByIdLike方法 List<Employee> list = em.selectEmployeeByIdLike(params); // 查看查询结果 list.foEach(employee -> System.out.printIn(employee)); } 运行上面的main方法，其通过SqlSession的getMapper(Class<T> type)方法获得mapper接口的代理对象EmployeeMapper。调用selctEmployeeByIdLike方法时会执行EmployeeMapper.xm中 元素中定义的sql语句。控制台显示如下:DEBUG [main]==> Preparing: SELBCT * FRON tb_employee WHERE state = 'ACTIVE' and id= ? DEBUG [main]==> Parameters: DEBUG [main]<== Total:2 Employee [id=1,loginname=jack,password=123456,name=马云,sex=男,age=26,phone=13900000001,sal=9800.0,state=ACTIVE]可以看到，执行的sql语句中因为传入了id属性，所以sql语句中包含了“and id= ?”，查询返回的Emplyee对象就是"id"为1的对象。接下来注释以下代码：// params.put("id",1);再次执行main方法，控制台显示如下:DEBUG [main]==> Breparing: SBLECT * FROM tb_employee WHERE state = 'ACTVE' DBBUG [main]--> Parameters: DEBUG [main]<== Total:4 DEBUG [main]<== Employee [id=1,loginname=jack,password-123456,name=马云,sex=男,age=26,phone=13900000001,sal=9800.0,state=ACTIVE] Employee [id=2,loginname=rose,password-123456,nanme=黛西,sex=女,age=21,phone=13900000002,sa1=6800.0,state=ACTIVE] Employee [id-3,loginname=tom,password=123456,name=汤姆,sex=男,age=25，phone=13900000003,sa1=8800.0,state=ACTIVE] Employee [id-4,loginname=alice,password=123456,name=爱丽丝,sex=女,age=20,phone=13900000004,sal=5800.0,state=ACTIVE]可以看到，由于传递的HashMap中没有id属性，故执行的sql语句中不再包含“and id=?”，查询返回了所有state='ACTIVE'的数据。如果想通过两个条件搜索该怎么办呢? 很简单，只要多加入一个条件即可。 SELECT * FROM tb_employee WHERE state = 'ACTIVE' <if test="loginname != null and password != null"> and loginname = #{loginname} and password = #{password) </if> List<Employee> selectEmployeeByLoginLike(HashMap<String,Object> params) ; public void testSelectEmployeeByLoginLike(SqlSession session) { EmployeeMapper em = session.getMapper(EmployeeMapper.class); HashMap<String, Object> params = new HashMap<String, Object>(); // 设置loginname 和password属性 params.put("loginname", "jack"); params.put("password", "1245"); List<Employee> list = em.selectEmployeeByLoginlike(param); list.forEach(employee -> System.out.println(employee)); } 运行上面的main方法，测试selectEmployeeByLoginLike方法，控制台显示如下:DBBUG [main]==> Preparing: SBLECT * EROM tb_employee WHERE state = 'ACTIVE' and loginname = ? and password = ? DEBUG [main]==> Parameters: jack(String),123456(String) DEBUG [main]==> Total :1 Employee [id=1,loginname=jack,password=123456,name=马云,sex=男,age=26,phone=13900000001,sal=9800.0,state=ACTIVE]可以看到，执行的sql语句中因为传入了loginname和password属性，故sql语句中包含了"and loginname = ? and password = ? "，查询返回的Emplyee对象就是logimame，即jack，并且password是123456的对象。insert 新增 中使用<if>标签:通过判断参数值是否为空来决定是否将SQL字段和对象加入到SQL语句中:<mapper namespace="cn.mybatis.mapper.EmployeeMapper"> <insert id="insertEmployee" useGeneratedKeys="true" keyProperty="id"> insert into tb_employee ( <if test="loginname != null and loginname != ''"> loginname, </if> <if test="password != null and password != ''"> password, </if> <if test="name != null and name != ''"> name, </if> status ) values ( <if test="loginname != null and loginname != ''"> #{loginname}, </if> <if test="password != null and password != ''"> #{password}, </if> <if test="name != null and name != ''"> #{name}, </if> #{status} ) </insert> </mapper>update修改中使用 if 标签通过判断参数值是否为空来决定是否将SQL字段和对象加入到SQL语句中:<mapper namespace="cn.mybatis.mapper.EmployeeMapper"> <update id="updateEmployee"> update tb_employee <set> <if test="loginname != null and loginname !=''"> loginname = #{loginname}, </if> <if test="password != null and password != ''"> password = #{password}, </if> <if test="name != null and name != ''"> name = #{name}, </if> <if test="status != null and status != ''"> status = #{status}, </if> id = #{id} where id = #{id} </set> </update> </mapper>
本文修订于2019年3月17日SQL脚本在实际项目开发中，多对多关系也是非常常见的关系，比如，一个购物系统中，一个用户可以有多个订单，这是一对多的关系；一个订单中可以购买多种商品，一种商品也可以属于多个不同的订单，订单和商品就是多对多的关系。对于数据库中多对多关系建议使用一个中间表来维护关系，中间表中的订单d作为外键参照订单表的id，商品id作为外键参照商品表的id。下面我们就用一个简单示例来看看MyBatis怎么处理多对多关系。首先，在数据库创建三个表：TB_USER、TB_ARTICLE和TB_ORDER，再创建一个中间表维护TB_ARTICLE和TB_ORDER的关系，并插入测试数据。SQL脚本如下:drop table if exists tb_user; -- 创建用户表 CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(18), loginname VARCHAR(18), PASSWORD VARCHAR(18), phone VARCHAR(18), address VARCHAR(18) ); -- 插入用户表测试数据 INSERT INTO tb_user (username,loginname,PASSWORD,phone,address) VALUES('马云','jack','123456','13600000000','杭州'); drop table if exists tb_article; -- 创建商品表 CREATE TABLE tb_article( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(18), price DOUBLE, remark VARCHAR(18) ); -- 插入商品表测试数据 INSERT INTO tb_article(NAME,price,remark) VALUES('精通Python自然语言处理',108.9,'经典著作'); INSERT INTO tb_article(NAME,price,remark) VALUES('自然语言处理原理与实践',99.9,'经典著作'); INSERT INTO tb_article(NAME,price,remark) VALUES('大数据架构详解',89.9,'经典著作'); INSERT INTO tb_article(NAME,price,remark) VALUES('推荐系统实践',69.9,'经典著作'); drop table if exists tb_order; -- 创建订单表 CREATE TABLE tb_order( id INT PRIMARY KEY AUTO_INCREMENT, CODE VARCHAR(32), total DOUBLE, user_id INT, FOREIGN KEY (user_id) REFERENCES tb_user(id) ); -- 插入订单表测试数据 INSERT INTO tb_order(CODE,total,user_id)VALUES('20180315ORDER1212',388.6,1); INSERT INTO tb_order(CODE,total,user_id)VALUES('20180315ORDER1213',217.8,1); drop table if exists tb_item; -- 创建中间表 CREATE TABLE tb_item( order_id INT, article_id INT, amount INT, PRIMARY KEY (order_id,article_id), FOREIGN KEY (order_id) REFERENCES tb_order(id), FOREIGN KEY (article_id) REFERENCES tb_article(id) ); -- 创建插入中间表数据 INSERT INTO tb_item(order_id,article_id,amount)VALUES(1,1,1); INSERT INTO tb_item(order_id,article_id,amount)VALUES(1,2,1); INSERT INTO tb_item(order_id, article_id,amount)VALUES(1,3,2); INSERT INTO tb_item(order_id, article_id,amount)VALUES(2,4,2); INSERT INTO tb_item(order_id, article_id,amount)VALUES(2,1,1); 提示:t_order表的user_id作为外键参照tb_user表的主键id。tb_item表作为中间表，用来维护tb_article和tb_order的多对多关系，tb_item表的order_id作为外键参照tb_order表的主键id，article_id作为外键参照tb_article 表的主键id实体类接下来， 建一个User对象、一个Article对象和一个Order对象分别映射tb_user、tb_article和tb_order表。import java.io.Serializable; import java.util.List; public class User implements Serializable { private static final long serialVersionUID = 1L; private Integer id; // 用户id，主键 private String username; // 用户名 private String loginname; // 登录名 private String password; // 密码 private String phone; // 联系电话 private String address; // 收货地址 // 用户和订单是一对多的关系，即一个用户可以有多个订单 private List<Order> orders; public User() { super(); } public User(String username, String loginname, String password, String phone, String address) { super(); this.username = username; this.loginname = loginname; this.password = password; this.phone = phone; this.address = address; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getLoginname() { return loginname; } public void setLoginname(String loginname) { this.loginname = loginname; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public List<Order> getOrders() { return orders; } public void setOrders(List<Order> orders) { this.orders = orders; } @Override public String toString() { return "User [id=" + id + ", username=" + username + ", loginname=" + loginname + ", password=" + password + ", phone=" + phone + ", address=" + address + "]"; } } 用户和订单是一对多的关系，即一个用户可以有多个订单。在User类中定义了一个orders属性，该属性是一个List集合，用来映射一对多的关联关系，表示一个用户有多个订单。import java.io.Serializable; import java.util.List; public class Order implements Serializable { private static final long serialVersionUID = 1L; private Integer id; // 订单id，主键 private String code; // 订单编号 private Double total; // 订单总金额 // 订单和用户是多对一的关系，即一个订单只属于一个用户 private User user; // 订单和商品是多对多的关系，即一个订单可以包含多种商品 private List<Article> articles; public Order() { super(); } public Order(String code, Double total) { super(); this.code = code; this.total = total; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Double getTotal() { return total; } public void setTotal(Double total) { this.total = total; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public List<Article> getArticles() { return articles; } public void setArticles(List<Article> articles) { this.articles = articles; } @Override public String toString() { return "Order [id=" + id + ", code=" + code + ", total=" + total + "]"; } } 订单和用户是多对一的关系，一个订单只属于一个用户，在Order类中定义了一个user属性，用来映射多对一的关联关系，表示该订单的用户；订单和商品是多对多的关系，即一个订单中可以包含多种商品，在Order类中定义了一个articles属性，该属性是一个List集合，用来映射多对多的关联关系，表示一个订单中包含多种商品。import java.io.Serializable; import java.util.List; public class Article implements Serializable { private static final long serialVersionUID = 1L; private Integer id; // 商品id，主键 private String name; // 商品名称 private Double price; // 商品价格 private String remark; // 商品描述 // 商品和订单是多对多的关系，即一种商品可以包含在多个订单中 private List<Order> orders; public Article() { super(); } public Article(String name, Double price, String remark) { super(); this.name = name; this.price = price; this.remark = remark; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public List<Order> getOrders() { return orders; } public void setOrders(List<Order> orders) { this.orders = orders; } @Override public String toString() { return "Article [id=" + id + ", name=" + name + ", price=" + price + ", remark=" + remark + "]"; } } 商品和订单是多对多的关系，即一种商品可以出现在多个订单中。在Article 类中定义了一个orders 属性，该属性是一个List集合，用来映射多对多的关联关系，表示该商品关联的多个订单。映射文件<mapper namespace="cn.mybatis.mydemo3.mapper.UserMapper"> <resultMap type="cn.mybatis.mydemo3.domain.User" id="userResultMap"> <id property="id" column="id" /> <result property="username" column="username" /> <result property="loginname" column="loginname" /> <result property="password" column="password" /> <result property="phone" column="phone" /> <result property="address" column="address" /> <collection property="orders" javaType="ArrayList" column="id" ofType="cn.mybatis.mydemo3.domain.Order" select="cn.mybatis.mydemo3.mapper.OrderMapper.selectOrderByUserId" fetchType="lazy"> <id property="id" column="id" /> <result property="code" column="code" /> <result property="total" column="total" /> </collection> </resultMap> SELECT * FROM tb_user WHERE id = #{id} </mapper> UserMapper.xml中定义了一一个<select.../>，其根据id查询用户信息。由于User 类除了简单的属性id、usemame、loginame、password和address之外，还有一个关联对象orders，所以返回的是一个名为userResultMap的resultMap。由于orders是一个List集合，因此userResultMap 中使用了<collection.../>元素映射一对多的关联关系，select属性表示会使用columm属性的id值作为参数执行OrderMapper中定义的selectOrderByUserId查询该用户所下的所有订单，查询出的数据将被封装到property表示的orders对象当中。注意，一对多使用的都是lazy(懒加载)。<mapper namespace="cn.mybatis.mydemo3.mapper.OrderMapper"> <resultMap type="cn.mybatis.mydemo3.domain.Order" id="orderResultMap"> <id property="id" column="oid" /> <result property="code" column="code" /> <result property="total" column="total" /> <association property="user" javaType="cn.mybatis.mydemo3.domain.User"> <id property="id" column="id" /> <result property="username" column="username" /> <result property="loginname" column="loginname" /> <result property="password" column="password" /> <result property="phone" column="phone" /> <result property="address" column="address" /> </association> <collection property="articles" javaType="ArrayList" column="oid" ofType="cn.mybatis.mydemo3.domain.Article" select="cn.mybatis.mydemo3.mapper.ArticleMapper.selectArticleByOrderId" fetchType="lazy"> <id property="id" column="id" /> <result property="name" column="name" /> <result property="price" column="price" /> <result property="remark" column="remark" /> </collection> </resultMap> SELECT u.*,o.id AS oid,CODE,total,user_id FROM tb_user u,tb_order o WHERE u.id = o.user_id AND o.id = #{id} SELECT * FROM tb_order WHERE user_id = #{id} </mapper> OrderMapper.xml中定义了一个 其根据用户id查询订单信息，返回的是简单的Order对象。还定义了一 ,其根据订单id 查询订单信息，由于 Order类和用户是多对一关系，和商品是多对多关系，而多对一通常都是立即加载，因此SQL语句是一条关联了tb_user和tb_order的多表查询语句。查询结果返回一个名为orderResultMap的resultMap。orderResultMap中使用了< association>元素映射多对一的关联关系，其将查询到的用户信息装载到Order 对象的user属性当中;orderResutMap中还使用了<collection..>元素映射多对多的关联关系，select属性表示会使用column 属性的oid 值作为参数执行ArticleMapper 中定义的selectArticleByOrderd 查询该订单中的所有商品，查询出的数据将被封装到property表示的articles对象当中。注意，一对多使用的都是lazy(懒加载)。提示:因为多表查询返回的结果集中tb user有个id列，tb_order也有个id列，当列同名时，MyBatis使用的元素中的column属性如果是id，则MyBatis会默认使用查询出的第一个id列。为了区分同名的列，最好的方法是给列取一个别名。SQL 语句中的o.id AS o.oid，resultMap中的column="oid"就是指使用的是tb_order 表的id:<mapper namespace="cn.mybatis.mydemo3.mapper.ArticleMapper"> SELECT * FROM tb_article WHERE id IN ( SELECT article_id FROM tb_item WHERE order_id = #{id} ) </mapper> ArticleMape.xml 中定义了一个 其根据订单id查询订单关联的所有商品，由于订单和商品是多对多的关系，数据库使用了一一个中间表tb_item维护多对多的关系，故此处使用了一个子查询，首先根据订单id定位到中间表中查询出所有的商品，之后根据所有商品的id 查询出所有的商品信息，并将这些信息封装到Atrticle对象当中。映射接口import cn.mybatis.mydemo3.domain.User; public interface UserMapper { User selectUserById(int id); } import cn.mybatis.mydemo3.domain.Order; public interface OrderMapper { Order selectOrderById(int id); } import cn.mybatis.mydemo3.domain.Article; public interface ArticleMapper { Article selectArticleByOrderId(int id); } 测试类 public class App { public static void main(String[] args) throws Exception { // 读取mybatis-config.xml文件 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 初始化mybatis，创建SqlSessionFactory类的实例 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 创建Session实例 SqlSession session = sqlSessionFactory.openSession(); App a = new App(); // 根据用户id查询用户，测试一对多 // a.testSelectUserById(session); // 根据订单id查询订单，测试多对多 a.testSelectOrderById(session); // 提交事务 session.commit(); // 关闭Session session.close(); } // 测试一对多，查询班级User（一）的时候级联查询订单Order（多） public void testSelectUserById(SqlSession session) { // 获得UserMapper接口的代理对象 UserMapper um = session.getMapper(UserMapper.class); // 调用selectUserById方法 User user = um.selectUserById(1); // 查看查询到的user对象信息 System.out.println(user.getId() + " " + user.getUsername()); // 查看user对象关联的订单信息 List<Order> orders = user.getOrders(); for (Order order : orders) { System.out.println(order); } } // 测试多对多，查询订单Order（多）的时候级联查询订单的商品Article（多） public void testSelectOrderById(SqlSession session) { // 获得OrderMapper接口的代理对象 OrderMapper om = session.getMapper(OrderMapper.class); // 调用selectOrderById方法 Order order = om.selectOrderById(2); // 查看查询到的order对象信息 System.out.println(order.getId() + " " + order.getCode() + " " + order.getTotal()); // 查看order对象关联的用户信息 User user = order.getUser(); System.out.println(user); // 查看order对象关联的商品信息 List<Article> articles = order.getArticles(); for (Article article : articles) { System.out.println(article); } } } 运行App类的main方法，首先测试testSelectUserById方法，根据用户id查询用户。控制台显示如下:DEBUG [main]--> Preparing: SELBCT * FROM tb_user WHERE id = ?DEBUG [main]==> Parameters: 1(Integer)DEBUG [main]<-- Total : 11 马云DEBUG [main]--> Preparing: SELBCT * FROM tb_order WHERE user_ id = ?DEBUG [main]--> Parameters: 1(Integer)DEBUG [mainj<== Total : 2Order [id=1,code=20180315ORDER1212,tota1=388.6]order [1d=2,code=20180315ORDER1213,total=217.81]可以看到，MyBatis执行了根据用户id查询用户的SQL语句，查询出了用户信息; 由于测试方法中立即又获取了用户的订单集合，故MyBatis又执行了根据用户id查询订单的SQL语句，查询出了该用户的两个订单。接下来测试testSelectOrderById方法，根据订单id查询订单信息。控制台显示如下:DEBUG Emain]-->Preparing: SELECT u.*, o.id AS oid, CODE, total, user_id FROM tb.user u, tb.order o WHERE u.id = o.user_id AND o.id = ?DEBUG (main]--> Parameters: 2(Integer)DEBUG [main]<==2 20180315ORDER1213 217.8User [id=1,username=马云,loginame=jack,password=123456,phone=13600000000,address=杭州]可以看到，MyBatis执行了一个多表连接查询，同时查询出了订单信息和用户信息，由于测试方法中注释了查询订单中的商品代码，故MyBatis采用了懒加载机制，没有立即查询商品信息。取消testSelectOrderByld方法中查询订单中的商品的代码注释，再次执行。控制台显示如下:DEBUG [main]--> Preparing: SBLECT u.*, o.id AS o.oid, CODE, total, user_id FROM tb user u, tb_order o WHERE u.id = o.user_id AND o_id = ?DEBUG [main]--> Parameters: 2(Integer)DEBUG [main]<== Total: 12 20180315ORDER1213 217.8User [id=1,username=马云,loginname=jack,password=123456,phone-13600000000,address=杭州]DEBUG [main]--> Preparing: SELBCT* FROM tb_article WHERE id IN (SEIBCT artiche_id FROM tb_item WHERB order_id = ?)DEBUG [main]--> Parameters: 2(Integer)DEBUG [main]<== Total: 2Article [id=1,name=精通Python自然语言处理,price=108.9,remark=经典著作]Article [id=4,name=推荐系统实践,price=69.9,remark=经典著作]可以看到，MyBatis 执行了ArticleMapper.xm中定义的子查询，查询出了订单所关联的所有商品信息。提示:多对多查询因为关联到中间表查询，所以读者需要对数据库的SQL知识有一定的了解。
本文修订于2019年3月17日SQL脚本在实际项目开发中，一对多是非常常见的关系，比如，一个班级可以有多个学生，一个学生只能属于一个班级，班级和学生是一对多的关系，而学生和班级是多对一的关系。数据库中一对多关系通常使用主外键关联，外键列应该在多方，即多方维护关系。下面我们就用一个简单示例来看看MyBatis怎么处理一对多关系。首先，给在数据库创建两个表tb_clazz 和tb_student，并插入测试数据。SQL脚本如下:drop table if exists tb_clazz; CREATE TABLE tb_clazz( id INT PRIMARY KEY AUTO_INCREMENT, CODE VARCHAR(18), NAME VARCHAR(18) ); INSERT INTO tb_clazz(CODE,NAME) VALUES('201803','计算机专业'); drop table if exists tb_student; CREATE TABLE tb_student( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(18), sex VARCHAR(18), age INT, clazz_id INT, FOREIGN KEY(clazz_id) REFERENCES tb_clazz(id) ); INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('马云','男',23,1) ; INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('李飞飞','女',23,1) ; INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('马化腾','男',23,1) ; INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('柳青','女',23,1) ; 提示:tb_student表的clazz_id作为外键参照tb_clazz表的主键id。实体类在数据库中执行SQL脚本，完成创建数据库和表的操作。接下来，创建一个Clazz对象和一个Student对象分别映射tb_clazz和tb_student表。班级和学生是一对多的关系，即一个班级可以有多个学生。在Clazz类当中定义了一个students属性，该属性是一个List集合，用来映射一对多的关联关系，表示一个班级有多个学生。import java.io.Serializable; import java.util.List; public class Clazz implements Serializable { private static final long serialVersionUID = 1L; private Integer id; // 班级id，主键 private String code; // 班级编号 private String name; // 班级名称 // 班级和学生是一对多的关系，即一个班级可以有多个学生 private List<Student> students; public Clazz() { super(); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Student> getStudents() { return students; } public void setStudents(List<Student> students) { this.students = students; } @Override public String toString() { return "Clazz [id=" + id + ", code=" + code + ", name=" + name + "]"; } } 班级和学生是一对多的关系，即一个班级可以有多个学生。在Clazz类当中定义了一个students属性，该属性是一个List集合，用来映射一对多的关联关系，表示一个班级有多个学生。import java.io.Serializable; public class Student implements Serializable { private static final long serialVersionUID = 1L; private Integer id; // 学生id，主键 private String name; // 姓名 private String sex; // 性别 private Integer age; // 年龄 // 学生和班级是多对一的关系，即一个学生只属于一个班级 private Clazz clazz; public Student() { super(); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Clazz getClazz() { return clazz; } public void setClazz(Clazz clazz) { this.clazz = clazz; } @Override public String toString() { return "Student [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]"; } 学生和班级是多对一的关系，即一个学生只属于一个班级。在Student类当中定义了一个clazz属性，该属性是一个Clazz类型，用来映射多对一的关联关系，表示该学生所属的班级。映射文件<mapper namespace="cn.mybatis.mydemo2.mapper.ClazzMapper"> SELECT * FROM tb_clazz WHERE id = #{id} <resultMap type="cn.mybatis.mydemo2.domain.Clazz" id="clazzResultMap"> <id property="id" column="id" /> <result property="code" column="code" /> <result property="name" column="name" /> <collection property="students" javaType="ArrayList" column="id" ofType="cn.mybatis.mydemo2.domain.Student" select="cn.mybatis.mydemo2.mapper.StudentMapper.selectStudentByClazzId" fetchType="lazy"> <id property="id" column="id" /> <result property="name" column="name" /> <result property="sex" column="sex" /> <result property="age" column="age" /> </collection> </resultMap> </mapper> ClazzMapper.xml中定义了一个<select.../>，其根据id查询班级信息。由于Clazz类除了简单的属性id、code、name 之外，还有一个关联对象students，所以返回的是一个名为clazzResultMap的resultMap。由于student是一个List集合，所以clazzResultMap中使用了<collection.../>元素映射一对多的关联关系，select属性表示会使用column属性的id值作为参数执行StudentMapper中定义的selectStudentByClazz查询该班级对应的所有学生数据，查询出的数据将被封装到property表示的students对象当中。还使用了一个新的属性fetchType，该属性的取值有eager和lazy，eager表示立即加载，即查询Clazz对象的时候，会立即执行关联的selectStudentByClazzId中定义的SQL语句去查询班级的所有学生；lazy表示懒加载，其不会立即发送SQL语句去查询班级的所有学生，而是等到需要使用到班级的students属性时，才会发送SQL语句去查询班级的所有学生。fetch机制更多的是为了性能考虑，如果查询班级时确定会访问班级的所有学生，则该属性应该设置为eager；如果查询班级时只是查询班级信息，有可能不会访问班级的所有学生，则该属性应该设置为lazy。正常情况下，一对多所关联的集合对象，都应该被设置成lazy。使用懒加载还需要在mybatis-config.xm 中增加如下配置：<settings> <setting name="logImpl" value="LOG4J"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings> lazyLoadingEnabled属性表示延迟加载的全局开关。当开启时，所有关联对象都会延迟加载。默认为false。agressiveLazyLoading属性启用时，会使带有延迟加载属性的对象立即加载; 反之，每种属性将会按需加载。默认为true,所以这里需要设置成false。<mapper namespace="cn.mybatis.mydemo2.mapper.StudentMapper"> SELECT * FROM tb_clazz c,tb_student s WHERE c.id = s.clazz_id AND s.id = #{id} SELECT * FROM tb_student WHERE clazz_id = #{id} <resultMap type="cn.mybatis.mydemo2.domain.Student" id="studentResultMap"> <id property="id" column="id" /> <result property="name" column="name" /> <result property="sex" column="sex" /> <result property="age" column="age" /> <association property="clazz" javaType="cn.mybatis.mydemo2.domain.Clazz"> <id property="id" column="id" /> <result property="code" column="code" /> <result property="name" column="name" /> </association> </resultMap> </mapper> StudentMapper.xm 中定义了一个 其会根据学生id查询学生信息，由于Student类除了简单的属性id、name、sex和age之外，还有一个关联对象clazz，所以它返回的是一个名为clazzResultMap的resultMap。clazzResutMap中使用了<association.../>元素映射多对一的关联关系，因为 的SQL 语句是一条多表连接，关联tb_clazz表的同时查询了班级数据，所以<association.../>只是简单地装载数据。在实际开发中，一对多关系通常映射为集合对象，而由于多方的数据量可能很大，所以通常使用懒加载; 而多对一只是关联到一个对象，所以通常使用多表连接直接把数据提取出来。StudentMapper.xml 中还定义了一个 其会根据班级id查询所有学生信息，该查询用于ClazzMapper.xml 中的关联查询。映射接口import cn.mybatis.mydemo2.domain.Clazz; public interface ClazzMapper { // 根据id查询班级信息 Clazz selectClazzById(Integer id); } import cn.mybatis.mydemo2.domain.Student; public interface StudentMapper { // 根据id查询学生信息 Student selectStudentById(Integer id); } 测试类public class App { public static void main(String[] args) throws Exception { // 读取mybatis-config.xml文件 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 初始化mybatis，创建SqlSessionFactory类的实例 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 创建Session实例 SqlSession session = sqlSessionFactory.openSession(); App app = new App(); app.testSelectClazzById(session); // t.testSelectStudentById(session); // 提交事务 session.commit(); // 关闭Session session.close(); } // 测试一对多，查询班级Clazz（一）的时候级联查询学生Student（多） public void testSelectClazzById(SqlSession session) { // 获得ClazzMapper接口的代理对象 ClazzMapper cm = session.getMapper(ClazzMapper.class); // 调用selectClazzById方法 Clazz clazz = cm.selectClazzById(1); // 查看查询到的clazz对象信息 System.out.println(clazz.getId() + " " + clazz.getCode() + " " + clazz.getName()); // 查看clazz对象关联的学生信息 List<Student> students = clazz.getStudents(); for (Student stu : students) { System.out.println(stu); } } // 测试多对一，查询学生Student（多）的时候级联查询 班级Clazz（一） public void testSelectStudentById(SqlSession session) { // 获得StudentMapper接口的代理对象 StudentMapper sm = session.getMapper(StudentMapper.class); // 调用selectStudentById方法 Student stu = sm.selectStudentById(1); // 查看查询到的Student对象信息 System.out.println(stu); // 查看Student对象关联的班级信息 System.out.println(stu.getClazz()); } } 在APP类中定义了一个testSelectClazzById()方法，该方法用于测试一对多关系，查询班级Clazz(一)的时候级联查询学生Student(多)的信息。运行APP类的main方法，其通过SqlSession的getMapper(Class<Type> t)方法获得mapper接口的代理对象ClazzMapper,调用selectClazzByld方法时会执行ClazzMapper.xml的 中定义的sql语句。控制台显示如下：DEBUG [main]==>Preparing: SELECT * PROM tb_clazz WHERE id = ?DEBUG [main]-->Parameters: 1(Integer)DEBUG [main]<==Total :11 201803 计算机专业可以看到，MyBatis只是执行了查询班级的SQL语句，由 中的<colletion fethType=lazy">使用的是懒加载，因此，当没有使用到关联的学生对象时，并没有发送查询学生的SQL语句。修改testSelectClazzByld方法，增加访问关联学生的代码：// 查看clazz对象关联的学生信息 List<Student> students = clazz.getStudents(); for (Student stu : students) { System.out.println(stu); } 再次运行APP类的main方法。控制台显示如下:DEBUG [main]--> Preparing: SBIECT * FRON tb_clazz WHERE id = ?DEBUG [main]==> Parameters: 1(Integer)DEBUG [main]<== Total :1 201803 计算机专业DEBUG [main]--> Preparing: SELECT * FRON tb_student WHERE clazz id = ?DEBUG [mainj--> Parameters: 1(Integer)DEBUG [main]<== Total: 4student [id=1,name=马云,sex=男,age=23]student [id=2,name=李飞飞,sex=女,age=23]Student [id=3,name=马化腾,sex=男,age=23]Student [id=4,name=柳青,sex=女,age=23]可以看到，MyBatis执行了查询班级的SQL语句之后，又执行了根据clazz_id 查询学生信息的SQL语句。这就是所谓的懒加载。增加一个testSelectStudentBy方法，测试多对一。在main方法中运行testSelectStudentById方法，控制台显示如下:DEBUG[main]---> Preparing: SELECT * FRON tb_clazz c,tb_student s WHERE c.id = s.clazz_id AND s.id = ?DEBUG[main]---> Parameters: 1(Integer)DEBUG[main]<--- Total:Student[id=1,name=马云,sex=男,age=23]Clazz[id=1,code=201803,name=计算机专业]可以看到，MyBatis执行了一个多表查询语句，并且将查询到的班级信息封装到了学生对象的关联属性中。
本文修订日期：2019年11月25日SQL脚本在实际项目开发中，经常存在一对一关系，比如一个人只能有一个身份证，一个身份证只能给一个人使用，这就是一对一的关系。一对一关系推荐使用唯一主外键关联，即两张表使用外键关联，由于是一对一关联，因此还需要给外键列增加unique唯一约束。下面我们就用一个简单示例来看看MyBatis怎么处理一对一关系。首先，在数据库创建两个表tb_card和tb_person，并插入测试数据。SQL 脚本如下: drop table if exists tb_card; create table tb_card ( id int primary key auto_increment, code varchar(18) ); insert into tb_card(code) values ('432801198009191038'); drop table if exists tb_person; create table tb_person ( id int primary key auto_increment, name varchar(18), sex varchar(18), age int, card_id int unique, foreign key(card_id) peferences tb_card(id) ); insert into tb_person(name, sex, age, card_id) values ('jack','男',23,1) ; 提示:tb_person表的card_id作为外键，其参照tb_card表的主键id，因为是一对一关系，即一个card只能让一个person使用，所以card_id做成了唯一键约束。如此一来，当一个person使用了一个card之后，其他的person就不能使用该card了。实体类接下来，创建一个Card对象和一个Person对象分别映射tb_card和tb_person表。package cn.mybatis.mydemo.domain; import java.io.Serializable; public class Card implements Serializable { private static final long serialVersionUID = 1L; private Integer id; // 主键id private String code; // 身份证编号 public Card() { super(); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } @Override public String toString() { return "Card [id=" + id + ", code=" + code + "]"; } } package cn.mybatis.mydemo.domain; import java.io.Serializable; public class Person implements Serializable { private static final long serialVersionUID = 1L; private Integer id; // 主键id private String name; // 姓名 private String sex; // 性别 private Integer age; // 年龄 // 人和身份证是一对一的关系，即一个人只有一个身份证 private Card card; public Person() { super(); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Card getCard() { return card; } public void setCard(Card card) { this.card = card; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]"; } } 映射文件人和身份证是一对一的关系，即一个人只有一个身份证。在Person类中定义了一个card属性，该属性是一个Card类型，用来映射一对一的关联关系，表示这个人的身份证。接下来是XML映射文件。<mapper namespace="cn.mybatis.mydemo.mapper.CardMapper"> select * from tb_card where id = #{id} </mapper> <mapper namespace="cn.mybatis.mydemo.mapper.PersonMapper"> select * from tb_person where id = #{id} <resultMap type="cn.mybatis.mydemo.domain.Person" id="personMapper"> <id property="id" column="id" /> <result property="name" column="name" /> <result property="sex" column="sex" /> <result property="age" column="age" /> <association property="card" column="card_id" select="cn.mybatis.mydemo.mapper.CardMapper.selectCardById" javaType="cn.mybatis.mydemo.domain.Card" /> </resultMap> </mapper> 在PersonMapper.xml中定义了一个<select.../>，其根据id查询Person信息，由于Person类除了简单的属性id、name、sex和age之外，还有一个关联对象card，所以返回的是一个名为personMapper的resultMap。personMapper中使用了<association .../>元素映射一对一的关联关系。其中，select属性表示会使用column属性的card_id值作为参数执行CardMapper中定义的selectCardById语句，查询对应的Card数据，查询出的数据将被封装到property表示的card对象当中。映射接口我们通常都是使用SqlSession对象调用insert，update，delete和select方法进行测试。实际上，Mybatis官方手册建议通过mapper接口的代理对象访问mybatis，该对象关联了SqlSession对象，开发者可以通过该对象直接调用方法操作数据库。下面定义一个mapper接口对象，需要注意的是，mapper接口对象的类名必须和之前的XML文件中的mapper的namespace一致，而方法名和参数也必须和XML文件中的<select.../>元素的id属性和parameterType属性一致。package cn.mybatis.mydemo.mapper; import cn.mybatis.mydemo.domain.Person; public interface PersonMapper { /** * 根据id查询Person，方法名和参数必须和XML文件中的<select.../>元素的id属性和parameterType属性一致 * */ Person selectPersonById(Integer id); } 测试类public class App { public static void main(String[] args) throws Exception { // 读取mybatis-config.xml文件 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 初始化mybatis，创建SqlSessionFactory类的实例 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 创建Session实例 SqlSession session = sqlSessionFactory.openSession(); Person p = session.selectOne("cn.mybatis.mydemo.mapper.PersonMapper.selectPersonById", 1); System.out.println(p); System.out.println(p.getCard().getCode()); // 获得mapper接口的代理对象 PersonMapper pm = session.getMapper(PersonMapper.class); // 直接调用接口的方法，查询id为1的Peson数据 Person p2 = pm.selectPersonById(1); // 打印Peson对象 System.out.println(p2); // 打印Person对象关联的Card对象 System.out.println(p2.getCard()); // 提交事务 session.commit(); // 关闭Session session.close(); } } 运行APP类的main方法，通过SqlSession的getMapper(Class<?> type)方法获得mapper接口的代理对象PersonMapper，调用selectPersonByld方法时会执行PersonMapper.xml中<select.../>元素中定义的sql语句。