Mybatis增強型註解簡化SQL語句(一)

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

1. 背景

MyBatis提供了簡單的Java註解,使得我們可以不配置XML格式的Mapper檔案,也能方便的編寫簡單的資料庫操作程式碼:

public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{userId}")
User getUser(@Param("userId") String userId);
}

但是註解對動態SQL的支援一直差強人意,即使MyBatis提供了InsertProvider等*Provider註解來支援註解的Dynamic SQL,也沒有降低SQL的編寫難度,甚至比XML格式的SQL語句更難編寫和維護。
註解的優勢在於能清晰明瞭的看見介面所使用的SQL語句,拋棄了繁瑣的XML程式設計方式。但沒有良好的動態SQL支援,往往就會導致所編寫的DAO層中的介面冗餘,所編寫的SQL語句很長,易讀性差……
Mybatis在3.2版本之後,提供了LanguageDriver介面,我們可以使用該介面自定義SQL的解析方式。故在這裡向大家介紹下以此來實現註解方式下的動態SQL。

2. 實現方案

我們先來看下LanguageDriver介面中的3個方法:

public interface LanguageDriver {
ParameterHandler createParameterHandler(MappedStatement var1, Object var2, BoundSql var3);
SqlSource createSqlSource(Configuration var1, XNode var2, Class<?> var3);
SqlSource createSqlSource(Configuration var1, String var2, Class<?> var3);
}
  1. createParameterHandler方法為建立一個ParameterHandler物件,用於將實際引數賦值到JDBC語句中
  2. 將XML中讀入的語句解析並返回一個sqlSource物件
  3. 將註解中讀入的語句解析並返回一個sqlSource物件

一旦實現了LanguageDriver,我們即可指定該實現類作為SQL的解析器,在XML中我們可以使用 lang 屬性來進行指定

<typeAliases>
<typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<select id="selectBlog" lang="myLanguage">
SELECT * FROM BLOG
</select>

也可以通過設定指定解析器的方式:

<settings>
<setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

如果不使用XML Mapper的形式,我們可以使用@Lang註解

public interface Mapper {
@Lang(MyLanguageDriver.class) 
@Select("SELECT * FROM users")
List<User> selectUser();
}

LanguageDriver的預設實現類為XMLLanguageDriver和RawLanguageDriver;分別為XML和Raw,Mybatis預設是XML語言,所以我們來看看XMLLanguageDriver中是怎麼實現的:

public class XMLLanguageDriver implements LanguageDriver {
public XMLLanguageDriver() {
}
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
if(script.startsWith("<script>")) {
XPathParser textSqlNode1 = new XPathParser(script, false, configuration.getVariables(),
new XMLMapperEntityResolver());
return this.createSqlSource(configuration, textSqlNode1.evalNode("/script"), parameterType);
} else {
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
return (SqlSource)(textSqlNode.isDynamic()?new DynamicSqlSource(configuration, textSqlNode)
:new RawSqlSource(configuration, script, parameterType));
}
}
}

發現其實mybatis已經幫忙寫好了解析邏輯,而且發現如果是以< script>開頭的字串傳入後,會被以XML的格式進行解析。那麼方案就可以確認了,我們繼承XMLLanguageDriver這個類,並且重寫其createSqlSource方法,按照自己編寫邏輯解析好sql後,再呼叫父類的方法即可。

3. 實現自定義註解

本段中給出一些常見的自定義註解的實現和使用方式。

3.1 自定義Select In註解

在使用Mybatis註解的時候,發現其對Select In格式的查詢支援非常不友好,在字串中輸入十分繁瑣,可以通過將自定義的標籤轉成格式;下面便通過我們自己實現的LanguageDriver來實現SQL的動態解析:

DAO介面層中程式碼如下:

@Select("SELECT * FROM users WHERE id IN (#{userIdList})")
@Lang(SimpleSelectInLangDriver.class)
List<User> selectUsersByUserId(List<Integer> userIdList);

LanguageDriver實現類如下:

