动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
需要判断的时候,条件写在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>
需要选择一个条件的时候
<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>
需要去掉where、and、逗号之类的符号的时候
<select id="selectListTrim" resultMap="BaseResultMap"
parameterType="user">
select <include refid="baseSQL">include>
<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>
需要遍历集合的时候
<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>
在项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况,当数据量非常大,比如超过几万条的时候,在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的配置。
<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>
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对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个部门有多个用户
<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标签里面可以配置:
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="proxyFactory" value="CGLIB" />
lazyLoadingEnabled决定了是否延迟加载(默认false)。
aggressiveLazyLoading决定了是不是对象的所有方法都会触发查询。
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()时才会发起第二次查询;
/**
* 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() + "---->"+user.getDept());
}
}
MyBatis里面有一个逻辑分页对象RowBounds,里面主要有两个属性,offset和limit(从第几条开始,查询多少条)。我们可以在Mapper接口的方法上加上这个参数,不需要修改xml里面的SQL语句。
接口中定义
public List<User> queryUserList(RowBounds rowBounds);
测试类
@Test
public void test01() throws Exception{
init();
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);
}
}
RowBounds的工作原理其实是对ResultSet的处理。它会舍弃掉前面offset条数据,然后再取剩下的数据的limit条。
// 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);
}
}
很明显,如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用subList(start,end)没什么区别)。所以我们要用到物理翻页。
物理翻页是真正的翻页,它是通过数据库支持的语句来翻页。
第一种简单的办法就是传入参数(或者包装一个page对象),在SQL语句中翻页。
<select id="selectUserPage" parameterType="map" resultMap="BaseResultMap">
select * from t_user limit #{curIndex} , #{pageSize}
select>
这种方式会存在两个问题:第一个问题是我们要在Java业务代码里面去计算起止序号;第二个问题是:每个需要翻页的Statement都要编写limit语句,会造成Mapper映射器里面很多代码冗余。
需要一种通用的方式,不需要去修改配置的任何一条SQL语句,我们只要传入当前是第几页,每页多少条就可以了,自动计算出来起止序号。
我们最常用的做法就是使用翻页的插件,比如PageHelper。
// pageSize每一页几条
PageHelper.startPage(pn, 10);
List<Employee> emps = employeeService.getAll();
// navigatePages 导航页码数
PageInfo page = new PageInfo(emps, 10);
return Msg.success().add("pageInfo", page);
PageHelper是通过MyBatis的拦截器实现的,简单地来说,它会根据PageHelper的参数,改写我们的SQL语句。比如MySQL会生成limit语句,Oracle会生成rownum语句,SQL Server会生成top语句。
在项目中使用MyBaits的时候,针对需要操作的一张表,需要创建实体类、Mapper映射器、Mapper接口,里面又有很多的字段和方法的配置,这部分的工作是非常繁琐的。而大部分时候我们对于表的基本操作是相同的,比如根据主键查询、根据Map查询、单条插入、批量插入、根据主键删除等等等等。当我们的表很多的时候,意味着有大量的重复工作。
所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper映射器、Mapper接口,里面包含了我们需要用到的这些基本方法和SQL呢?
MyBatis也提供了一个代码生成器,叫做MyBatis Generator,简称MBG(它是MyBatis的一个插件)。我们只需要修改一个配置文件,使用相关的jar包命令或者Java代码就可以帮助我们生成实体类、映射器和接口文件。
MBG的配置文件里面有一个Example的开关,这个东西用来构造复杂的筛选条件的,换句话说就是根据我们的代码去生成where条件。
原理:在实体类中包含了两个有继承关系的Criteria,用其中自动生成的方法来构建查询条件。把这个包含了Criteria的实体类作为参数传到查询参数中,在解析Mapper映射器的时候会转换成SQL条件。
DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<classPathEntry location="C:\Users\mysql-connector-java-8.0.11.jar" />
<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressAllComments" value="true" />
commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/db?characterEncoding=utf-8&serverTimezone=UTC"
userId="root"
password="123456">
<property name="nullCatalogMeansCurrent" value="true" />
jdbcConnection>
<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
javaTypeResolver>
<javaModelGenerator targetPackage="com.domain" targetProject="./src/main/java">
<property name="enableSubPackages" value="false" />
<property name="trimStrings" value="true" />
javaModelGenerator>
<sqlMapGenerator targetPackage="com.mapper" targetProject="./src/main/java">
<property name="enableSubPackages" value="false" />
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.mapper" targetProject="./src/main/java">
<property name="enableSubPackages" value="false" />
javaClientGenerator>
<table tableName="t_user" domainObjectName="User" />
context>
generatorConfiguration>
在pom.xml中添加对应的插件
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.2version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xmlconfigurationFile>
configuration>
plugin>
plugins>
build>
maven项目中执行插件帮助我们快速生成需要的表结构对应的相关文件。
当我们的表字段发生变化的时候,我们需要修改实体类和Mapper文件定义的字段和方法。如果是增量维护,那么一个个文件去修改。如果是全量替换,我们还要去对比用MBG生成的文件。字段变动一次就要修改一次,维护起来非常麻烦。
因为MyBatis的Mapper是支持继承的。所以可以把Mapper.xml和Mapper接口都分成两个文件。一个是MBG生成的,这部分是固定不变的。然后创建DAO类继承生成的接口,变化的部分就在DAO里面维护。
public interface UserMapperExt extends UserMapper {
public List<User> selectUserByName(String userName);
}
对应的映射文件
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mapper.UserMapperExt" >
<resultMap id="BaseResultMapExt" type="com.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" />
<result column="d_id" property="dId" jdbcType="INTEGER" />
<result column="i_id" property="iId" jdbcType="INTEGER" />
resultMap>
<select id="selectUserByName" resultMap="BaseResultMapExt" >
select * from t_user where user_name = #{userName}
select>
mapper>
在全局配置文件中我们也需要扫描
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
<mapper resource="mapper/UserMapperExt.xml"/>
mappers>
所以以后只要修改Ext的文件就可以了。这么做有一个缺点,就是文件会增多。
既然针对每张表生成的基本方法都是一样的,也就是公共的方法部分代码都是一样的,能不能把这部分合并成一个文件,让它支持泛型呢?
编写一个支持泛型的通用接口,比如叫GPBaseMapper,把实体类作为参数传入。这个接口里面定义了大量的增删改查的基础方法,这些方法都是支持泛型的。
自定义的Mapper接口继承该通用接口,例如BlogMapper extends GPBaseMapper,自动获得对实体类的操作方法。遇到没有的方法,我们依然可以在我们自己的Mapper里面编写。
能想到的解决方案,早就有人做了这个事了,这个东西就叫做通用Mapper。
用途:主要解决单表的增删改查问题,并不适用于多表关联查询的场景。
除了配置文件变动的问题之外,通用Mapper还可以解决:
使用方式:在Spring中使用时,引入jar包,替换applicationContext.xml中的sqlSessionFactory和configure。
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.crud.dao"/>
bean>
MyBatis-Plus是原生MyBatis的一个增强工具,可以在使用原生MyBatis的所有功能的基础上,使用plus特有的功能。
MyBatis-Plus的核心功能:
通用 CRUD:定义好Mapper接口后,只需要继承BaseMapper 接口即可获得通用的增删改查功能,无需编写任何接口方法与配置文件。
条件构造器:通过EntityWrapper(实体包装类),可以用于拼接 SQL 语句,并且支持排序、分组查询等复杂的SQL。
代码生成器:支持一系列的策略配置与全局配置,比MyBatis的代码生成更好用。