Mybatis JPA-集成方案+源碼


2018-04-18 update

當前文章已過時,請訪問代碼倉庫查看當前版本wiki。

github https://github.com/cnsvili/mybatis-jpa

gitee https://gitee.com/svili/mybatis-jpa

--------------------------------------

 

源碼地址(git):https://github.com/LittleNewbie/mybatis-jpa

一、Mybatis簡介

mybatis中文官方文檔:http://www.mybatis.org/mybatis-3/zh/index.html

簡介是為后面用到的內容做鋪墊,熟悉mybatis的朋友可以直接跳過,到第二章節。

關於mybatis-jpa的使用方式,請參見博文:http://www.cnblogs.com/svili/p/6828077.html

1.1 SqlSession

Mybatis中3個重要的概念:Configuration(容器),SqlSessionFactory(工廠),SqlSession;

相對於Spring中的applicationContext,BeanFactory,Bean。

不同之處在於SqlSession包含了所有的SQL方法,即這個SqlSession有且只有一個。SqlSession可以執行mybatis中注冊的所有方法。官方示例說明

<!-- SqlSession 完全包含了面向數據庫執行 SQL 命令所需的所有方法。
你可以通過 SqlSession 實例來直接執行已映射的 SQL 語句。例如:-->
SqlSession session = sqlSessionFactory.openSession();
try {
  Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
  session.close();
}

<!-- 映射器實例(Mapper Instances)-->
SqlSession session = sqlSessionFactory.openSession();
try {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // do work
} finally {
  session.close();
}

1.2 Namespace

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--  xml中命名空間與java中Mapper接口一致  -->
<mapper namespace="org.apache.ibatis.submitted.rounding.Mapper">

</mapper>

1.3 ResultType 和 ResultMap

ResultMap是myabtis最重要最強大的元素,是SQL列名columnName與POJO屬性名filedName的高級結果映射。對ResultMap不熟悉的朋友可以閱讀官方文檔了解。

ResultType可以理解為mybatis自動映射生成的簡單的ResultMap,當POJO中的filedName與數據庫ColumnName不一致時,無法完成映射。

1.4 javaType和typeHandler

ResultMap中使用

<resultMap id="userResultMap" type="User">
  <!--  主鍵   -->
  <id property="id" column="user_id" />
  <!--  當column類型與field類型不一致時,需指定javaType或typeHandler,二者選其一即可。   -->
  <!--  簡單的類型轉換只需指定javaType即可,需要邏輯處理的使用typeHandler  -->
  <result property="remark" column="remark" javaType="string"/>
  <result property="effective" column="is_effective" typeHandler="BooleanTypeHandler"/>
</resultMap>

1.5 jdbcType

官方說明:

jdbcType JDBC 類型是 僅需要對插入,更新和刪除操作可能為空的列進行處理。這是 JDBC jdbcType 的需要,而不是 MyBatis 的。

NOTE: 如果 null 被當作值來傳遞,對於所有可能為空的列,JDBC Type 是需要的。你可以自己通過閱讀預處理語句的 setNull() 方法的 JavaDocs 文檔來研究這種情況。java.sql.PreparedStatement.setNull()

示例:

<insert id="insertAuthor">
  insert into Author
    (id, username)
  values
    <!--  請注意,當sql中參數可能為null時,需要指定jdbcType,不然會出錯  -->
    ('1', #{username,jdbcType=VARCHAR})
</insert>

1.6 MappedStatement

Mapper中的方法(方法簽名和可執行的sql語句)會被封裝為MappedStatement注冊到Configuration中。

詳見mybatis源碼MapperBuilderAssistant.addMappedStatement(args);

二、Mybatis JPA

2.1 需求

1)首先,我們希望能夠與spring集成,使用spring的依賴注入。

2)其次,我們希望能夠兼容spring-mybatis集成的代碼,拒絕污染。

3)解析注冊ResultMap和MappedStatement。

2.2 主線

1)參考spring data jpa,使用@RepositoryDefinition注解,標記需要自動生成sql的dao。

我們使用@MapperDefinition和@StatementDefinition注解,標記需要自動生成sql的dao和method。

這個是關鍵,既保證了不污染原有代碼,又可以使用spring-mybatis已經實現的依賴注入。

我們只需要在此基礎上,對特定注解標注的mapper類和方法做處理即可。

2)參考spring-mybatis

MapperScannerConfigurer,掃描mapper並注冊到mybatis Configuration中,繼而生成代理類。