// 一次編寫即可
public class SimpleSelectInLangDriver extends XMLLanguageDriver implements LanguageDriver {
private static final Pattern inPattern = Pattern.compile("\\(#\\{(\\w )\\}\\)");
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
Matcher matcher = inPattern.matcher(script);
if (matcher.find()) {
script = matcher.replaceAll("<foreach collection=\"$1\" item=\"_item\" open=\"(\" "  
"separator=\",\" close=\")\" >#{_item}</foreach>");
}
script = "<script>"   script   "</script>";
return super.createSqlSource(configuration, script, parameterType);
}
}

通過自己實現LanguageDriver,在伺服器啟動的時候,就會將我們自定義的標籤解析為動態SQL語句,其等同於:

@Select("SELECT * "  
"FROM users "  
"WHERE id IN "  
"<foreach item='item' index='index' collection='list'open='(' separator=',' close=')'>"  
"#{item}"  
"</foreach>")
List<User> selectUsersByUserId(List<Integer> userIdList);

通過實現LanguageDriver,剝離了冗長的動態SQL語句,簡化了Select In的註解程式碼。

需要注意的是在使用Select In的時候,請務必在傳入的引數前加@Param註解,否則會導致Mybatic找不到引數而丟擲異常。

3.2 自定義Update Bean註解

在擴充套件update註解時,資料庫每張表的欄位和實體類的欄位必須遵循一個約定(資料庫中採用下劃線命名法,實體類中採用駝峰命名法)。當我們update的時候,會根據每個欄位的對映關係,寫出如下程式碼:

<update id="updateUsersById" parameterType="com.lucifer.bean.User">
UPDATE users
<set>
<if test=“userName != null">
user_name = #{userName} ,
</if>
<if test=“password != null">
password = #{password} ,
</if>
<if test=“phone != null">
phone = #{phone},
</if>
<if test=“email != null">
email = #{email},
</if>
<if test=“address != null">
address = #{address},
</if>
<if test="gmtCreated != null">
gmt_created = #{gmtCreated},
</if>
<if test="gmtModified != null">
gmt_modified = #{gmtModified},
</if>
</set>
WHERE id = #{id}
</update> 

我們可以將實體類中的駝峰式程式碼轉換為下劃線式命名方式,這樣就可以將這種對映規律自動化
經過實現LanguageDriver後,註解程式碼為

@Update("UPDATE users (#{user}) WHERE id = #{id}")
@Lang(SimpleUpdateLangDriver.class)
void updateUsersById(User user);

相對於原始的程式碼量有很大的減少,並且,一個類中欄位越多,改善也就越明顯。實現方式為:

public class SimpleUpdateLangDriver extends XMLLanguageDriver implements LanguageDriver{
private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w )\\}\\)");
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
Matcher matcher = inPattern.matcher(script);
if (matcher.find()) {
StringBuilder sb = new StringBuilder();
sb.append("<set>");
for (Field field : parameterType.getDeclaredFields()) {
String tmp = "<if test=\"_field != null\">_column=#{_field},</if>";
sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column",
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())));
}
sb.deleteCharAt(sb.lastIndexOf(","));
sb.append("</set>");
script = matcher.replaceAll(sb.toString());
script = "<script>"   script   "</script>";
}
return super.createSqlSource(configuration, script, parameterType);
}
}

3.3 自定義Insert Bean註解

同理,我們可以抽象化Insert操作,簡化後的Insert註解為

@Insert("INSERT INTO users (#{user})")
@Lang(SimpleInsertLangDriver.class)
void insertUserDAO(User user);

實現方式為:

public class SimpleInsertLangDriver extends XMLLanguageDriver implements LanguageDriver {
private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w )\\}\\)");
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
Matcher matcher = inPattern.matcher(script);
if (matcher.find()) {
StringBuilder sb = new StringBuilder();
StringBuilder tmp = new StringBuilder();
sb.append("(");
for (Field field : parameterType.getDeclaredFields()) {
sb.append(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())   ",");
tmp.append("#{"   field.getName()   "},");
}
sb.deleteCharAt(sb.lastIndexOf(","));
tmp.deleteCharAt(tmp.lastIndexOf(","));
sb.append(") values ("   tmp.toString()   ")");
script = matcher.replaceAll(sb.toString());
script = "<script>"   script   "</script>";
}
return super.createSqlSource(configuration, script, parameterType);
}
}

