MyBatis最佳实践

01 动态SQL

MyBatis提供10种动态SQL标签,包括条件(<if>)、选择(<choose><when><otherwise>)、<trim><where><set>)、<foreach><bind><include>标签。执行原理使用OGNL从SQL参数对象计算表达式值,根据表达式值动态拼接SQL,以此来完成动态SQL功能。

1.1 where-if

<where> 标签会先删除整体筛选条件前缀and或者or关键字,再添加筛选条件前缀where关键字。需要注意,<where> 标签不会删除筛选条件后缀关键字。

xml 复制代码
<select id="selectListIf" parameterType="user" resultMap="BaseResultMap">
    select
        <include refid="baseSQL"/>
    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-when-otherwise

MyBatis提供<choose>-<when>-<otherwise>三个标签, 类似于Java的switch-case-default语句,用于实现多分支选择。

xml 复制代码
<select id="selectListChoose" parameterType="user" resultMap="BaseResultMap">
    select
        <include refid="baseSQL"></include>
    from t_user
    <where>
        <choose>
            <when test="id != null">
                and id = #{id}
            </when>
            <when test="userName != null and userName != ''">
                and user_name like CONCAT(CONCAT('%',#{userName}),'%')
            </when>
            <otherwise>
            </otherwise>
        </choose>
    </where>
</select>

1.3 trim

标签属性 描述
prefix 标签内语句添加前缀
suffix 标签内语句添加后缀
prefixOverrides 删除多余前缀内容,可以使用` `包含多个删除内容
suffixOverrides 删除多余后缀内容,可以使用` `包含多个删除内容
xml 复制代码
<!-- where标签 -->
<select id="selectListWhere" resultMap="BaseResultMap" parameterType="user">
    select
        <include refid="baseSQL"/>
    from t_user
    <where>
        <if test="userName != null">
            and user_name = #{userName}
        </if>
        <if test="age != 0">
            and age = #{age}
        </if>
    </where>
</select>

<!-- trim: 替代where标签的使用 -->
<select id="selectListTrim" resultMap="BaseResultMap" parameterType="user">
    select
        <include refid="baseSQL"/>
    from t_user
    <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=","  suffix="where id = #{id}">
        <if test="userName != null">
            user_name = #{userName},
        </if>
        <if test="age != 0">
            age = #{age}
        </if>
    </trim>
</update>

1.4 set

使用<set>标签行首动态插入set关键字,并且删掉后缀多余逗号。

xml 复制代码
<mapper>
    <update id="updateAuthorIfNecessary">
        update t_user
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="email != null">email = #{email}</if>
        </set>
        where id = #{id}
    </update>
</mapper>

1.5 foreach

foreach用于集合遍历,生成动态in条件或批量插入语句。

标签属性 描述
item 集合元素迭代别名,参数必选
index 在List和数组是元素序号,Map是元素Key,参数可选
open 语句开始符号,常用于in和values使用场景,参数可选
separator 元素之间分隔符,避免手动输入逗号导致SQL错误,参数可选
close 语句关闭符号,常用于in和values使用场景,参数可选
collection 标签循环解析对象
xml 复制代码
<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>

1.6 bind

bind 元素允许基于OGNL表达式创建一个变量,并将其绑定到当前上下文。

xml 复制代码
<mapper>
    <select id="selectBlogs" resultType="user">
        <bind name="pattern" value="'%' + _parameter.getTitle() + '%'"/>
        select * from t_blog
        where title like #{pattern}
    </select>
</mapper>

02 批量操作

项目导入文件批量处理数据,比如批量新增商户或者批量修改商户信息,大数据单条操作IO操作就非常耗时,所以为了解决性能问题采用批量操作解决。测试结果,循环插入10000条大约耗时3秒钟。

java 复制代码
public class TestBatch {
    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 testInsert() {
        long start = System.currentTimeMillis();

        UserMapper userMapper = session.getMapper(UserMapper.class);
        int count = 12000;
        for (int i = 2000; i < count; i++) {
            User user = new User();
            user.setUserName("a" + i);
            userMapper.insertUser(user);
        }
        session.commit();
        session.close();

        long end = System.currentTimeMillis();
        System.out.printf("batch insert data num=%d, cost time=%ldms", count, end - start);
    }
}

2.1 批量插入

MyBatis提供<foreach>标签动态拼接values语句,实现数据批量插入操作。测试结果,循环插入10000条大约耗时1秒钟。也就是说,动态SQL批量插入要比循环SQL执行效率高得多。

xml 复制代码
<mapper>
    <!-- 批量插入 -->
    <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>
</mapper>
java 复制代码
public class TestBatch {
    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();
    }

    // 批量插入
    @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.printf("batch insert data num=%d, cost time=%ldms", count, end - start);
    }
}

