# Mybatis Dynamic SQL
# 1. 关于 Mybatis Dynamic SQL
官网地址是:Mybatis Dynamic SQL官网 (opens new window)。
首先要澄清的是,这里的『动态 SQL』并非之前的 mybatis mapper.xml 中的 if、foreach 那个『动态 SQL』,而是 Mybatis 官方的另一个项目,这个项目并不是为了取代 Mybatis ,而是为了让开发者更方便的使用 Mybatis , 也就是说它只是 Mybatis 的一个补充。
Mybatis Dynamic SQL 是一个用于生成动态 SQL 语句的框架。简单来说,就是你在 Java 代码中调用特定的方法,而在这些方法背后,你实际上是 “拼” 出了一条 SQL 语句。当然,根据个人的 “审美” 的不同,有些人可能觉得这样毫无必要,而宁愿在配置文件中去编写 SQL 。这也无可厚非。
简单来说,注解的出现『干掉』了大量的 mapper.xml 文件,而 Mybatis Dynamic SQL 的出现就是为了『干掉』大量的 Example 对象,进一步简化代码。
# 2. 集成 Dynamic SQL
在 pom.xml 中添加如下依赖,对比之前使用 MBG(MyBatis Generator),仅仅多添加了 MyBatis 的动态 SQL 依赖;
<dependency>
<groupId>org.mybatis.dynamic-sql</groupId>
<artifactId>mybatis-dynamic-sql</artifactId>
<version>1.2.1</version>
</dependency>
在执行 mybatis-generator 生成代码时,需要将 context 的 targetRuntime
属性更改为 MyBatis3DynamicSQL 。
runtime=MyBatis3DynamicSql
dao.type=ANNOTATEDMAPPER
dao.package=xxx.yyy.zzz.dao
po.package=xxx.yyy.zzz.dao.po
xml.package=mybatis/mapper
一切准备就绪,执行 mybatis-generator ,可以发现已经不再生成 mapper.xml 文件和 Example 类,取而代之的是生成了 DynamicSqlSupport 类。
# 3. SqlTable 和它的子类们
mybatis-generator 所生成的 “东西” 里面最关键的是生了一些名为 XxxDynamicSqlSupport 的工具类,而我们需要关注(未来会频繁涉及到)的是它们的内部类,例如:
public final class DepartmentDynamicSqlSupport {
...
public static final class Department extends SqlTable {
public final SqlColumn<Long> id = column("id", JDBCType.BIGINT); // 字段名和字段类型
public final SqlColumn<String> name = column("name", JDBCType.VARCHAR);
public final SqlColumn<String> location = column("location", JDBCType.VARCHAR);
public Department() {
super("department"); // 表名
}
}
}
这些内部类都继承自 SqlTable 类。显而易见,从 SqlTable 这个名字来看,你大概就能猜到它和它的子类的作用:在 MyBatis Dynamic SQL 中,这些内部类就是用来映射表和表字段的。很显然。
这些内部类的无参构造方法中调用的父类构造方法时传递的字符串,就是对应着某张表的表名。
例如上例中的
super("department")
;这些内部类的各个属性,就是对应着某张表的字段名和字段类型。
例如上例中的
id = column("id", JDBCType.BIGINT)
。
注意
不过,有点讨厌的是,这些 SqlTable 内部类会和你的 PO 类同名。为了避免不必要的麻烦,你可能需要改变一下两者中的某一个,以便于将它俩区分开。
你再仔细观察下 DepartmentDynamicSqlSupport 的源码,其实它做的事情就是 new 了一个 Department 对象作为静态属性(public static final) 再将它(和它的属性)暴露出去给我们(和 MyBatis Dynamic SQL)使用。
例如,MyBatis Generator 生成的 Mapper/Dao 接口中,有一个 selectList 属性,就用到了它们。
补充
既然说到了 Mapper/DAO 接口中的 selectList 属性,那么这里有一个和它类似的 “东西” :SqlTable 的子类会从 SqlTable 那里继承到一个 allColumns() 方法,它所返回的 BasicColumn 可以用来代表 select * from ...
中的那个 * 。
# 4. 实现基本的 CRUD 操作
略。
# 5. SqlBuilder
import static org.mybatis.dynamic.sql.SqlBuilder.*;
SqlBuilder 是一个非常有用的类,使用它可以灵活地构建 SQL 语句的条件,一些常用的条件构建方法如下。
条件 | 例子 | 对应 SQL |
---|---|---|
Between | where(foo, isBetween(x).and(y)) | where foo between ? and ? |
Equals | where(foo, isEqualsTo(x)) | where foo = ? |
Greater Than | where(foo, isGreaterThan(x)) | where foo > ? |
In | where(foo, isIn(x, y)) | where foo in (?, ?) |
Like | where(foo, isLike(x)) | where foo like ? |
Not Equals | where(foo, isNotEqualsTo(x)) | where foo <> ? |
Null | where(foo, isNull()) | where foo is null |
# 6. 条件查询
实现思路
使用 SqlBuilder 类构建 StatementProvider,然后调用 Mapper 接口中的方法即可。
按用户名和状态查询后台用户并按创建时间降序排列为例。SQL 实现如下:
SELECT *
FROM employee
WHERE department_id = 2
AND salary BETWEEN 500 AND 3000
ORDER BY salary DESC;
在使用 Dynamic SQL 来实现上述 SQL 语句时,你会发现你所调用的 Dao 的 select 方法接收 2 种类型的参数:SelectStatementProvider 和 SelectDSLCompleter 。
也就是说,你有 2 种方式、风格来『描述』你心里想执行的 SQL 语句。Provider 的写法更像 SQL 语句,对于熟悉 SQL 语句的 Dynamic SQL 的初学者来说,更容易理解;Completer 的写法更简洁。
注意
SelectDSLCompleter 写法要比 SelectStatementProvider 写法简洁,因为它省略掉了关于查询的列(即 SQL 语句中 select ...
的这一部分的 )设置。
在简单情况下,你所执行的 SQL 语句可能就是 select *
,或者是 select 所有列
这种逻辑,但是对于有些情况,比如关联查询,SelectDSLCompleter 省略掉这一部分之后,返回会让你对这部分无法设置,从而拿不到你预期的结果。
所有,优先建议大家使用 Provider 。或者,使用 Completer ,然后在必要的时候转成 Provider 使用。
# 7. 逻辑条件的组合
逻辑条件的组合大体分为 2 种:
单纯的
...与...与...
/...或...或...
与或
混用,由于或
的优先级更高,因此可以改造成(... and ...) or (... and ...) or ...
这样的统一形式。
# 8. 条件删除
TIP
使用 Dynamic SQL 实现条件删除,直接调用 Mapper 接口中生成好的 delete 方法即可。
我们『心里』期望执行的 SQL 如下:
DELETE FROM department WHERE name = 'test';
使用 Dynamic SQL 对应 Java 中的实现如下:
DeleteStatementProvider provider = SqlBuilder.deleteFrom(DEPARTMENT)
.where(DEPARTMENT.name, isEqualTo("test"))
.build().render(RenderingStrategies.MYBATIS3)
;
DeleteDSLCompleter completer = c -> c
.where(DEPARTMENT.name, isEqualTo("test"))
;
departmentDao.delete(provider);
# 9. 条件修改
TIP
使用 Dynamic SQL 实现条件修改,直接调用 Mapper 接口中生成好的update方法即可。
我们『心里』期望执行的 SQL 如下:
update
department
set name = 'hello',
location = 'world'
where id = 5;
使用 Dynamic SQL 对应 Java 中的实现如下:
// Provider 写法
UpdateStatementProvider provider = SqlBuilder.update(DEPARTMENT)
.set(DEPARTMENT.name).equalTo("hello")
.set(DEPARTMENT.location).equalTo("world")
.where(DEPARTMENT.id, isEqualTo(5L))
.build().render(RenderingStrategies.MYBATIS3);
;
// Completer 写法
UpdateDSLCompleter completer = c -> c
.set(DEPARTMENT.name).equalTo("hello")
.set(DEPARTMENT.location).equalTo("world")
.where(DEPARTMENT.id, isEqualTo(5L))
;
departmentDao.update(completer);
# 9. 关联查询:select 方案
略。
# 10. group 和 join 查询
TIP
涉及到多表查询,之前使用 mybatis-generator 的时候基本只能在 mapper.xml 中手写 SQL 实现,使用 Dynamic SQL 可以支持多表查询。
我们『心里』期望执行的 SQL 如下:
-- 查询所有部门信息(部门信息中包含该部门下的员工数量)
select department.id, department.name, department.location, count(employee.id) as employee_quantity
from department
left join employee on department.id = employee.department_id
group by department.id;
现在 mapper.xml 中定义好映射规则:
<resultMap id="selectDepartmentWithEmployeeQuantityResultMap" type="com.woniu.mybatisdynamicsqlsample.dao.po.Department">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="location" jdbcType="VARCHAR" property="location"/>
<result column="employee_quantity" jdbcType="INTEGER" property="employeeQuantity"/>
</resultMap>
# 11. 关联查询:association 方案
SelectStatementProvider provider = SqlBuilder.select(employee.allColumns(),
department.id.as("did"),
department.name.as("dname"),
department.location)
.from(employee)
.leftJoin(department).on(employee.departmentId, equalTo(department.id))
.where(employee.salary, isGreaterThan(salary))
.and(department.name, isEqualTo(name))
.build().render(RenderingStrategies.MYBATIS3);
System.out.println( provider.getSelectStatement() );
略
命名示例:
Keyword | Sample | SQL 部分 |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals | findByFirstnameIs, findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection age) | … where x.age not in ?1 |
TRUE | findByActiveTrue() | … where x.active = true |
FALSE | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |