Mybatis Dynamic SQL


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> 
Copied!

在執行 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 
Copied!

一切准備就緒,執行 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"); // 表名 } } } 
Copied!

這些內部類都繼承自 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.*; 
Copied!

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; 
Copied!

在使用 Dynamic SQL 來實現上述 SQL 語句時,你會發現你所調用的 Dao 的 select 方法接收 2 種類型的參數:SelectStatementProvider 和 SelectDSLCompleter 。

也就是說,你有 2 種方式、風格來『描述』你心里想執行的 SQL 語句。Provider 的寫法更像 SQL 語句,對於熟悉 SQL 語句的 Dynamic SQL 的初學者來說,更容易理解;Completer 的寫法更簡潔。

方式一:SelectStatementProvider

使用 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

使用 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); 
 
Completer 轉 Provider

SelectDSLCompleter 的底層最終還是使用的是 Provider ,即 Provider 才是根本。

當你擁有一個 Completer 對象時,你可以使用類似如下方式獲得對應的 Provider 對象:

SelectDSLCompleter completer = ...; SelectStatementProvider provider = completer.apply(SqlBuilder.select(selectList).from(employee)) .build().render(RenderingStrategies.MYBATIS3); 
 

 

Copied!

注意

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(...) ; 
Copied!
 
縮寫

另外,你可以把第一個條件『納入』到 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(...) ; 
Copied!
縮寫

和 ...與...與... 情況一樣,你可以把第一個條件『納入』到 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))); 
Copied!
 
縮寫

一樣,你也可以將第一個條件納入到 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))); 
 
Copied!
 

#8. 條件刪除

TIP

使用 Dynamic SQL 實現條件刪除,直接調用 Mapper 接口中生成好的 delete 方法即可。

我們『心里』期望執行的 SQL 如下:

DELETE FROM department WHERE name = 'test'; 
Copied!

使用 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); 
Copied!

#9. 條件修改

TIP

使用 Dynamic SQL 實現條件修改,直接調用 Mapper 接口中生成好的update方法即可。

我們『心里』期望執行的 SQL 如下:

update department set name = 'hello', location = 'world' where id = 5; 
Copied!

使用 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); 
Copied!

#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; 
Copied!

現在 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> 
Copied!
初步方案

先在 Dao 中添加一個 selectDepartmentWithEmployeeQuantity 方法,然后使用 @ResultMap 注解引用定義好結果集映射規則;

public interface UmsAdminDao { @SelectProvider(type = SqlProviderAdapter.class, method = "select") @ResultMap("selectDepartmentWithEmployeeQuantityResultMap") List<Department> selectDepartmentWithEmployeeQuantity() { } 
Copied!

然后在 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); 
Copied!
 
改進方案
 

考慮到 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); } 
Copied!

 

#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() ); 
Copied!

命名示例:

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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM