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);
}
- createParameterHandler方法爲創建一個ParameterHandler對象,用於將實際參數賦值到JDBC語句中
- 將XML中讀入的語句解析並返回一個sqlSource對象
- 將註解中讀入的語句解析並返回一個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語句,其等同於:
通過實現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.注意事項&遇到的一些坑
- 務必確保數據庫中列名和實體類中字段能一一對應
- 在使用自定義SQL解析器的時候,只能傳入一個參數,即相應的對象參數即可;傳入多個參數會導致解析器中獲得到的class對象改變,使得sql解析異常
- Update的實現能滿足大部分的業務,但有些業務場景可以會遇到根據查詢條件來更新查詢參數的情況,比如Update uesrs SET uesr_name = ‘tom’ WHERE user_name = ‘Jack’; 在這中場景的時候請不要使用自定義的SQL解析器
- 請使用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);