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(); }
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; } }
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(); } }
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(); }
ok,以上就是mybatis-jpa的主要设计思路了,具体的细节,我已经尽可能的在代码中增加注释。
关于mybatis-jpa的代码构建使用方式,请参见博文:http://www.cnblogs.com/svili/p/6828077.html
由于个人能力有限,代码可能有些简陋,如有不妥之处,欢迎指正交流。