MapperAnnotationBuilder實現java注解生成ResultMap和MappedStatement。

2.3 入口-MapperEnhancerScaner

在spring容器初始化后,對@MapperDefinition標注的mapper類進行掃描。

2.4 解析POJO 生成ResultMap

重點:columnName與fieldName映射,特殊字段的jdbcType和typeHandler。

1)columnName與fieldName映射,使用JPA注解 @Cloumn即可,但是,我們希望能夠自動轉換駝峰與下划線風格,即對於符合規范命名的,不需要注解,直接映射。參見:PersistentUtil,ColumnNameUtil。

 /**
     * 將駝峰標識轉換為下划線
     * 
     * @param text
     * @return camel
     */
    public static String camelToUnderline(String text) {
        if (text == null || "".equals(text.trim())) {
            return "";
        }
        StringBuilder result = new StringBuilder(text.length() + 1);
        result.append(text.substring(0, 1));
        for (int i = 1; i < text.length(); i++) {
            if (!Character.isLowerCase(text.charAt(i))) {
                result.append('_');
            }
            result.append(text.substring(i, i + 1));
        }
        return result.toString().toLowerCase();
    }

    /**
     * 將下划線標識轉換為駝峰
     * 
     * @param text
     * @return underline
     */
    public static String underlineToCamel(String text) {
        if (text == null || "".equals(text.trim())) {
            return "";
        }
        int length = text.length();
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < length; i++) {
            char c = text.charAt(i);
            if (c == '_') {
                if (++i < length) {
                    result.append(Character.toUpperCase(text.charAt(i)));
                }
            } else {
                result.append(c);
            }
        }
        return result.toString();
    }        
ColunNameUtil

 

2)jdbcType和typeHandler

處理了以下3種類型:POJO中的Enum,Boolean,以及數據庫中的CLOB,代碼見MybatisColumnMeta。

需要強調說明的是,這里為所有的field都聲明了jdbcType,是為了規避sql中參數為null時,產生異常。

/** meta resolver */
    private static class ColumnMetaResolver {

        public static String resolveJdbcAlias(Field field) {

            Class<?> fieldType = field.getType();
            if (field.getType().isEnum()) {
                if (field.isAnnotationPresent(Enumerated.class)) {
                    // 獲取注解對象
                    Enumerated enumerated = field.getAnnotation(Enumerated.class);
                    // 設置了value屬性
                    if (enumerated.value() == EnumType.ORDINAL) {
                        return "INTEGER";
                    }
                }
                return "VARCHAR";
            }
            if (field.isAnnotationPresent(Lob.class)) {
                if (String.class.equals(fieldType)) {
                    return "CLOB";
                }
            }
            if (Integer.class.equals(fieldType)) {
                return "INTEGER";
            }
            if (Double.class.equals(fieldType)) {
                return "DOUBLE";
            }
            if (Float.class.equals(fieldType)) {
                return "FLOAT";
            }
            if (String.class.equals(fieldType)) {
                return "VARCHAR";
            }
            // date類型需聲明
            if (java.util.Date.class.isAssignableFrom(fieldType)) {
                return "TIMESTAMP";
            }
            return null;
        }

        public static JdbcType resolveJdbcType(String alias) {
            if (alias == null) {
                return null;
            }
            try {
                return JdbcType.valueOf(alias);
            } catch (IllegalArgumentException e) {
                throw new BuilderException("Error resolving JdbcType. Cause: " + e, e);
            }
        }

        @SuppressWarnings("unchecked")
        public static Class<? extends TypeHandler<?>> resolveTypeHandler(Field field) {
            Class<? extends TypeHandler<?>> typeHandlerClass = null;
            if (field.getType().isEnum()) {
                typeHandlerClass = (Class<? extends TypeHandler<?>>) EnumTypeHandler.class;
                if (field.isAnnotationPresent(Enumerated.class)) {
                    // 獲取注解對象
                    Enumerated enumerated = field.getAnnotation(Enumerated.class);
                    // 設置了value屬性
                    if (enumerated.value() == EnumType.ORDINAL) {
                        typeHandlerClass = (Class<? extends TypeHandler<?>>) EnumOrdinalTypeHandler.class;
                    }
                }
            }

            if (field.getType().equals(Boolean.class)) {
                typeHandlerClass = (Class<? extends TypeHandler<?>>) BooleanTypeHandler.class;
            }
            return typeHandlerClass;
        }
    }
ColumnMetaResolver

 

3)ResultMap注冊

