MyBatis最佳实践

动态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

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>

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>

 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>

 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语句生成的问题。

批量操作

        我们在项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况(批量新增商户、批量修 改商户信息),当数据量非常大,比如超过几万条的时候,在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语句。

批量插入

        批量插入的语法是这样的,只要在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执行要高得多。最关键的地方就在于减少了跟 数据库交互的次数,并且避免了开启和结束事务的时间消耗。

批量更新

        批量更新的语法是这样的,通过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 )+"毫秒");
    }

批量删除

        批量删除也是类似的。

<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>

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

  1. SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  2. ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
  3. 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();
            }
        }
    }

关联查询

嵌套查询

        我们在查询业务数据的时候经常会遇到关联查询的情况,比如查询员工就会关联部门(一对一), 查询学生成绩就会关联课程(一对一),查询订单就会关联商品(一对多),等等。 用户和部门的对应关系是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>

延迟加载

        在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());
        }
    }

触发延迟加载的方法可以通过配置,默认 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>

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值