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 的寫法更簡潔。
使用 SelectStatementProvider 構建 Dynamic SQL 。使用 SqlBuilder 的 select
方法可以指定查詢列,使用 from
方法可以指定查詢表,使用 where
方法可以構建查詢條件,使用 orderBy
方法可以指定排序。
import static org.mybatis.dynamic.sql.SqlBuilder.*; // .isEqualTo(), .isBetween(), ... import static xxx.yyy.zzz.dao.EmployeeDynamicSqlSupport.*; // .employee, .departmentId, .salary, ... // PageHelper.startPage(pageNum, pageSize); SelectStatementProvider provider = SqlBuilder .select(EmployeeDao.selectList) .from(employee) .where(departmentId, isEqualTo(2L)) .and(salary, isBetween(500).and(3000)) .orderBy(salary.descending()) .build().render(RenderingStrategies.MYBATIS3); employeeDao.selectMany(provider).forEach(System.out::println);
使用 SelectDSLCompleter 接口,實現它,或使用等價的 lambda 表達式。Completer 寫法要比 Provider 寫法 “省” 兩三行代碼。
import static org.mybatis.dynamic.sql.SqlBuilder.*; // .isEqualTo(), .isBetween(), ... import static xxx.yyy.zzz.dao.EmployeeDynamicSqlSupport.*; // .departmentId, .salary, ... // PageHelper.startPage(pageNum, pageSize); SelectDSLCompleter completer = c -> c .where(departmentId, isEqualTo(2L)) .and(salary, isBetween(500).and(3000)) .orderBy(salary.descending()); employeeDao.select(completer).forEach(System.out::println);
SelectDSLCompleter 的底層最終還是使用的是 Provider ,即 Provider 才是根本。
當你擁有一個 Completer 對象時,你可以使用類似如下方式獲得對應的 Provider 對象:
SelectDSLCompleter completer = ...; SelectStatementProvider provider = completer.apply(SqlBuilder.select(selectList).from(employee)) .build().render(RenderingStrategies.MYBATIS3);
注意
SelectDSLCompleter 寫法要比 SelectStatementProvider 寫法簡潔,因為它省略掉了關於查詢的列(即 SQL 語句中 select ...
的這一部分的 )設置。
在簡單情況下,你所執行的 SQL 語句可能就是 select *
,或者是 select 所有列
這種邏輯,但是對於有些情況,比如關聯查詢,SelectDSLCompleter 省略掉這一部分之后,返回會讓你對這部分無法設置,從而拿不到你預期的結果。
所有,優先建議大家使用 Provider 。或者,使用 Completer ,然后在必要的時候轉成 Provider 使用。
#7. 邏輯條件的組合
邏輯條件的組合大體分為 2 種:
-
單純的
...與...與...
/...或...或...
-
與或
混用,由於或
的優先級更高,因此可以改造成(... and ...) or (... and ...) or ...
這樣的統一形式。
import static org.mybatis.dynamic.sql.SqlBuilder.*; import static xxx.yyy.zzz.dao.EmployeeDynamicSqlSupport.*; // Provider 寫法 SelectStatementProvider provider = SqlBuilder .select(EMPLOYEE.allColumns()) .from(employee) .where() .and(departmentId, isEqualTo(2L)) .and(salary, isBetween(500).and(3000)) .orderBy(salary.descending()) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where() .and(departmentId, isEqualTo(2L)) .and(salary, isLessThan(1300)) .and(...) ;
另外,你可以把第一個條件『納入』到 where()
中,從而寫成
import static org.mybatis.dynamic.sql.SqlBuilder.*; import static xxx.yyy.zzz.dao.EmployeeDynamicSqlSupport.*; // Provider 寫法 SelectStatementProvider provider = SqlBuilder.select(EMPLOYEE.allColumns()).from(EMPLOYEE) .where(departmentId, isEqualTo(2L)) .and(salary, isBetween(500).and(3000)) .orderBy(salary.descending()) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where(departmentId, isEqualTo(2L)) .and(salary, isLessThan(1300)) .and(...) ;
除了上述的『平行』地寫法外,你還可以將 and()
方法嵌入到 and()
方法中,寫成形如:.and(..., and(...), and(...), ...)
的形式:
// Provider 寫法 SelectStatementProvider provider = SqlBuilder.select(EMPLOYEE.allColumns()).from(EMPLOYEE) .where(departmentId, isEqualTo(2L), and(salary, isBetween(500).and(3000))) .orderBy(salary.descending()) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where() .and(departmentId, isEqualTo(2L), and(salary, isLessThan(1300)), and(...)) ;
// Provider 寫法 SelectStatementProvider provider = SqlBuilder.select(EMPLOYEE.allColumns()).from(EMPLOYEE) .where() .or(salary, isLessThan(1000)) .or(commission, isNotNull()) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where() .or(salary, isLessThan(1000)) .or(commission, isNotNull()) .or(...) ;
和 ...與...與...
情況一樣,你可以把第一個條件『納入』到 where()
中,從而寫成
// Provider 寫法 SelectStatementProvider provider = SqlBuilder.select(EMPLOYEE.allColumns()).from(EMPLOYEE) .where(salary, isLessThan(1000)) .or(commission, isNotNull()) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where(salary, isLessThan(1000)) .or(commission, isNotNull()) .or(...) ;
和 與與
情況一樣,除了上述的『平行』地寫法外,你也可以將 or()
方法嵌入到 or()
方法中,寫成形如:.or(..., or(...), or(...), ...)
的形式:
// Provider 寫法 SelectStatementProvider provider = SqlBuilder.select(EMPLOYEE.allColumns()).from(EMPLOYEE) .where(salary, isLessThan(1000), or(commission, isGreaterThan(200))) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where(salary, isLessThan(1000), or(commission, isNotNull()), or(...)) ;
與或
混用的情況下,先要把你『心里』的 SQL 語句改造成通用形式:(... and ...) or (... and ...) or ...
。
// Provider 寫法 SelectStatementProvider provider = SqlBuilder.select(EMPLOYEE.allColumns()).from(employee) .where() .or(departmentId, isEqualTo(2L), and(salary, isLessThan(1500))) .or(departmentId, isEqualTo(3L), and(salary, isGreaterThan(1300))) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where() .or(departmentId, isEqualTo(2L), and(salary, isLessThan(1500))) .or(departmentId, isEqualTo(3L), and(salary, isGreaterThan(1300)));
一樣,你也可以將第一個條件納入到 where()
中。
// Provider 寫法 SelectStatementProvider provider = SqlBuilder.select(EMPLOYEE.allColumns()).from(employee) .where(departmentId, isEqualTo(2L), and(salary, isLessThan(1500))) .or(departmentId, isEqualTo(3L), and(salary, isGreaterThan(1300))) .build().render(RenderingStrategies.MYBATIS3); // Completer 寫法 SelectDSLCompleter completer = c -> c .where(departmentId, isEqualTo(2L), and(salary, isLessThan(1500))) .or(departmentId, isEqualTo(3L), and(salary, isGreaterThan(1300)));
#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>
先在 Dao 中添加一個 selectDepartmentWithEmployeeQuantity 方法,然后使用 @ResultMap 注解引用定義好結果集映射規則;
public interface UmsAdminDao { @SelectProvider(type = SqlProviderAdapter.class, method = "select") @ResultMap("selectDepartmentWithEmployeeQuantityResultMap") List<Department> selectDepartmentWithEmployeeQuantity() { }
然后在 Service 中調用它,StatementProvider 即可,對應的 Java 代碼實現如下:
BasicColumn[] selectList = BasicColumn.columnList(DEPARTMENT.id, DEPARTMENT.name, DEPARTMENT.location, SqlBuilder.count(EMPLOYEE.id).as("employee_quantity")); SelectStatementProvider provider = SqlBuilder.select(selectList) .from(DEPARTMENT) .leftJoin(EMPLOYEE).on(DEPARTMENT.id, equalTo(EMPLOYEE.departmentId)) .groupBy(DEPARTMENT.id) .build().render(RenderingStrategies.MYBATIS3); departmentDao.selectDepartmentWithEmployeeQuantity(provider).forEach(System.out::println);
考慮到 Dao 中有自動生成的 selectOne 和 selectMany 可以供我們使用,所以,我們的 selectDepartmentWithEmployeeQuantity 方法可以去調用它們(從而將 provider/completer 參數挪到方法中,而不是從外部傳入)。
改造 dao 接口中的 selectDepartmentWithEmployeeQuantity 方法:
default List<Department> selectDepartmentWithEmployeeQuantity() { BasicColumn[] selectList = BasicColumn.columnList(id, name, location, SqlBuilder.count(EMPLOYEE.id).as("employee_quantity")); SelectStatementProvider provider = SqlBuilder.select(selectList) .from(DEPARTMENT) .leftJoin(EMPLOYEE).on(id, equalTo(EMPLOYEE.departmentId)) .groupBy(id) .build().render(RenderingStrategies.MYBATIS3); return selectMany(provider); }
#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) |