从Spring data jpa看Mybatis, 实现自己的JpaMapper
一、Spring data jpa
1.1 Spring data jpa概述
JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
Jpa是一个标准,就像JTA、JMS这种一样。Java Persistence API里面定义了一对注解,并没有实现。
JPA的实现框架有:Hibernate EntityManager(RedHat)、TopLink Essentials(Oracle/GlassFish,EJB 3.0中的JPA参考实现),Apache OpenJPA(BEA)、EclipseLink(http://www.eclipse.org/eclipselink/)、JDO等ORM框架。
在Spring家族里,有Spring data jpa(https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference)提供ORM,Spring Data JPA的默认实现是Hibernate,当然也可以是其他的JPA Provider。如Spring Data JPA 1.10支持Querydsl 4、Hibernate 5、OpenJPA 2.4 和 EclipseLink 2.6.1。
1.2 Jpa 与mybatis
所以,JPA跟mybatis没关系。 然而,Spring data jpa的风格却特别优雅,我们可以用mybatis去实现这种风格。
如: Spring对JPA实现的核心的API:
- Repository: 所有接口的父接口,而且是一个空接口,目的是为了统一所有Repository的类型,让组件扫描的时候能进行识。
- CrudRepository:是Repository的子接口,提供CRUD的功能。
- PagingAndSortingRepository:是CrudRepository的子接口,添加分页和排序的功能。
- JpaRepository:是PagingAndSortingRepository的子接口,增加了一些实用的功能,例如批量操作。
这些,我们完全可以用mybatis去实现。实现无sql的开发过程。
1.3 Spring data jpa实例
代码语言:javascript复制@Entity
@Table(name = "user_role")
public class UserRole {
@Id
private int id;
@Column(name = "user_name")
private String userName;
private String role;
}
public interface UserRoleDao extends CrudRepository<UserRole, Integer> {
List<UserRole> findByRole(String role);
@Query("select u from UserRole u where u.userName like ?1%")
List<UserRole> findByAndSort(String userName, Sort sort);
}
public interface UserRoleDao extends PagingAndSortingRepository<UserRole, Integer> {
Page<UserRole> findByUserName(String username, Pageable pageable);
}
二、Mybatis优雅编程
2.1 Mybatis原理
- Configuration: MyBatis 或者 MyBatisPlus全局配置对象。
- MappedStatement:一个 MappedStatement 对象对应 Mapper 配置文件中的一个。select/update/insert/delete 节点,主要描述的是一条 SQL 语句。
- SqlMethod : 枚举对象 ,MyBatisPlus支持的 SQL 方法。
- TableInfo:数据库表反射信息 ,可以获取到数据库表相关的信息。
- SqlSource: SQL 语句处理对象。
- MapperBuilderAssistant: 用于缓存、SQL 参数、查询方剂结果集处理等。通过 MapperBuilderAssistant 将每一个 mappedStatement 添加到onfiguration 中的 mappedstatements 中。
2.2 Mybatis代码生成工具概述
Mybatis的代码生成工具有很多: mybatis-generator、 easycode 等
这些工具是读取数据库表结构,然后按照模板生成entity、mapper、xml文件的。
如果不想下载插件,不想配置数据源,我们完全可以用js去自己生成code。 比如我自己做的一个:https://www.pomit.cn/java/project/sqlToMapper.html
生成代码,虽然提高了开发效率,但是不方便维护。 开发原则:约定大于配置,配置大于代码。所以有很多动态生成mybatis sqlsource的工具,如tk-mybatis、mybatis-plus。
2.3 Mybatis增强工具的原理
- tk-mybatis:框架自定义的MapperFactoryBean重写了checkDaoConfig()方法,完成对所有sql语句的设置
- mybatis-plus:继承MapperAnnotationBuilder,重写了MappedStatement。
Mybatis的MapperAnnotationBuilder是注解方式sql的处理器,其中parseStatement方法就是对Method上的注解进行解析,生成sqlsource并addMappedStatement,如果实现无sql的方式,只要有自己的parseStatement,并addMappedStatement即可。
那如何实现呢?
2.4 实现Spring data jpa风格的Mybatis
2.4.1 Mybatis的addMappedStatement
代码语言:javascript复制public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
这就是一个MappedStatement所需要的东西,虽然很多,但是不用担心,真正需要的就几个:
- SqlSource,就是sql
- SqlCommandType定义CRUD类型
- resultType返回值类型
- keyGenerator主键生成策略
其他的不是很重要。最重要的是SqlSource该怎么写呢?
languageDriver.createSqlSource(configuration, sql, parameterTypeClass);
这里使用configuration、注解的sql和参数一起创建了SqlSource。所以,思路来了,我们可以使用Java Persistence API的注解和反射去拼接SQL!
2.4.2 获取Mapper
同时,我们也可以通过手段获取到Mapper。
代码语言:javascript复制@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
MapperRegistry mapperRegistry = configuration.getMapperRegistry();
List<Class<?>> mappers = new ArrayList<>(mapperRegistry.getMappers());
// 可以记录下所有的mapper
}
// 此后可以扫描mapper的所有方法,可以按照方法是否存在注解去兼容mybatis常规写法和自动生成代码。
在Spring环境下,我们很容易获取到SqlSessionFactory,通过SqlSessionFactory,我们就可以获取到我们想要的东西: 这里,我们可以获取到Configuration和mappper。这两个很有用, 在MapperAnnotationBuilder中,会通过Configuration和mappper生成MapperBuilderAssistant,然后MapperBuilderAssistant调用addMappedStatement添加自定义MappedStatement。
2.4.3 定义实体
为了自动生成SQL,就需要我们根据实体去拼接SQL。那就解决两个问题:生成哪些方法的sql、实体从哪儿来。 当然有办法:
- 可以模仿Spring data jpa的CrudRepository和PagingAndSortingRepository,建立一个CrudMapper和PagingAndSortingMapper,加上泛型,泛型来作为实体,可以使用Java Persistence API的注解,如
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Id;
@Table(name = "f_auth_apply")
public class UserAuthApply {
@Id
@Column(name = "user_name")
private String userName;
@Column(name = "role_name")
private String roleName;
}
- CrudMapper和PagingAndSortingMapper定义常用的方法,如save、update、delete等,并实现findBy 字段名进行查询、deleteBy 字段名规则。
- 让获取到的Mapper,继承CrudMapper和PagingAndSortingMapper,这样我们就可以通过解析继承的接口,获取到泛型类:
Class<?> interfases[] = mapper.getInterfaces();
if (interfases == null || interfases.length < 1) {
return NO_MAPPER;
}
for (Class<?> interfase : interfases) {
if (ReflectUtil.checkTypeFit(interfase, JMapper.class)) {
if (interfase.equals(SimpleShardingMapper.class)) {
return SHARDING_MAPPER;
} else if (interfase.equals(PagingAndSortingMapper.class)) {
return PAGESORT_MAPPER;
} else {
return CRUD_MAPPER;
}
}
}
- 同时,可以获取到Mapper的方法:
Method[] methods = mapper.getMethods();
for (Method method : methods) {
if (method.getAnnotations() == null || method.getAnnotations().length < 1) {
//无注解,代表是需要被生成sql的方法,有注解无视即可,会自己走mybatis的逻辑。
}
}
- 解析出实体,那自然就能拿到实体上的注解。解析出方法,自然可以按照方法的格式去生成sql了。
2.4.4 实例
代码语言:javascript复制@Table(name="user_info")
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name="user_name")
private String userName;
@Column()
//@ShardingKey(prefix="_", methodPrecis="getTable", methodRange = "getTables")
private String mobile;
@Column()
private String name;
@Column()
private String passwd;
@Column()
private String valid;
}
代码语言:javascript复制import org.apache.ibatis.annotations.Mapper;
import com.github.ffch.boot.domain.UserInfo;
import com.github.ffch.jpamapper.core.mapper.CrudMapper;
@Mapper
public interface UserInfoDao extends CrudMapper<UserInfo, String> {
List<UserInfo> findByUserName(String userName);
int deleteByUserName(String userName);
@Select({
"<script>",
"SELECT ",
" user_name as userName,",
"role as role",
"FROM user_info",
"</script>"})
List<UserInfo> selectAll();
}
代码语言:javascript复制@Mapper
public interface UserInfoSortDao extends PagingAndSortingMapper<UserInfo, String>{
Page<UserInfo> pageByPasswd(String passwd, Pageable pageable);
List<UserInfo> sortByPasswd(String passwd, Sort sort);
}
2.5 可以做什么
这种方式仅仅可以固定的SQL么,非也。
- 可以实现findBy 字段名(And/OR)进行查询、deleteBy 字段名规则等,就是对检测到某正则匹配的方法名,where条件加上相应的字段名。
- 可以实现分页排序,对加上Pageable的方法,模拟
@Result(property = “userList”, javaType=List.class, many =@Many(select=“selectUsersByGroupId”), column = “group_id”)})
这种形式,对ResultType进行解析,隐式创建一个MappedStatement。 - 可以实现联表查询,可以对@JoinColumns和@JoinColumn的含义重写,同样类似分页去模拟一个@Many或者@One注解即可。
- 可以实现分表查询,例如指定某个字段为分表字段,制造SQL的时候使用
<bind name=”patternTable“ value=”@com.cff.springbootwork.sharding.jdbc.domain.ChatInfo@findTable(liveId)
, 这种形式可以根据方法动态生成表名。
下面是三年前写的一个小工具: https://www.pomit.cn/jpa-mapper/#/