2.2 批量更新

MySQL提供case-when-end批量更新语法,动态生成拼装SQL执行更新。

sql 复制代码
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 (? , ? , ? )
xml 复制代码
<mapper>
    <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}
        </foreach>
    </update>
</mapper>
java 复制代码
public class TestBatch {
    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();
    }

    // 批量更新
    @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.printf("batch insert data num=%d, cost time=%ldms", count, end - start);
    }
}

2.3 批量删除

xml 复制代码
<mapper>
    <delete id="deleteByList" parameterType="java.util.List">
        delete from t_user where id in
        <foreach collection="list" item="item" open="(" separator="," close=")">
            #{item.id}
        </foreach>
    </delete>
</mapper>

2.4 BatchExecutor

MyBatis动态标签批量操作存在一定缺点,比如数据量特别大拼接SQL语句过大,导致超过MySQL服务端接收数据包大小限制异常,不过可以通过修改默认配置项,或者手动控制数据条数解决。

xml 复制代码
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

不过,MyBatis提供BatchExecutor类型执行器解决,仅需通过<setting>标签配置全局执行器类型,或者手动配置会话执行器类型。

xml 复制代码
<!-- 配置 -->
<configuration>
    <!-- 全局配置 -->
    <settings>
        <setting name="defaultExecutorType" value="BATCH" />
    </settings>
</configuration>
ini 复制代码
// 会话指定执行器类型
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
执行器类型 功能
SimpleExecutor 简单执行器,每次语句执行使用独立Statement执行,用完立即关闭
ReuseExecutor 重用执行器,内部使用Map缓存重复使用执行语句Statement
BatchExecutor 批量执行器,依赖JDBC批量处理,通过构造多个Statement解决MySQL包过大问题
xml 复制代码
public class Main {
    public static void main(String[] args) {
        String jdbcUrl = "";
        String jdbcUser = "";
        String jdbcPwd = "";
        String sql = "insert into blog values (?, ?, ?)";
        try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPwd);
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            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();
        }
    }
}

03 关联查询

3.1 嵌套查询

业务数据经常会遇到关联查询情况,比如查询员工就会关联部门(一对一),查询学生成绩就会关联课程(一对一),查询订单就会关联商品(一对多)。

xml 复制代码
<mapper>
    <!-- 用户[1]:[1]部门-->
    <resultMap id="nestedMap" 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="nestedMap">
        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>
</mapper>

嵌套查询,还有就是1对多关联关系

xml 复制代码
<mapper>
    <!-- 部门[1] : [N]用户 -->
    <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>
</mapper>

3.2 延迟加载

在MyBatis通过开启延迟加载开关,解决关联嵌套查询立即加载问题。

xml 复制代码
<!-- 延迟加载全局开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 主动惰性加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!--指定创建具有延迟加载对象所用到的代理工具, 默认JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />

lazyLoadingEnabled决定是否延迟加载,aggressiveLazyLoading决定是不是对象所有方法都会触发查询。

xml 复制代码
<mapper>
    <resultMap id="nestedMapLazy" 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="nestedMapLazy">
        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>
</mapper>

注意 :开启延迟加载开关,调用User#getDept(包括equalsclonehashCodetoString)时才会发起第二次查询。也可以通过<lazyLoadTriggerMethods>配置出发延迟加载方法,默认值为equals,clone,hashCode,toString

04 分页操作

4.1 逻辑分页

MyBatis提供逻辑分页对象RowBounds,主要包含offsetlimit两个属性。仅需Mapper接口方法添加RowBounds参数,ResultSet会按照offsetlimit处理。很明显,如果数据量很大,这种翻页方式效率会很低。

java 复制代码
public class TestBatch {
    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();
    }

    @Test
    public void test01() throws Exception {
        UserMapper mapper = session.getMapper(UserMapper.class);
        // 设置分页的数据
        RowBounds rowBounds = new RowBounds(1, 3);
        List<User> users = mapper.queryUserList(rowBounds);
        for (User user : users) {
            System.out.println(user);
        }
    }
}
java 复制代码
// DefaultResultSetHandler.java
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext();
    ResultSet resultSet = rsw.getResultSet();
    this.skipRows(resultSet, rowBounds);
    while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
        ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(resultSet, resultMap, (String)null);
        Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
        this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
}

4.2 物理分页

物理翻页才是真正翻页,通过数据库支持语句来翻页。第一种简单方法,就是传入参数(或者包装Page对象)实现SQL语句翻页,业务需要额外代码计算起止序号,SQL语句需要添加分页处理存在代码冗余。

xml 复制代码
<mapper>
    <select id="selectUserPage" parameterType="map" resultMap="BaseResultMap">
        select * from t_user limit #{curIndex}, #{pageSize}
    </select>
</mapper>

最常用分页使用方式,就是使用翻页插件,比如PageHelper底层通过拦截器改写SQL语句实现。

xml 复制代码
// 设置分页
PageHelper.startPage(pageNum, pageSize);
List<Employee> emps = employeeService.getAll();
// navigatePages: 导航页码数
PageInfo page = new PageInfo(emps, navigatePages);

05 Mapper继承关系

MyBatis提供Mapper继承关系支持,比如扩展Mapper子接口实现新功能方法,子接口就可以拥有父Mapper接口已有方法功能。

Mapper继承参照文档:github.com/mybatis/myb...

java 复制代码
public interface UserMapperExt extends UserMapper {
    public List<User> selectUserByName(String userName);
}
xml 复制代码
<mapper namespace="com.feiyu.mapper.UserMapperExt">
    <resultMap id="BaseResultMapExt" type="com.boge.vip.domain.User">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="user_name" property="userName" jdbcType="VARCHAR"/>
        <result column="real_name" property="realName" jdbcType="VARCHAR"/>
        <result column="password" property="password" jdbcType="VARCHAR"/>
        <result column="age" property="age" jdbcType="INTEGER"/>
    </resultMap>

    <select id="selectUserByName" resultMap="BaseResultMapExt">
        select *
        from t_user
        where user_name = #{userName}
    </select>
</mapper>
相关推荐
你我约定有三1 分钟前
RabbitMQ--消息丢失问题及解决
java·开发语言·分布式·后端·rabbitmq·ruby
程序视点33 分钟前
望言OCR 2025终极评测:免费版VS专业版全方位对比(含免费下载)
前端·后端·github
rannn_1111 小时前
Java学习|黑马笔记|Day23】网络编程、反射、动态代理
java·笔记·后端·学习
一杯科技拿铁1 小时前
Go 的时间包:理解单调时间与挂钟时间
开发语言·后端·golang
独泪了无痕1 小时前
Hutool之CollStreamUtil:集合流操作的神器
后端
陈随易2 小时前
AI新技术VideoTutor,幼儿园操作难度,一句话生成讲解视频
前端·后端·程序员
弥金2 小时前
LangChain基础
人工智能·后端
码事漫谈2 小时前
AI行业热点抓取和排序系统实现案例
后端
方圆想当图灵2 小时前
关于 Nacos 在 war 包部署应用关闭部分资源未释放的原因分析
后端
Lemon程序馆2 小时前
今天聊聊 Mysql 的那些“锁”事!
后端·mysql