、MyBatis最佳实践
1.动态SQL语句
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条 件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗 号。利用动态 SQL,可以彻底摆脱这种痛苦。 复制代码
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了 这一特性的易用性。 复制代码
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替 换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。 复制代码
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
1.1 if
需要判断的时候,条件写在test中 复制代码
<select id="selectListIf" parameterType="user" resultMap="BaseResultMap" > select <include refid="baseSQL"></include> from t_user <where> <if test="id != null"> and id = #{id} </if> <if test="userName != null"> and user_name = #{userName} </if> </where> </select> 复制代码
1.2 choose
需要选择一个条件的时候 复制代码
<!-- choose 的使用 --> <select id="selectListChoose" parameterType="user" resultMap="BaseResultMap" > select <include refid="baseSQL"></include> from t_user <where> <choose> <when test="id != null"> id = #{id} </when> <when test="userName != null and userName != ''"> and user_name like CONCAT(CONCAT('%',#{userName,jdbcType=VARCHAR}),'%') </when> <otherwise> </otherwise> </choose> </where> </select> 复制代码
1.3 trim
需要去掉where、and、逗号之类的符号的时候 复制代码
<!-- trim 的使用 替代where标签的使用 --> <select id="selectListTrim" resultMap="BaseResultMap" parameterType="user"> select <include refid="baseSQL"></include> <!-- <where> <if test="username!=null"> and name = #{username} </if> </where> --> <trim prefix="where" prefixOverrides="AND |OR "> <if test="userName!=null"> and user_name = #{userName} </if> <if test="age != 0"> and age = #{age} </if> </trim> </select> <!-- 替代set标签的使用 --> <update id="updateUser" parameterType="User"> update t_user <trim prefix="set" suffixOverrides=","> <if test="userName!=null"> user_name = #{userName}, </if> <if test="age != 0"> age = #{age} </if> </trim> where id=#{id} </update> 复制代码
1.4 foreach
需要遍历集合的时候 复制代码
<delete id="deleteByList" parameterType="java.util.List"> delete from t_user where id in <foreach collection="list" item="item" open="(" separator="," close=")"> #{item.id,jdbcType=INTEGER} </foreach> </delete> 复制代码
动态SQL主要是用来解决SQL语句生成的问题。
2.批量操作
我们在项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况(批量新增商户、批量修改商户信息),当数据量非常大,比如超过几万条的时候,在Java代码中循环发送SQL到数据库执行肯定是不现实的,因为这个意味着要跟数据库创建几万次会话。即使在同一个连接中,也有重复编译和执行SQL的开销。 复制代码
例如循环插入10000条(大约耗时3秒钟): 复制代码
public class Test03Batch { public SqlSession session; @Before public void init() throws IOException { // 1.获取配置文件 InputStream in = Resources.getResourceAsStream("mybatis-config.xml"); // 2.加载解析配置文件并获取SqlSessionFactory对象 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in); // 3.根据SqlSessionFactory对象获取SqlSession对象 session = factory.openSession(); } /** * 循环插入10000 */ @Test public void test1(){ long start = System.currentTimeMillis(); UserMapper mapper = session.getMapper(UserMapper.class); int count = 12000; for (int i=2000; i< count; i++) { User user = new User(); user.setUserName("a"+i); mapper.insertUser(user); } session.commit(); session.close(); long end = System.currentTimeMillis(); System.out.println("循环批量插入"+count+"条,耗时:" + (end -start )+"毫秒"); } } 复制代码
在MyBatis里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直接传入一个List、Set、Map或者数组,配合动态SQL的标签,MyBatis会自动帮我们生成语法正确的SQL语句。 复制代码
2.1 批量插入
批量插入的语法是这样的,只要在values后面增加插入的值就可以了。 复制代码
insert into tbl_emp (emp_id, emp_name, gender,email, d_id) values ( ?,?,?,?,? ),( ?,?,?,?,? ),( ?,?,?,?,? ) 复制代码
在Mapper文件里面,我们使用foreach标签拼接 values部分的语句:
<!-- 批量插入 --> <insert id="insertUserList" parameterType="java.util.List" > insert into t_user(user_name,real_name) values <foreach collection="list" item="user" separator=","> (#{user.userName},#{user.realName}) </foreach> </insert> 复制代码
Java代码里面,直接传入一个List类型的参数。
/** * 批量插入 */ @Test public void test2(){ long start = System.currentTimeMillis(); UserMapper mapper = session.getMapper(UserMapper.class); int count = 12000; List<User> list = new ArrayList<>(); for (int i=2000; i< count; i++) { User user = new User(); user.setUserName("a"+i); list.add(user); } mapper.insertUserList(list); session.commit(); session.close(); long end = System.currentTimeMillis(); System.out.println("循环批量插入"+count+"条,耗时:" + (end -start )+"毫秒"); } 复制代码
插入一万条大约耗时1秒钟。 复制代码
可以看到,动态SQL批量插入效率要比循环发送SQL执行要高得多。最关键的地方就在于减少了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。 复制代码
2.2 批量更新
批量更新的语法是这样的,通过case when,来匹配id相关的字段值 复制代码
update t_user set user_name = case id when ? then ? when ? then ? when ? then ? end , real_name = case id when ? then ? when ? then ? when ? then ? end where id in ( ? , ? , ? ) 复制代码
所以在Mapper文件里面最关键的就是case when和where的配置。 复制代码
需要注意一下open属性和separator属性。 复制代码
<update id="updateUserList"> update t_user set user_name = <foreach collection="list" item="user" index="index" separator=" " open="case id" close="end"> when #{user.id} then #{user.userName} </foreach> ,real_name = <foreach collection="list" item="user" index="index" separator=" " open="case id" close="end"> when #{user.id} then #{user.realName} </foreach> where id in <foreach collection="list" item="item" open="(" separator="," close=")"> #{item.id,jdbcType=INTEGER} </foreach> </update> 复制代码
java代码实现
/** * 批量更新 */ @Test public void test3(){ long start = System.currentTimeMillis(); UserMapper mapper = session.getMapper(UserMapper.class); int count = 12000; List<User> list = new ArrayList<>(); for (int i=2000; i< count; i++) { User user = new User(); user.setId(i); user.setUserName("a"+i); list.add(user); } mapper.updateUserList(list); session.commit(); session.close(); long end = System.currentTimeMillis(); System.out.println("批量更新"+count+"条,耗时:" + (end -start )+"毫秒"); } 复制代码
2.3 批量删除
批量删除也是类似的。
<delete id="deleteByList" parameterType="java.util.List"> delete from t_user where id in <foreach collection="list" item="item" open="(" separator="," close=")"> #{item.id,jdbcType=INTEGER} </foreach> </delete> 复制代码
2.4 BatchExecutor
当然MyBatis的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的时候,拼接出来的SQL语句过大。 复制代码
MySQL的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是 4M,需要修改默认配置或者手动地控制条数,才可以解决这个问题。 复制代码
Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (7188967 > 4194304). You can change this value on the server by setting the max_allowed_packet' variable. 复制代码
在我们的全局配置文件中,可以配置默认的Executor的类型(默认是SIMPLE)。其中有一种BatchExecutor。 复制代码
<setting name="defaultExecutorType" value="BATCH" /> 复制代码
也可以在创建会话的时候指定执行器类型
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH); 复制代码
Executor
- SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
- ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
- BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。executeUpdate()是一个语句访问一次数据库,executeBatch()是一批语句访问一次数据库(具体一批发送多少条SQL跟服务端的max_allowed_packet有关)。BatchExecutor底层是对JDBC ps.addBatch()和ps. executeBatch()的封装。
@Test public void testJdbcBatch() throws IOException { Connection conn = null; PreparedStatement ps = null; try { conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatisdb?rewriteBatchedStatements=true", "root", "123456"); ps = conn.prepareStatement( "INSERT into blog values (?, ?, ?)"); for (int i = 1000; i < 101000; i++) { Blog blog = new Blog(); ps.setInt(1, i); ps.setString(2, String.valueOf(i)+""); ps.setInt(3, 1001); ps.addBatch(); } ps.executeBatch(); ps.close(); conn.close(); } catch (SQLException se) { se.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (ps != null) ps.close(); } catch (SQLException se2) { } try { if (conn != null) conn.close(); } catch (SQLException se) { se.printStackTrace(); } } } 复制代码
3.关联查询
3.1 嵌套查询
我们在查询业务数据的时候经常会遇到关联查询的情况,比如查询员工就会关联部门(一对一),查询学生成绩就会关联课程(一对一),查询订单就会关联商品(一对多),等等。 复制代码
用户和部门的对应关系是1对1的关系 复制代码
<!-- 嵌套查询 1对1 1个用户对应一个部门--> <resultMap id="nestedMap1" type="user"> <id property="id" column="id" jdbcType="INTEGER"/> <result property="userName" column="user_name" jdbcType="VARCHAR" /> <result property="realName" column="real_name" jdbcType="VARCHAR" /> <result property="password" column="password" jdbcType="VARCHAR"/> <result property="age" column="age" jdbcType="INTEGER"/> <result property="dId" column="d_id" jdbcType="INTEGER"/> <association property="dept" javaType="dept"> <id column="did" property="dId"/> <result column="d_name" property="dName"/> <result column="d_desc" property="dDesc"/> </association> </resultMap> <select id="queryUserNested" resultMap="nestedMap1"> SELECT t1.`id` ,t1.`user_name` ,t1.`real_name` ,t1.`password` ,t1.`age` ,t2.`did` ,t2.`d_name` ,t2.`d_desc` FROM t_user t1 LEFT JOIN t_department t2 ON t1.`d_id` = t2.`did` </select> 复制代码
还有就是1对多的关联关系,嵌套查询
<!-- 嵌套查询 1对多 1个部门有多个用户--> <resultMap id="nestedMap2" type="dept"> <id column="did" property="dId"/> <result column="d_name" property="dName"/> <result column="d_desc" property="dDesc"/> <collection property="users" ofType="user"> <id property="id" column="id" jdbcType="INTEGER"/> <result property="userName" column="user_name" jdbcType="VARCHAR" /> <result property="realName" column="real_name" jdbcType="VARCHAR" /> <result property="password" column="password" jdbcType="VARCHAR"/> <result property="age" column="age" jdbcType="INTEGER"/> <result property="dId" column="d_id" jdbcType="INTEGER"/> </collection> </resultMap> <select id="queryDeptNested" resultMap="nestedMap2"> SELECT t1.`id` ,t1.`user_name` ,t1.`real_name` ,t1.`password` ,t1.`age` ,t2.`did` ,t2.`d_name` ,t2.`d_desc` FROM t_user t1 RIGHT JOIN t_department t2 ON t1.`d_id` = t2.`did` </select> 复制代码
3.2 延迟加载
在MyBatis里面可以通过开启延迟加载的开关来解决这个问题。
在settings标签里面可以配置:
<!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认false --> <setting name="lazyLoadingEnabled" value="true"/> <!--当开启时,任何方法的调用都会加载该对象的所有属性。默认false,可通过select标签的 fetchType来覆盖--> <setting name="aggressiveLazyLoading" value="false"/> <!-- MyBatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST --> <setting name="proxyFactory" value="CGLIB" /> 复制代码
lazyLoadingEnabled决定了是否延迟加载(默认false)。
aggressiveLazyLoading决定了是不是对象的所有方法都会触发查询。
1对1的延迟加载配置
<!-- 延迟加载 1对1 --> <resultMap id="nestedMap1Lazy" type="user"> <id property="id" column="id" jdbcType="INTEGER"/> <result property="userName" column="user_name" jdbcType="VARCHAR" /> <result property="realName" column="real_name" jdbcType="VARCHAR" /> <result property="password" column="password" jdbcType="VARCHAR"/> <result property="age" column="age" jdbcType="INTEGER"/> <result property="dId" column="d_id" jdbcType="INTEGER"/> <association property="dept" javaType="dept" column="d_id" select="queryDeptByUserIdLazy"> </association> </resultMap> <resultMap id="baseDept" type="dept"> <id column="did" property="dId"/> <result column="d_name" property="dName"/> <result column="d_desc" property="dDesc"/> </resultMap> <select id="queryUserNestedLazy" resultMap="nestedMap1Lazy"> SELECT t1.`id` ,t1.`user_name` ,t1.`real_name` ,t1.`password` ,t1.`age` ,t1.d_id FROM t_user t1 </select> <select id="queryDeptByUserIdLazy" parameterType="int" resultMap="baseDept"> select * from t_department where did = #{did} </select> 复制代码
注意:开启了延迟加载的开关,调用user.getDept()以及默认的
(equals,clone,hashCode,toString)时才会发起第二次查询,其他方法并不会触发查询,比如blog.getName();
/** * 1对1 关联查询 延迟加载 * @throws Exception */ @Test public void test03() throws Exception{ init(); UserMapper mapper = session.getMapper(UserMapper.class); List<User> users = mapper.queryUserNestedLazy(); for (User user : users) { System.out.println(user.getUserName() ); //System.out.println(user.getUserName() + "---->"+user.getDept()); } } 复制代码
触发延迟加载的方法可以通过<lazyLoadTriggerMethods>配置,默认 equals(),clone(),hashCode(),toString()。
1对多的延迟加载的配置
<!-- 1对多 延迟加载 --> <resultMap id="nestedMap2Lazy" type="dept"> <id column="did" property="dId"/> <result column="d_name" property="dName"/> <result column="d_desc" property="dDesc"/> <collection property="users" ofType="user" column="did" select="queryUserByDeptLazy"> </collection> </resultMap> <select id="queryDeptNestedLazy" resultMap="nestedMap2"> SELECT ,t2.`did` ,t2.`d_name` ,t2.`d_desc` FROM t_department t2 </select> <select id="queryUserByDeptLazy" resultMap="BaseResultMap" > select * from t_user where d_id = #{did} </select> 复制代码