見ResultMapAdapter.parseResultMap(args);

2.5 MappedStatement注冊

分類處理,select需要用到ResultMap,默認為Pojo.getSimpleName() + "ResultMap";

insert和insertSelective的區別:在於null值的處理,假設column_1在數據庫設置了默認值,而參數中的field_1為null值,則insert 在數據庫寫入null,而insertSelective寫入數據庫默認值.

需要特別說明的是,動態SQL需要使用"<script></script>"標簽包圍。

對於各種sql方法的語句生成方法,詳見com.mybatis.jpa.statement.builder包下的類。

這里以InsertSelective和select為例

public class InsertSelectiveBuilder implements StatementBuildable {

    @Override
    public String buildSQL(PersistentMeta persistentMeta, Method method) {
        // columns
        StringBuilder columns = new StringBuilder();
        columns.append("<trim prefix='(' suffix=')' suffixOverrides=',' > ");
        // values
        StringBuilder values = new StringBuilder();
        values.append("<trim prefix='(' suffix=')' suffixOverrides=',' > ");
        for (MybatisColumnMeta columnMeta : persistentMeta.getColumnMetaMap().values()) {
            // columns
            columns.append("<if test='" + columnMeta.getProperty() + "!= null'> ");
            columns.append(columnMeta.getColumnName() + ", ");
            columns.append("</if> ");
            // values
            values.append("<if test='" + columnMeta.getProperty() + "!= null'> ");
            values.append(SqlAssistant.resolveSqlParameter(columnMeta) + ", ");
            values.append("</if> ");
        }

        columns.append("</trim> ");
        values.append("</trim> ");

        return "<script>" + "INSERT INTO " + persistentMeta.getTableName() + columns.toString() + " VALUES "
                + values.toString() + "</script>";
    }

    @Override
    public void parseStatement(MybatisStatementAdapter adapter, PersistentMeta persistentMeta, Method method) {
        // 方法名
        adapter.setMethodName(method.getName());
        // 參數類型
        adapter.setParameterTypeClass(persistentMeta.getType());
        // sqlScript
        adapter.setSqlScript(buildSQL(persistentMeta, method));
        // 返回值類型
        adapter.setResultType(int.class);
        adapter.setResultMapId(null);

        adapter.setSqlCommandType(SqlCommandType.INSERT);

        // 主鍵策略
        adapter.setKeyGenerator(new NoKeyGenerator());
        adapter.setKeyProperty(null);
        adapter.setKeyColumn(null);

        adapter.parseStatement();

    }

}
InsertSelectiveBuilder

 

public class SelectBuilder implements StatementBuildable {

    @Override
    public String buildSQL(PersistentMeta persistentMeta, Method method) {
        return "SELECT " + persistentMeta.getColumnNames() + " FROM " + persistentMeta.getTableName()
                + SqlAssistant.buildSingleCondition(method, persistentMeta);
    }

    @Override
    public void parseStatement(MybatisStatementAdapter adapter, PersistentMeta persistentMeta, Method method) {
        // 方法名
        adapter.setMethodName(method.getName());
        // 參數類型
        if (method.getParameterTypes().length > 0) {
            // Mybatis mapper 方法最多支持一個參數,先設置成Object.class,mybatis會在sql中解析
            adapter.setParameterTypeClass(Object.class);
        } else {
            adapter.setParameterTypeClass(void.class);
        }

        String orderBy = " ";

        if (method.isAnnotationPresent(OrderBy.class)) {
            orderBy = " order by " + method.getAnnotation(OrderBy.class).value();
        }

        // sqlScript
        adapter.setSqlScript(buildSQL(persistentMeta, method) + orderBy);
        // 返回值類型
        adapter.setResultType(persistentMeta.getType());
        adapter.setResultMapId(persistentMeta.getType().getSimpleName() + "ResultMap");

        adapter.setSqlCommandType(SqlCommandType.SELECT);

        // 主鍵策略
        adapter.setKeyGenerator(new NoKeyGenerator());
        adapter.setKeyProperty(null);
        adapter.setKeyColumn(null);

        adapter.parseStatement();

    }
SelectBuilder

 

ok,以上就是mybatis-jpa的主要設計思路了,具體的細節,我已經盡可能的在代碼中增加注釋。

關於mybatis-jpa的代碼構建使用方式,請參見博文:http://www.cnblogs.com/svili/p/6828077.html

由於個人能力有限,代碼可能有些簡陋,如有不妥之處,歡迎指正交流。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM