1. 程式人生 > 實用技巧 >Mybatis3 Dynamic Sql實踐

Mybatis3 Dynamic Sql實踐

背景

最近在做專案的時候,需要用到多表關聯查詢,關聯的表和查詢的條件都是不確定的,且可能會有非常複雜的查詢場景,導致查詢條件會很複雜,在這種場景下,sql模版是不確定的,所以傳統的MyBatis3風格(即經常用的xml風格)或者MyBatis3Simple風格的sql模版框架就顯得力不從心,亟需一個更加靈活的動態sql框架,就在我一籌莫展的時候,Mybatis3 Dynamic Sql走進了我的視線。

前言

本文將首先介紹一下Mybatis3 Dynamic Sql是什麼,有啥好處,怎麼使用Mybatis Generator (MBG)來生成Mybatis3 Dynamic Sql風格的DAO層程式碼,以及怎麼使用其來構建自己的sql語句,以及最後,當Mybatis3 Dynamic Sql現有功能不滿足我們需求的時候,怎樣去拓展它,實現諸如having語句、find_in_set函式、group_concat函式、甚至是ST_Distance_Sphere(ADS球面距離函式)等一些Mybatis3 Dynamic Sql暫不支援的sql語句或函式。

Mybatis3 Dynamic Sql簡介

Mybatis3 Dynamic Sql是一個SQL模板庫,它內建支援MyBatis和Spring JDBC template這兩個O/R-M框架,生成的SQL可以直接在這兩個框架中執行。

Mybatis3 Dynamic Sql的優勢

  • 不再生成XML對映檔案,不需要支援"by example"能力,大量啟用MyBatis3的註解,結合現代編碼風格,總體程式碼量比傳統執行時生成的程式碼小很多,也簡單很多。

  • 使用它生成的高階條件查詢靈活性較大,並支援分頁、join、union、group by、order by等語句,支援通過任意and/or及巢狀的條件組合構建複雜的where條件查詢。

Mybatis Generator (MBG) 用法

MBG的用法以及相關的配置,這篇文章已經很詳細了,https://www.cnblogs.com/ZhangZiSheng001/p/12820344.html,在此就不再贅述。

Mybatis3 Dynamic Sql使用方法

如果你資料庫中表的DDL是類似這樣的:

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)
);
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");
}
}
}
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);
}
@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);
}
}
   SimpleTableRecord record = new SimpleTableRecord();
record.setId(100);
record.setFirstName("Joe");
record.setLastName("Jones");
record.setBirthDate(new Date());
record.setEmployed(true);
record.setOccupation("Developer");

InsertStatementProvider<SimpleTableRecord> insertStatement = insert(record)
.into(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategies.MYBATIS3);

int rows = mapper.insert(insertStatement);
   UpdateStatementProvider updateStatement = update(animalData)
.set(bodyWeight).equalTo(record.getBodyWeight())
.set(animalName).equalToNull()
.where(id, isIn(1, 5, 7))
.or(id, isIn(2, 6, 8), and(animalName, isLike("%bat")))
.or(id, isGreaterThan(60))
.and(bodyWeight, isBetween(1.0).and(3.0))
.build()
.render(RenderingStrategies.MYBATIS3);

int rows = mapper.update(updateStatement);
public class FindInSet extends AbstractFunction<Object, FindInSet> {

private Object arg;

private FindInSet(Object arg, BindableColumn<Object> column) {
super(column);
this.arg = arg;
}

public static FindInSet of(Object arg, BindableColumn<Object> column) {
return new FindInSet(arg, column);
}

@Override
public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) {
if(arg instanceof String) {
//引數兩邊帶引號
return "find_in_set(" + "\"" + securitySql(arg.toString()) + "\"," + this.column.renderWithTableAlias(tableAliasCalculator) + ")";
} else {
return "find_in_set(" + securitySql(arg.toString()) + "," + this.column.renderWithTableAlias(tableAliasCalculator) + ")";
}
}

/**
* 防止sql注入
* @param sql
* @return
*/
private static String securitySql(String sql) {
if(null == sql) {
return null;
}
return sql.replace(";", "");
}

