【深刻淺出MyBatis系列八】SQL自動生成插件

#0 系列目錄#java

本文提供了一種自動生成sql語句的方法,它針對的對象是有主鍵或惟一索引的單表,提供的操做有增、刪、改、查4種。理解本文和本文的提供的代碼須要有java註解的知識,由於本文是基於註解生成sql的。sql

#1 準備# ##1.1 爲何在StatementHandler攔截## 在SQL執行流程分析(源碼篇)章節介紹了一次sqlsession的完整執行過程,從中能夠知道sql的解析是在StatementHandler裏完成的,因此爲了重寫sql須要攔截StatementHandler。數據庫

##1.2 MetaObject簡介## 在實現裏大量使用了MetaObject這個對象,所以有必要先介紹下它。MetaObject是Mybatis提供的一個的工具類,經過它包裝一個對象後能夠獲取或設置該對象的本來不可訪問的屬性(好比那些私有屬性)。它有個三個重要方法常常用到:緩存

MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) 用於包裝對象;session

Object getValue(String name) 用於獲取屬性的值(支持OGNL的方法);app

void setValue(String name, Object value) 用於設置屬性的值(支持OGNL的方法);ide

#2 攔截器簽名#工具

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})  
public class AutoMapperInterceptor implements Interceptor {  
    ...  
}

從簽名裏能夠看出,要攔截的目標類型是StatementHandler(注意:type只能配置成接口類型),攔截的方法是名稱爲prepare參數爲Connection類型的方法。源碼分析

#3 intercept實現#ui

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})  
public class AutoMapperInterceptor implements Interceptor {  
    private static final Log logger = LogFactory.getLog(AutoMapperInterceptor.class);  
    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();  
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();  
  
    @Override  
    public Object intercept(Invocation invocation) throws Throwable {  
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();  
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,  
                DEFAULT_OBJECT_WRAPPER_FACTORY);  
        // 分離代理對象鏈  
        while (metaStatementHandler.hasGetter("h")) {  
            Object object = metaStatementHandler.getValue("h");  
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);  
        }  
        // 分離最後一個代理對象的目標類  
        while (metaStatementHandler.hasGetter("target")) {  
            Object object = metaStatementHandler.getValue("target");  
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);  
        }  
        String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");  
        Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");  
        Object parameterObject = metaStatementHandler.getValue("delegate.boundSql.parameterObject");  
        if (null == originalSql || "".equals(originalSql)) {  
            String newSql = "";  
            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler  
                    .getValue("delegate.mappedStatement");  
            // 根據ID生成相應類型的sql語句(id需剔除namespace信息)  
            String id = mappedStatement.getId();  
            id = id.substring(id.lastIndexOf(".") + 1);  
            if ("insert".equals(id)) {  
                newSql = SqlBuilder.buildInsertSql(parameterObject);  
            } else if ("update".equals(id)) {  
                newSql = SqlBuilder.buildUpdateSql(parameterObject);  
            } else if ("delete".equals(id)) {  
                newSql = SqlBuilder.buildDeleteSql(parameterObject);  
            } else if ("select".equals(id)) {  
                newSql = SqlBuilder.buildSelectSql(parameterObject);  
            }  
            logger.debug("Auto generated sql:" + newSql);  
            //  
            SqlSource sqlSource = buildSqlSource(configuration, newSql, parameterObject.getClass());  
            List<ParameterMapping> parameterMappings = sqlSource.getBoundSql(parameterObject).getParameterMappings();  
            metaStatementHandler.setValue("delegate.boundSql.sql", sqlSource.getBoundSql(parameterObject).getSql());  
            metaStatementHandler.setValue("delegate.boundSql.parameterMappings", parameterMappings);  
        }  
        // 調用原始statementHandler的prepare方法  
        statementHandler = (StatementHandler) metaStatementHandler.getOriginalObject();  
        statementHandler.prepare((Connection) invocation.getArgs()[0]);  
        // 傳遞給下一個攔截器處理  
        return invocation.proceed();  
    }  
  
    @Override  
    public Object plugin(Object target) {  
        if (target instanceof StatementHandler) {  
            return Plugin.wrap(target, this);  
        } else {  
            return target;  
        }  
    }  
  
    @Override  
    public void setProperties(Properties properties) {  
  
    }  
  
    private SqlSource buildSqlSource(Configuration configuration, String originalSql,   
    Class<?> parameterType) {  
        SqlSourceBuilder builder = new SqlSourceBuilder(configuration);  
        return builder.parse(originalSql, parameterType, null);  
    }  
}

