<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
由於以前在專案中一直使用sqlmap.xml進行mybatis語句的編寫和實現,其xml實現動態更新和查詢較為方便,而目前由於技術框架所定,採用@Select、@Insert等註解方式來實現對應的持久化操作(MyBatis提供了簡單的Java註解,使得我們可以不設定XML格式的Mapper檔案,也能方便的編寫簡單的資料庫操作程式碼)
對於簡單的資料庫操作基本能夠滿足日常需要,但註解對動態SQL的支援一直差強人意,即使MyBatis提供了InsertProvider等*Provider註解來支援註解的Dynamic SQL,也沒有降低SQL的編寫難度,甚至比XML格式的SQL語句更難編寫和維護,實現較為複雜的語句時還是不那麼方便,團隊成員一直通過類來實現SQL語句的硬拼接
這樣的硬語句編寫的SQL語句很長又冗餘,給維護和修改帶來一定的成本且易讀性差,為了提高效率,一次編寫重複使用的原則,Mybatis在3.2版本之後,其實提供了LanguageDriver介面,就是便於使用該介面自定義SQL的解析方式。
故在這裡將研究的MyBatis如何在註解模式下簡化SQL語句的硬拼接實現動態組合版SQL的方案進行分享。
我們先來看下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); }
一旦實現了LanguageDriver,我們即可指定該實現類作為SQL的解析器,在不使用XML Mapper的形式下,我們可以使用@Lang註解
@Mapper public interface RoleDAO { /** * 查詢角色資訊列表 * * @param roleParam 查詢引數 * @return 角色列表 */ @Select("select id,name,description,enabled,deleted,date_created as dateCreated,last_modified as lastModified" + " from admin_role (#{roleParam})") @Lang(SimpleSelectLangDriver.class) List<RoleDO> findListRoleByPage(ListRoleParam roleParam);
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已經幫忙寫好了解析邏輯,而且發現如果是以開頭的字串傳入後,會被以XML的格式進行解析。那麼方案就可以確認了,我們繼承XMLLanguageDriver這個類,並且重寫其createSqlSource方法,按照自己編寫邏輯解析好sql後,再呼叫父類別的方法即可。
本例中將給出一些常見的自定義註解的實現和使用方式。
在本例中的業務場景下,我們需要根據物件中的欄位進行查詢,就會寫出硬SQL語句拼接類,如下程式碼:
/** * 查詢 * * @param userParam 查詢條件 * @return 使用者資訊列表 */ @SelectProvider(type = UserSql.class, method = "listByPage") List<UserDO> listByPage(@Param(value = "userParam") ListUserParam userParam);
public class UserSql { /** * 拼接查詢語句 * * @param params 查詢條件 * @return 查詢語句 */ public static String listByPage(Map params) { ListUserParam userParam = (ListUserParam)params.get("userParam"); if (userParam == null) { return ""; } long begin = (userParam.getPi() - 1) * userParam.getPs(); long end = userParam.getPi() * userParam.getPs(); String condition = ""; StringBuffer sb = new StringBuffer(" with query as ( "); sb.append( " select row_number() over(order by user.last_modified desc, user.date_created desc) as row_nr, user.* "); sb.append(" from ( "); sb.append(" select * from admin_user where 1=1 "); condition = " and username like '%#{userParam.username}%'"; sb.append(StringUtils.isBlank(userParam.getUsername()) ? "" : condition); condition = " and name like '%#{userParam.name}%'"; sb.append(StringUtils.isBlank(userParam.getName()) ? "" : condition); condition = " and mobile like '%#{userParam.mobile}%'"; sb.append(StringUtils.isBlank(userParam.getMobile()) ? "" : condition); condition = " and authorities like '%#{userParam.authorities}%'"; sb.append(StringUtils.isBlank(userParam.getAuthorities()) ? "" : condition); condition = " and enabled = #{userParam.enabled}"; sb.append(StringUtils.isBlank(userParam.getEnabled()) ? "" : condition); sb.append(" ) "); sb.append(" user) "); sb.append(" "); sb.append(" select "); sb.append(" id, username, password, name, mobile, authorities, enabled, deleted, "); sb.append(" creator_id as creatorId, creator, date_created as dateCreated, "); sb.append(" modifier_id as modifierId, modifier, last_modified as lastModified "); sb.append(" from query where row_nr > "); sb.append(begin); sb.append(" and row_nr <= "); sb.append(end); sb.append(" order by last_modified desc, date_created desc "); log.info("====UserSql.query====sb:{}", sb.toString()); return sb.toString(); } }
對於這樣硬拼接的SQL語句可讀性較差,也不利用日常維護,我們可以通過實現LanguageDriver將where子句抽象化,以此來簡化Select查詢語句。
簡化後程式碼如下( 在上述實現方案中已貼過程式碼):
/** * 查詢角色資訊列表 * * @param roleParam 查詢引數 * @return 角色列表 */ @Select("select id,name,description,enabled,deleted,date_created as dateCreated,last_modified as lastModified" + " from admin_role (#{roleParam})") @Lang(SimpleSelectLangDriver.class) List<RoleDO> findListRoleByPage(ListRoleParam roleParam);
其SimpleSelectLangDriver的實現程式碼如下:
package com.szss.admin.common; import java.lang.reflect.Field; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; import org.apache.ibatis.session.Configuration; import com.google.common.base.CaseFormat; /** * @author Allen * @date 2018/3/9 * * 自定義Select註解,用於動態生成Select語句 */ public class SimpleSelectLangDriver extends XMLLanguageDriver implements LanguageDriver { /** * Pattern靜態申明 */ private final Pattern inPattern = Pattern.compile("\(#\{(\w+)\}\)"); /** * 實現自定義Select註解 * @param configuration 設定引數 * @param script 入參 * @param parameterType 引數型別 * @return 轉換後的SqlSource */ @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); } }
上述程式碼實現了動態生成SQL語句的功能,但由於在VO實體類中可能有部分引數是我們不想加入到動態組合裡面的或部分欄位在資料庫中並不存在相應的列(比如自動 生成的serialVersionUID等其他欄位),這時我們就需要排除VO實體類的一些多餘的不匹配的欄位進行邏輯隱藏;我們增加一個自定義的註解,並且對Language的實現稍作修改即可。
新建一個註解,其程式碼如下:
package com.szss.admin.common; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author Allen * @date 2018/3/9 * * 自定義的註解,用於排除多餘的變數(自定義註解,過濾多餘欄位) */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Invisible { }
然後在VO實體類中不需要加入的欄位可進行參照該註解
package com.szss.admin.model.param; import com.szss.admin.common.Invisible; import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** * 角色查詢引數 * * @author Allen * @date 2018/3/8 */ @Data public class ListRoleParam { /** * 角色名稱 */ @ApiModelProperty(value = "角色名稱", example = "管理員", position = 1) private String name; /** * 是否啟用:0-不可用,1-可用 */ @ApiModelProperty(value = "是否啟用", example = "0", position = 2) private Boolean enabled; /** * 刪除標示:0-未刪除,1-已刪除 */ @ApiModelProperty(value = "刪除標示", example = "0", position = 3) private Boolean deleted; /** * 當前頁碼 */ @ApiModelProperty(value = "當前頁碼", example = "1", position = 4) @Invisible private long pi; /** * 當前頁面大小 */ @ApiModelProperty(value = "當前頁面大小", example = "10", position = 5) @Invisible private long ps; }
最後需要對上述中的SimpleSelectLangDriver實現類中將被該註解宣告過的欄位排除操作,程式碼如下:
for (Field field : parameterType.getDeclaredFields()) { // 排除被Invisble修飾的變數 if (!field.isAnnotationPresent(Invisible.class)) { 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()))); } }
如上所示,只是對SimpleSelectLangDriver類增加了if (!field.isAnnotationPresent(Invisible.class)) 這樣的判斷,已過濾多餘的變數。
需要注意的是在使用Select的時候,傳入的引數前無需加入@Param註解,否則會導致Mybatis找不到引數而丟擲異常,如需加入 就需要繫結物件屬性(如在語句中就需要使用param.name)。
在使用Mybatis註解的時候,發現其對Select In格式的查詢支援不是很友好,在字串中輸入十分繁瑣,可以通過將自定義的標籤轉成格式;下面便通過我們自己實現的LanguageDriver來實現SQL的動態解析:
DAO介面層中程式碼如下:
@Select("SELECT * FROM admin_role WHERE id IN (#{roleIdList})") @Lang(SimpleSelectInLangDriver.class) List<RoleDO> selectRolesByRoleId(List<Integer> roleIdList);
LanguageDriver實現類如下:
package com.szss.admin.common; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; import org.apache.ibatis.session.Configuration; /** * @author Allen * @date 2018/3/9 * * 自定義Select in 註解,用於動態生成Select in 語句 */ public class SimpleSelectInLangDriver extends XMLLanguageDriver implements LanguageDriver { /** * Pattern靜態申明 */ private final Pattern inPattern = Pattern.compile("\(#\{(\w+)\}\)"); /** * 實現自定義Select in 註解 * @param configuration 設定引數 * @param script 入參 * @param parameterType 引數型別 * @return 轉換後的SqlSource */ @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 admin_role " + "WHERE id IN " + "<foreach item='item' index='index' collection='list'open='(' separator=',' close=')'>" + "#{item}" + "</foreach>") List<RoleDO> selectRolesByRoleId(List<Integer> roleIdList);
通過實現LanguageDriver,剝離了冗長的動態拼接SQL語句,簡化了Select In的註解程式碼。
需要注意的是在使用Select In的時候,則與上述相反需務必在傳入的引數前加@Param註解,否則會導致Mybatis找不到引數而丟擲異常。
在擴充套件update註解時,資料庫每張表的欄位和實體類的欄位必須遵循一個約定(資料庫中採用下劃線命名法,實體類中採用駝峰命名法)。
當我們update的時候,會根據每個欄位的對映關係,寫出如下程式碼:
/** * 更新 * * @param roleDO 角色資訊 * @return 影響行數 */ @Update("update admin_role set role_name = #{roleDO.roleName}, " + " enabled = #{roleDO.enabled}, deleted = #{roleDO.deleted}, modifierId = #{roleDO.modifierId}," + " modifier = #{roleDO.modifier}, last_modified = #{roleDO.lastModified} where id = #{roleDO.id}") int update(@Param(value = "roleDO") RoleDO roleDO);
上述的程式碼我們可以將實體類中的駝峰式程式碼轉換為下劃線式命名方式,這樣就可以將這種對映規律自動化,但此程式碼存在一定的問題,就是當你在更新部分欄位時其餘所有欄位原來的值必須傳入,否則可能會將原有資料更新為null或空,亦或在更新時先查詢原資料後將變更的資料進行操作,這樣不僅增加了資料庫查詢操作且會造成程式碼冗餘,而經過實現LanguageDriver後,註解程式碼如下:
/** * 更新 * * @param roleParam 角色資訊 */ @Update("update admin_role (#{roleDO}) where id=#{id}") @Lang(SimpleUpdateLangDriver.class) void update(RoleParam roleParam);
相對於原始的程式碼量有很大的減少,尤其是對於一個類中欄位越多,改善也就越明顯。實現方式為:
package com.szss.admin.common; import java.lang.reflect.Field; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; import org.apache.ibatis.session.Configuration; import com.google.common.base.CaseFormat; /** * @author Allen * @date 2018/3/9 * * 自定義Update註解,用於動態生成Update語句 */ public class SimpleUpdateLangDriver extends XMLLanguageDriver implements LanguageDriver { /** * Pattern靜態申明 */ private final Pattern inPattern = Pattern.compile("\(#\{(\w+)\}\)"); /** * 實現自定義Update註解 * @param configuration 設定引數 * @param script 入參 * @param parameterType 引數型別 * @return 轉換後的SqlSource */ @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()) { // 排除被Invisble修飾的變數 if (!field.isAnnotationPresent(Invisible.class)) { 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); } }
注意此處在傳入的引數前無需加入@Param註解。
我們可以抽象化Insert操作,簡化後的Insert註解為:
/** * 插入 * * @param roleParam 角色資訊 */ @Insert("insert into admin_role (#{roleDO})") @Lang(SimpleInsertLangDriver.class) void insert(RoleParam roleParam);
SimpleInsertLanguageDriver實現類程式碼如下:
package com.szss.admin.common; import java.lang.reflect.Field; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; import org.apache.ibatis.session.Configuration; import com.google.common.base.CaseFormat; /** * @author Allen * @date 2018/3/9 * * 自定義Insert註解,用於動態生成Insert語句 */ public class SimpleInsertLangDriver extends XMLLanguageDriver implements LanguageDriver { /** * Pattern靜態申明 */ private final Pattern inPattern = Pattern.compile("\(#\{(\w+)\}\)"); /** * 實現自定義Insert註解 * @param configuration 設定引數 * @param script 入參 * @param parameterType 引數型別 * @return 轉換後的SqlSource */ @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()) { if (!field.isAnnotationPresent(Invisible.class)) { 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); } }
至此我們完成了基本的@Select、@Update、@Insert自定義註解,簡單化繁雜的拼接SQL語句的尷尬,但以上程式碼在SimpleSelectLangDriver中還有一定的侷限性,比如對於一些欄位我們需要使用like來進行查詢,這時就需要對上述自定義Seleect註解進行完善以實現各種業務的場景。
通過實現Language Driver,我們可以很方便的自定義自己的註解。在遵循一些約定的情況下(資料庫下劃線命名,實體駝峰命名),我們可以大幅度的減少SQL的編寫量,並且可以完全的遮蔽掉麻煩的XML編寫方式,再也不用再編寫複雜的拼接動態SQL的煩惱,簡化工作,提高開發效率。
//簡潔的資料庫操作 /** * 查詢角色資訊列表 * * @param roleParam 查詢引數 * @return 角色列表 */ @Select("select id,name,description,enabled,deleted,date_created as dateCreated,last_modified as lastModified" + " from admin_role (#{roleParam})") @Lang(SimpleSelectLangDriver.class) List<RoleDO> findListRoleByPage(ListRoleParam roleParam); /** * 插入 * * @param roleParam 角色資訊 */ @Insert("insert into admin_role (#{roleDO})") @Lang(SimpleInsertLangDriver.class) void insert(RoleParam roleParam); /** * 更新 * * @param roleParam 角色資訊 */ @Update("update admin_role (#{roleDO}) where id=#{id}") @Lang(SimpleUpdateLangDriver.class) void update(RoleParam roleParam);
通過@Lang註解以及自定義LanguageDriver類實現來簡化資料庫操作,不僅程式碼減少便於可讀的同時,還避免了在更新時需要獲取原資料的操作。
注:上述通過@Lang及實現LanguageDriver類的方法目前已基本不建議採用了,可採取Mybatis Plus來進行取代更為方便和快捷,具體可參加本部落格的另一篇文章<Spring Boot環境下Mybatis Plus的快速應用>
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45