現狀
MyBatis 的強大特性之一便是它的動態 SQL。如果你有使用 JDBC 或其它類似框架的經驗,你就能體會到根據不同條件拼接 SQL 語句的痛苦。例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最后一個列名的逗號。利用動態 SQL 這一特性可以徹底擺脫這種痛苦。
提到Mybatis動態Sql,多數人瞬間想到的畫面是這樣的
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
或者像這樣
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
在 Mapper XML
中借助 動態 SQL 元素
來實現SQL語句的條件拼接。
不得不說Mybatis的這個實現已經非常強大了,大大提高了我們拼接SQL的效率。
但是對于程序員來說,在代碼中進行條件判斷遠比在XML中更自如,更靈活,更有底氣,我們更希望在享受 強大的 MyBatis 映射
的同時,讓動態SQL的編寫更加簡潔、更加可控
—— 于是真正的Mybatis動態SQL —— MyBatis Dynamic SQL
應運而生。
MyBatis Dynamic SQL
項目地址
https://github.com/mybatis/mybatis-dynamic-sql
官方文檔
https://mybatis.org/mybatis-dynamic-sql/docs/CHANGELOG.html
項目介紹
Initial Release - December 17, 2017
Last Published: 23 November 2019 | Version: 1.1.4
2017-12-17,該項目發布了第一版本,
最近一個版本是2019-11-23發布的1.1.4
,該項目是Mybatis官方項目之一。
作者
Jeff Butler【https://github.com/jeffgbutler】,是 Mybatis組織 成員,是 Mybatis 的主要貢獻者,同時也是我們常用工具 MyBatis Generator 的作者。
Mybatis Generator
可想而知,Mybatis Dynamic SQL 被 Mybatis Genrator 很好的支持,在Mybatis Dynamic SQL 最新版發布的第二天,2019-11-24 Mybatis Genrator 1.4.0 發布,這個版本做了較大改動,主要是
- New Runtime for Kotlin using MyBatis Dynamic SQL
- New Runtime for Java using MyBatis Dynamic SQL
- MyBatis Dynamic SQL is now the default runtime
- Move to Java 8
- Remove support for iBatis2
主要是移除對iBatis2的支持,同時使用 MyBatis Dynamic SQL 作為默認運行時,這無疑給Mybatis用戶帶來了極大便利。在以后的文章會進行詳細介紹。
簡介
這個項目是用來生成動態SQL語句的框架。可以將它看作是額外支持MyBatis3和Spring JDBC的類型安全的SQL模板庫。
該庫將生成完整的DELETE、INSERT、SELECT和UPDATE格式化語句,可由MyBatis或Spring使用。最普遍的使用情況是生成能夠被Mybatis直接使用的的語句和一組匹配參數。同時,它也能夠生成兼容Spring JDBC templates的語句和參數對象。
這個庫通過實現一個 類似于SQL領域專用語言 (domain-specific language,DSL)來工作,DSL能夠創建一個包含完整SQL語句以及語句所需要全部參數的對象。這個對象能夠被Mybatis作為傳入Mapper方法的參數直接使用,正如我們平常所做的那樣。
特性
該庫能生成如下類型的SQL語句(并不止于這些,最新信息可查看 Change Log):
- 支持子查詢的靈活的WHERE子句
- 帶有靈活WHERE子句的DELETE語句
- 多種INSERT語句:
- 單條記錄的插入語句并且將會插入
null
值(一個“完全”插入) - 單條記錄的插入語句并且將忽略輸入值為
null
的列(一個“選擇性”插入) - 帶有SELECT語句的插入
- 多行插入(類似于<foreach/>)
- 批量插入(Spring Batch 或 JDBC Batch)
- 支持返回自增主鍵
- 單條記錄的插入語句并且將會插入
- 帶有靈活列表、靈活WHERE子句的SELECT語句,支持distinct、count(distinct ...)、group by、
joins, unions, “order by”, 等等。 - 帶有靈活WHERE子句的UPDATE語句,像插入語句一樣,也兩種更新:
- “完全”更新
- “選擇性”更新
目標
這個庫的主要目標是:
- 類型安全 — 該庫將盡可能確保參數類型與數據庫列類型匹配
- 富有表現力 — 語句以一種清晰地傳達其含義的方式構建 (Hamcrest提供的一些靈感)
- 靈活的 — where子句能夠使用and、or以及嵌套條件的任意組合來構建
- 可擴展的 — 該庫將生成為MyBatis3、Spring JDBC Template或純JDBC工作的語句。它還可以擴展為為其他框架生成子句。如果沒有一個內置的 條件語句 充分滿足您的需要,你可以很容易的自定義(畢竟代碼的編寫就是你擅長的事)
- 小 — 沒有依賴傳遞
提示
Java版本在快速的更新,但是直到今天,對于Java開發者影響最大的或許就是Java8,Java8為開發者帶了TYPE_USE Annotations
,帶來了函數編程和Lamda
、新的日期和時間處理庫
、Optional
等等,甚至有人說 —— Java8給開發者帶來了工具和機會【https://www.infoq.com/articles/Type-Annotations-in-Java-8/】
比如:
- Checker Framework【https://checkerframework.org/】Java類型系統增強框架,或者說是靜態類型編譯時檢查工具。
- GridExcel【https://github.com/liuhuagui/gridexcel】使用函數編程使Excel讀寫變得更簡單。
同樣的,Java8促使了 MyBatis Dynamic SQL 的出現,后續我們會看到,函數編程在 MyBatis Dynamic SQL 被運用到極致。
所以,如果要使用 MyBatis Dynamic SQL ,請保證你的工作環境為:Java8+
快速使用
步驟
使用 MyBatis Dynamic SQL 需要下列步驟:
- 創建Table和Column對象
- (為Mybatis3)創建 Mappers (XML or Java Based)
- 寫SQL并且去使用它
出于討論的目的,首先我們展示出來用來執行CRUD的表結構:
create table SimpleTable (
id int not null,
first_name varchar(30) not null,
last_name varchar(30) not null,
birth_date date not null,
employed varchar(3) not null,
occupation varchar(30) null,
primary key(id)
);
定義常量Tables和Columns
org.mybatis.dynamic.sql.SqlTable
類被用來定義一個Table。Table 的定義包含實際的表明(甚至是合適的schema 和 catalog )。如果期望,Table的alias能夠用于SQL語句。你的Table應該繼承SqlTable 類。
org.mybatis.dynamic.sql.SqlColumn
類被用來定義在庫中使用的columns,SqlColumns 應該基于SqlTable來創建。一個列的定義包含:
- The Java type
- The actual column name (an alias can be applied in a select statement)
- The JDBC type (optional)
- 如果不需要默認的Type Handler,可以給出需要在Mybatis中使用的Type Handler的完全限定名。
(注意: 不同于PO(Persistent Object 持久化對象,它跟持久層(通常是關系型數據庫)的數據結構形成一一對應的映射關系),在這里Table和Column對象,是對 表
以及 列
的嚴格意義的抽象,它們被定義為靜態常量,允許你在SQL語句構建中不斷重用,就像是在命令行中手寫SQL語句那樣。)
建議使用如下使用模式來提供最大的靈活性。這個模式允許你用“qualified” or “un-qualified” 的習慣來使用你的Tables和columns,就像在命令行中寫SQL語句那樣。例如,可以將下面這個column寫為firstName或simpleTable.firstName。
package examples.simple;
import java.sql.JDBCType;
import java.util.Date;
import org.mybatis.dynamic.sql.SqlColumn;
import org.mybatis.dynamic.sql.SqlTable;
public final class SimpleTableDynamicSqlSupport {
public static final SimpleTable simpleTable = new SimpleTable();
public static final SqlColumn<Integer> id = simpleTable.id;
public static final SqlColumn<String> firstName = simpleTable.firstName;
public static final SqlColumn<String> lastName = simpleTable.lastName;
public static final SqlColumn<Date> birthDate = simpleTable.birthDate;
public static final SqlColumn<Boolean> employed = simpleTable.employed;
public static final SqlColumn<String> occupation = simpleTable.occupation;
public static final class SimpleTable extends SqlTable {
public final SqlColumn<Integer> id = column("id", JDBCType.INTEGER);
public final SqlColumn<String> firstName = column("first_name", JDBCType.VARCHAR);
public final SqlColumn<String> lastName = column("last_name", JDBCType.VARCHAR);
public final SqlColumn<Date> birthDate = column("birth_date", JDBCType.DATE);
public final SqlColumn<Boolean> employed = column("employed", JDBCType.VARCHAR, "examples.simple.YesNoTypeHandler");
public final SqlColumn<String> occupation = column("occupation", JDBCType.VARCHAR);
public SimpleTable() {
super("SimpleTable");
}
}
}
創建 MyBatis3 Mappers
這個庫能創建用作MyBatis mapper輸入的類。這些類包含生成的SQL以及匹配的參數集合。兩者都是MyBatis所需要的。這些對象是MyBatis mapper方法的唯一參數。
(注意:MyBatis Dynamic SQL 不需要XML文件就能工作的很好,但并不意味著不支持XML,畢竟 MyBatis 最初被設計為是一個 XML 驅動的框架。當你使用關聯查詢,需要復雜的映射,那么使用XML <ResultMap> 與 MyBatis Dynamic SQL 結合起來或者是更好選擇,你的XML或許只需要包含一些<ResultMap>)
例如,一個Mapper可以像下面這樣:
package examples.simple;
import java.util.List;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider;
import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
@Mapper
public interface SimpleTableAnnotatedMapper {
@InsertProvider(type=SqlProviderAdapter.class, method="insert")
int insert(InsertStatementProvider<SimpleTableRecord> insertStatement);
@UpdateProvider(type=SqlProviderAdapter.class, method="update")
int update(UpdateStatementProvider updateStatement);
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@Results(id="SimpleTableResult", value= {
@Result(column="A_ID", property="id", jdbcType=JdbcType.INTEGER, id=true),
@Result(column="first_name", property="firstName", jdbcType=JdbcType.VARCHAR),
@Result(column="last_name", property="lastName", jdbcType=JdbcType.VARCHAR),
@Result(column="birth_date", property="birthDate", jdbcType=JdbcType.DATE),
@Result(column="employed", property="employed", jdbcType=JdbcType.VARCHAR, typeHandler=YesNoTypeHandler.class),
@Result(column="occupation", property="occupation", jdbcType=JdbcType.VARCHAR)
})
List<SimpleTableRecord> selectMany(SelectStatementProvider selectStatement);
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@ResultMap("SimpleTableResult")
SimpleTableRecord selectOne(SelectStatementProvider selectStatement);
@DeleteProvider(type=SqlProviderAdapter.class, method="delete")
int delete(DeleteStatementProvider deleteStatement);
@SelectProvider(type=SqlProviderAdapter.class, method="select")
long count(SelectStatementProvider selectStatement);
}
用Mybatis3執行SQL
在DAO或服務類中,可以使用生成的語句作為映射器方法的輸入。下面是一個來自examples.simple.SimpleTableAnnotatedMapperTest
的示例:
@Test
public void testSelectByExample() {
try (SqlSession session = sqlSessionFactory.openSession()) {
SimpleTableAnnotatedMapper mapper = session.getMapper(SimpleTableAnnotatedMapper.class);
SelectStatementProvider selectStatement = select(id.as("A_ID"), firstName, lastName, birthDate, employed, occupation)
.from(simpleTable)
.where(id, isEqualTo(1))
.or(occupation, isNull())
.build()
.render(RenderingStrategies.MYBATIS3);
List<SimpleTableRecord> rows = mapper.selectMany(selectStatement);
assertThat(rows.size()).isEqualTo(3);
}
}
最后
想要了解更多,Mybatis Dynamic SQL 高級用法,工作原理,實踐中的坑,以及如何更友好的使用 Mybatis Generator 可以關注 公眾號:流花鬼的博客 ,持續更新中。。。