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
(包括equals
、clone
、hashCode
和toString
)时才会发起第二次查询。也可以通过<lazyLoadTriggerMethods>
配置出发延迟加载方法,默认值为equals,clone,hashCode,toString
。
04 分页操作
4.1 逻辑分页
MyBatis提供逻辑分页对象RowBounds,主要包含offset
和limit
两个属性。仅需Mapper接口方法添加RowBounds参数,ResultSet会按照offset
和limit
处理。很明显,如果数据量很大,这种翻页方式效率会很低。
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>