StatementHandler的默認實現類是RoutingStatementHandler,所以攔截的實際對象是它。RoutingStatementHandler的主要功能是分發,它根據配置Statement類型建立真正執行數據庫操做的StatementHandler,並將其保存到delegate屬性裏。因爲delegate是一個私有屬性而且沒有提供訪問它的方法,所以須要藉助MetaObject的幫忙。經過MetaObject的封裝後咱們能夠輕易的得到想要的屬性。

在上面的方法裏有個兩個循環,經過他們能夠分離出原始的RoutingStatementHandler(而不是代理對象)。

有了插件幫你生成sql語句後,mapper配置文件裏單表的增刪改查部分就不須要再配置sql代碼了,但因爲插件須要經過id來生成不一樣類型的sql語句,所以必要的配置仍是須要的,並且相應的id必須是下面的這幾個(區分大小寫):

<update id="update" parameterType="UserDto"></update>  
<insert id="insert" parameterType="UserDto"></insert>  
<delete id="delete" parameterType="UserDto"></delete>  
<select id="select" parameterType="UserDto" resultType="UserDto""></select>

#3 SqlBuilder# SqlBuilder的相應方法接受一個dto對象做爲參數,它們根據這個對象的屬性值和配置的註解生成相應的sql。

@TableMapperAnnotation(tableName = "t_user", uniqueKeyType = UniqueKeyType.Single, uniqueKey = " userid ")  
public class UserDto {  
    @FieldMapperAnnotation(dbFieldName = "userid", jdbcType = JdbcType.INTEGER)  
    private Integer userid;  
    @FieldMapperAnnotation(dbFieldName = "username", jdbcType = JdbcType.VARCHAR)  
    private String username;  
    ...  
}

這個對象包含了兩種註解,一個是TableMapperAnnotation註解,它保存了表名、惟一鍵類型和構成惟一鍵的字段;另外一個是FieldMapperAnnotation註解,它保存了數據庫字段名和字段類型信息。這兩個註解都是必須的。SqlBuilder生成sql時會用到他們,下面以生成insert語句的方法爲例,其餘方法相似:

public static String buildInsertSql(Object object) throws Exception {  
    if (null == object) {  
        throw new RuntimeException("Sorry,I refuse to build sql for a null object!");  
    }  
    Map dtoFieldMap = PropertyUtils.describe(object);  
    // 從參數對象裏提取註解信息  
    TableMapper tableMapper = buildTableMapper(object.getClass());  
    // 從表註解裏獲取表名等信息  
    TableMapperAnnotation tma = (TableMapperAnnotation) tableMapper.getTableMapperAnnotation();  
    String tableName = tma.tableName();  
    StringBuffer tableSql = new StringBuffer();  
    StringBuffer valueSql = new StringBuffer();  
  
    tableSql.append("insert into ").append(tableName).append("(");  
    valueSql.append("values(");  
  
    boolean allFieldNull = true;  
    // 根據字段註解和屬性值聯合生成sql語句  
    for (String dbFieldName : tableMapper.getFieldMapperCache().keySet()) {  
        FieldMapper fieldMapper = tableMapper.getFieldMapperCache().get(dbFieldName);  
        String fieldName = fieldMapper.getFieldName();  
        Object value = dtoFieldMap.get(fieldName);  
        // 因爲要根據字段對象值是否爲空來判斷是否將字段加入到sql語句中,所以DTO對象的屬性不能是簡單類型,反而必須是封裝類型  
        if (value == null) {  
            continue;  
        }  
        allFieldNull = false;  
        tableSql.append(dbFieldName).append(",");  
        valueSql.append("#{").append(fieldName).append(",").append("jdbcType=")  
                .append(fieldMapper.getJdbcType().toString()).append("},");  
    }  
    if (allFieldNull) {  
        throw new RuntimeException("Are you joking? Object " + object.getClass().getName()  
                + "'s all fields are null, how can i build sql for it?!");  
    }  
    tableSql.delete(tableSql.lastIndexOf(","), tableSql.lastIndexOf(",") + 1);  
    valueSql.delete(valueSql.lastIndexOf(","), valueSql.lastIndexOf(",") + 1);  
    return tableSql.append(") ").append(valueSql).append(")").toString();  
}

#4 plugin實現#

public Object plugin(Object target) {  
    // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標自己,減小目標被代理的  
    // 次數  
    if (target instanceof StatementHandler) {  
        return Plugin.wrap(target, this);  
    } else {  
        return target;  
    }  
}
相關文章
相關標籤/搜索