MyBatis實現@Select等註解動態組合SQL語句

目錄

實現方案

自定義Select註解

自定義Select in註解

自定義Update的註解

自定義Insert的註解

注意事項

總結


     使用SqlMapper.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接口中的方法:

public interface LanguageDriver {

//createParameterHandler方法爲創建ParameterHandler對象,用於將實際參數複製到JDBC語句中
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

//將XML中讀入的語句解析並返回一個sqlSource對象
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

//將註解中讀入的語句解析並返回一個sqlSource對象
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

}

一旦實現了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後,再調用父類的方法即可。

自定義Select註解

在本例中的業務場景下,我們需要根據對象中的字段進行查詢,就會寫出硬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 = params.get("userParam");
		if(userParam == null){
			return "";
		}
		long begin=(userParam.getPi()-1)*userParam.getPs();
		long end=userParam.getPi().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;
 
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;


/**

* 自定義的註解,用於排除多餘的變量(自定義註解,過濾多餘字段)

*/

@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;

/**
* 角色查詢參數
*/
@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)。

自定義Select in註解

在使用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;


/**


* 自定義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註解時,數據庫每張表的字段和實體類的字段必須遵循一個約定(數據庫中採用下劃線命名法,實體類中採用駝峯命名法)。當我們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;


/**

* 自定義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操作,簡化後的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;


/**


* 自定義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註解進行完善以實現各種業務的場景。

注意事項

  1. 務必確保數據庫中列名和實體類中字段能一一對應。
  2. 在使用自定義SQL解析器的時候,只能傳入一個參數,即相應的對象參數即可;傳入多個參數會導致解析器中獲得到的class對象改變,使得sql解析異常。
  3. Update的實現能滿足大部分的業務,但有些業務場景可以會遇到根據查詢條件來更新查詢參數的情況,比如Update user SET uesr_name = ‘王雷’  WHERE user_name = ‘小王’; 在這樣的場景中請不要使用自定義的SQL解析器。
  4. 請使用Mybatis 3.3以上版本。3.2以下版本會存在一些Bug,在本例中使用的爲mybatis-spring-boot-starter1.3.1,其Mybatis爲3.4.5。

總結

通過實現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來進行取代更爲方便和快捷。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章