3.4 自定義Select註解

有的業務場景下,我們需要根據物件中的欄位進行查詢,就會寫出如下程式碼:

<select id="selectUser" resultType="com.lucifer.bean.User">
SELECT id,user_name,password,phone,address,email
FROM users
<where>
<if test="isDel != null">
AND is_del = #{isDel}
</if>
<if test="userName != null">
AND user_name = #{userName}
</if>
<if test="email != null">
AND email = #{email}
</if>
<if test="phone != null">
AND phone = #{phone}
</if>
</where>
</select>

和Update操作一樣,我們可以實現LanguageDriver將where子句抽象化,以此來簡化Select查詢語句。簡化後程式碼如下:


@Select("SELECT id,user_name,password,phone,address,email FROM users (#{user})")
@Lang(SimpleSelectLangDriver.class)
void selectUserDAO(User user);
public class SimpleSelectLangDriver extends XMLLanguageDriver implements LanguageDriver {
private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w )\\}\\)");
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
Matcher matcher = inPattern.matcher(script);
if (matcher.find()) {
StringBuilder sb = new StringBuilder();
sb.append("<where>");
for (Field field : parameterType.getDeclaredFields()) {
String tmp = "<if test=\"_field != null\">AND _column=#{_field}</if>";
sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column",
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())));
}
sb.append("</where>");
script = matcher.replaceAll(sb.toString());
script = "<script>"   script   "</script>";
}
return super.createSqlSource(configuration, script, parameterType);
}
}

4.排除多餘的變數

一個常見的情況是,可能會遇到實體類中的部分欄位在資料庫中並不存在相應的列,這就需要對多餘的不匹配的欄位進行邏輯隱藏;我們增加一個自定義的註解,並且對Language的實現稍作修改即可。
註解為:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Invisible {
}
public class User {
...
@Invisible
private List<String> userList;
...
}

然後在實現類中將被該註解宣告過的欄位排除

for (Field field : parameterType.getDeclaredFields()) {
if (!field.isAnnotationPresent(Invisible.class)) {  // 排除被Invisble修飾的變數
String tmp = "<if test=\"_field != null\">_column=#{_field},</if>";
sb.append(tmp.replaceAll("_field", field.getName()).replaceAll("_column",
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())));
}
}

5.注意事項&遇到的一些坑

  1. 務必確保資料庫中列名和實體類中欄位能一一對應
  2. 在使用自定義SQL解析器的時候,只能傳入一個引數,即相應的物件引數即可;傳入多個引數會導致解析器中獲得到的class物件改變,使得sql解析異常
  3. Update的實現能滿足大部分的業務,但有些業務場景可以會遇到根據查詢條件來更新查詢引數的情況,比如Update uesrs SET uesr_name = ‘tom’ WHERE user_name = ‘Jack’; 在這中場景的時候請不要使用自定義的SQL解析器
  4. 請使用Mybatis 3.3以上版本。3.2版本有bug,會另開一篇重新描述

6.總結

通過實現Language Driver,我們可以方便的自定義自己的註解。在遵循一些約定的情況下(資料庫下劃線命名,實體駝峰命名),我們可以大幅度的減少SQL的編寫量,並且可以完全的遮蔽掉麻煩的XML編寫方式,再也不用編寫複雜的動態SQL了有木有

// 簡潔的資料庫操作
@Select("SELECT * FROM users WHERE id IN (#{userIdList})")
@Lang(SimpleSelectInLangDriver.class)
List<User> selectUsersByUserId(List<Integer> userIdList);
@Insert("INSERT INTO users (#{user})")
@Lang(SimpleInsertLangDriver.class)
void insertUserDAO(User user);
@Update("UPDATE users (#{user}) WHERE id = #{id}")
@Lang(SimpleUpdateLangDriver.class)
void updateUsersById(User user);
@Select("SELECT id,user_name,password,phone,address,email FROM users (#{user})")
@Lang(SimpleSelectLangDriver.class)
void selectUserDAO(User user);

相關文章

程式語言 最新文章