@Override
protected FindInSet copy() {
return new FindInSet(arg, column);
}

}
public class Point extends AbstractFunction<Object, Point> {

private BindableColumn<Object> column1;

private BindableColumn<Object> column2;

public Point(BindableColumn<Object> column1, BindableColumn<Object> column2) {
super(Constant.of(""));
this.column1 = column1;
this.column2 = column2;
}

public static Point of(BindableColumn<Object> column1, BindableColumn<Object> column2) {
return new Point(column1, column2);
}

@Override
protected Point copy() {
return new Point(column1, column2);
}

@Override
public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) {
return "point(" + column1.renderWithTableAlias(tableAliasCalculator) +
"," + column2.renderWithTableAlias(tableAliasCalculator) + ")";
}
}
public class StDistanceSphere extends AbstractFunction<Object, StDistanceSphere> {

private Point point1;

private Point point2;

private Long radius;

public StDistanceSphere(Point point1, Point point2, Long radius) {
super(Constant.of(""));
this.point1 = point1;
this.point2 = point2;
this.radius = radius;
}

public StDistanceSphere(Point point1, Point point2) {
super(Constant.of(""));
this.point1 = point1;
this.point2 = point2;
}

public static StDistanceSphere of(Point point1, Point point2, Long radius) {
return new StDistanceSphere(point1, point2, radius);
}

public static StDistanceSphere of(Point point1, Point point2) {
return new StDistanceSphere(point1, point2);
}

public StDistanceSphere withRadius(Long radius) {
this.radius = radius;
return this;
}

@Override
protected StDistanceSphere copy() {
return new StDistanceSphere(point1, point2, radius);
}

@Override
public String renderWithTableAlias(TableAliasCalculator tableAliasCalculator) {
if(null == radius) {
return "ST_Distance_Sphere(" + point1.renderWithTableAlias(tableAliasCalculator) + ","
+ point2.renderWithTableAlias(tableAliasCalculator) + ")";
} else {
return "ST_Distance_Sphere(" + point1.renderWithTableAlias(tableAliasCalculator) + ","
+ point2.renderWithTableAlias(tableAliasCalculator) + "," + radius + ")";
}
}
}

https://www.cnblogs.com/ZhangZiSheng001/p/12820344.html

參考資料

專案地址https://github.com/mybatis/mybatis-dynamic-sql 官方文件https://mybatis.org/mybatis-dynamic-sql/docs/introduction.html

說了這麼多,其實Mybatis3 Dynamic Sql還有很多不足的地方,比如前面說到的,不支援某些聚合函式、不支援having語句,不支援子查詢等,但相信這些在不久的將來都會逐步完善起來,本文只是起一個拋磚引玉的作用,把這個專案介紹給大家,主要是本人在使用這個專案的過程中,覺得實在是挺好用的,而且在拓展一些sql語句的時候,也發現該專案拓展性極強,程式碼風格和設計也很好,確實是很值得學習的。目前Mybatis3 Dynamic Sql是github上的一個開源專案,主要由Jeff Butler大神維護,相關的文件和社群還不是很完善,也歡迎感興趣的小夥伴去開源社群貢獻一下自己的力量,讓Mybatis3 Dynamic Sql越來越強大!

結語

實現ST_Distance_Sphere函式的方式同樣可以繼承AbstractFunction:

實現完point函式之後,就可以實現ST_Distance_Sphere函數了,以下是阿里雲文件中關於ST_Distance_Sphere函式的說明:

實現point函式類似,同樣可以繼承AbstractFunction:

拓展實現ADS的point及ST_Distance_Sphere函式

Mybatis3 Dynamic Sql目前並不支援find_in_set函式,但是通過閱讀原始碼,發現要實現這個函式也並不難,只需要繼承AbstractFunction這個類就行了,顧名思義,這個類是一個抽象函式類,接收一個column(列)作為引數。find_in_set函式實現如下:

拓展實現find_in_set函式

目前Mybatis3 Dynamic Sql還處於發展中的階段,很多函式或者語句的支援還很有限,當這並不妨礙我們自己拓展並使用它,Mybatis3 Dynamic Sql的可拓展性還是很好的。下面舉幾個我在使用的過程中,拓展實現的一些函式或語句的例子。

當然了,這裡也並不是說這種方式在技術上會比xml的方式先進多少,而是提供了一種更加靈活地構建sql的選擇,當你的查詢條件很複雜的時候,當你需要多表join,並且join的表還都不確定的時候(執行時確定),當你無法通過xml構建出穩定的sql模版的時候,那麼,或許你應該考慮這種方式了。

可以看出,由於Mybatis3 Dynamic Sql大量使用了Java8的函式、lambda表示式、介面預設方法實現等新特性,以及鏈式函式呼叫的風格,所以構建sql語句的過程中,程式碼的自解釋性很強,基本符合我們平時書寫sql的習慣,寫Java程式碼的同時其實就是在寫sql。這樣做的好處是顯而易見的,sql構建靈活並且程式碼可讀性強,但是缺點也是有的,學習成本比較高,對於習慣了使用xml來配置sql模版的同學來說可能一開始會不太適應,但是其實只要使用了一段時間,就會感覺到這種方法確實是比xml的方式要便捷和靈活。

同樣地,也可以構建insert語句或者update語句:

有了這兩個類,我們就可以通過Mybatis3 Dynamic Sql的風格構建自己的查詢語句,並且交由mapper類執行了,如下:

該類可以理解為一個支援類,它的作用是可以很方便地拿到表中每個欄位對應的SqlColumn,方便後面構建查詢或插入語句。另外一個類就是mapper類,它通過註解的方式實現了資料庫欄位和類欄位的對映,可以很方便地執行查詢或者插入語句,如下:

那麼使用Mybatis Generator (MBG)之後,不會生成xml檔案,而是會預設生成兩個類,一個是support類,類似這樣: