先看下Repository相关的类图:
整体类图虽然咋看上去很庞杂,但其实主线脉络还是比较清晰的。
对主体repository层级提供的主要方法进行简单的梳理,如下:
下面对各个repository接口进行简单的独立介绍。
JpaRepository 与其前面的几个父类相比是个特殊的存在,其中补充添加了一组JPA规范的接口方法。前面的几个接口类都是Spring Data为了兼容NoSQL而进行的一些抽象封装(因为SpringData项目是一个庞大的家族,支持各种SQL与NoSQL的数据库,SpringData JPA是SpringData家族中面向SQL数据库的一个子分支项目),从 JpaRepository 开始是对关系型数据库进行抽象封装。
从类图可以看得出来它继承了 PagingAndSortingRepository 类,也就继承了其所有方法,并且实现类也是 SimpleJpaRepository 。从类图上还可以看出 JpaRepository 继承和拥有了 QueryByExampleExecutor 的相关方法。
通过源码和 CrudRepository 相比较,它支持Query By Example,批量删除,提高删除效率,手动刷新数据库的更改方法,并将默认实现的查询结果变成了List。
额外补充一句:
实际的项目编码中,大部分的场景中,我们自定义Repository都是继承 JpaRepository 来实现的。
先看个自定义Repository的例子,如下:
看下对应类图结构,自定义Repository继承了JpaRepository,具备了其父系所有的操作接口,此外,额外扩展了业务层面自定义的一些接口方法:
自定义Repository 的时候,继承JpaRepository需要传入两个泛型:
分页,排序使用 Pageable 对象进行传递,其中包含 Page 和 Sort 参数对象。
查询的时候,直接传递 Pageable 参数即可(注意下,如果是用原生SQL查询的方式,此法行不通,后文有详细说明)。
// 定义repository接口的时候,直接传入Pageable参数即可
List findAllByDepartment(DepartmentEntity department, Pageable pageable); 还有一种特殊的分页场景。比如,DB表中有100w条记录,然后现在需要将这些数据全量的加载到ES中。如果逐条查询然后插入ES,显然效率太慢;如果一次性全部查询出来然后直接往ES写,服务端内存可能会爆掉。
这种场景,其实可以基于 Slice 结果对象进行实现。Slice的作用是,只知道是否有下一个 Slice 可用,不会执行count,所以当查询较大的结果集时,只知道数据是足够的就可以了,而且相关的业务场景也不用关心一共有多少页。
private void fullLoadToEs(IESLoadService esLoadService) {
try {
final int batchHandleSize = 10000;
Pageable pageable = PageRequest.of(0, batchHandleSize);
do {
// 批量加载数据,返回Slice类型结果
Slice entitySilce = esLoadService.slicePageQueryData(pageable);
// 具体业务处理逻辑
List esDocumentData = esLoadService.buildEsDocumentData(entitySilce);
esUtil.batchSaveOrUpdateAsync(esDocumentData);
// 获取本次实际上加载到的具体数据量
int pageLoadedCount = entitySilce.getNumberOfElements();
if (!entitySilce.hasNext()) {
break;
}
// 自动重置page分页参数,继续拉取下一批数据
pageable = entitySilce.nextPageable();
} while (true);
} catch (Exception e) {
log.error("error occurred when load data into es", e);
}
} 按照条件进行搜索查询,是项目中遇到的非常典型且常用的场景。但是条件搜索也分几种场景,下面分开说下。
所谓简单固定,即查询条件就是固定的1个字段或者若干个字段,且查询字段数量不会变,比如根据部门查询具体人员列表这种。
这种情况,我们可以简单地直接在repository中,根据命名规范定义一个接口即可。
@Repository
public interface UserRepository extends JpaRepository {
// 根据一个固定字段查询
List findAllByDepartment(DepartmentEntity department);
// 根据多个固定字段组合查询
UserEntity findFirstByWorkIdAndUserNameAndDepartment(String workId, String userName, DepartmentEntity department);
} 考虑一种场景,界面上需要做一个用户搜索的能力,要求支持根据用户名、工号、部门、性别、年龄、职务等等若干个字段中的1个或者多个的组合来查询符合条件的用户信息。
显然,上述通过直接在repository中按照命名规则定义接口的方式行不通了。这个时候, Example 对象便派上用场了。
其实在前面整体介绍Repository的UML图中,就已经有了 Example 的身影了,虽然这个名字起得很敷衍,但其功能确是挺实在的。
看下具体用法:
public Page queryUsers(Request request, UserEntity queryParams) {
// 查询条件构造出对应Entity对象,转为Example查询条件
Example example = Example.of(queryParams);
// 构造分页参数
Pageable pageable = PageHelper.buildPageable(request);
// 按照条件查询,并分页返回结果
return userRepository.findAll(example, pageable);
} 如果是一些自定义的复杂查询场景,可以通过定制SQL语句的方式来实现。
@Repository
public interface UserRepository extends JpaRepository {
@Query(
value = "select t.*,(select group_concat(a.assigner_name) from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id) deal_person,"
+ " (select a.task_name from workflow_task a where a.state='R' and a.proc_inst_id=t.proc_inst_id limit 1) cur_step "
+ " from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) order by t.create_time desc",
countQuery = "select count(1) from workflow_info t where t.state='R' and t.type in (?1) "
+ "and exists(select 1 from workflow_task b where b.assigner=?2 and b.state='R' and b.proc_inst_id=t.proc_inst_id) ",
nativeQuery = true)
Page queryResource(List type, String workId, Pageable pageable);
} 此外,还可以基于 JpaSpecificationExecutor 提供的能力接口来实现。
自定义接口需要增加 JpaSpecificationExecutor 的继承,然后利用 Page
// 增加对JpaSpecificationExecutor的继承
@Repository
public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {
} public List queryUsers(QueryParams queryParams) {
// 构造Specification查询条件
Specification specification =
(root, query, cb) -> {
List predicates = new ArrayList<>();
// 范围查询条件构造
predicates.add(cb.greaterThanOrEqualTo(root.get("age"), queryParams.getMinAge()));
predicates.add(cb.lessThanOrEqualTo(root.get("age"), queryParams.getMaxAge()));
// 精确匹配查询条件构造
predicates.add(cb.equal(root.get("department"), queryParams.getDepartment()));
// 关键字模糊匹配条件构造
if (Objects.nonNull(queryParams.getNameKeyword())) {
predicates.add(cb.like(root.get("userName"), "%" + queryParams.getNameKeyword() + "%"));
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
// 执行复杂查询条件
return userRepository.findAll(specification);
} 实际项目中,经常会有一种场景,就是需要监听某个数据的变更然后做一些额外的处理逻辑。一种逻辑,是写操作的时候顺便调用下相关业务的处理API,这样会造成业务间耦合加深;优化点的策略是搞个MQ队列,然后在这个写DB操作的同时发个消息到MQ里面,然后一堆的consumer会监听MQ并去做对应的处理逻辑,这样引入个消息队列代价也有点高。
这个时候,我们可以借助JPA的自定义 EntityListener 功能来完美解决。通过监听某个Entity表的变更情况,通知或者调用相关其他的业务代码处理,完美实现了与主体业务逻辑的解耦,也无需引入其他组件。
举个例子:现有一个论坛发帖系统,发帖Post和评论Comment属于两个相对独立又有点关系的数据,现在需要检测当评论变化的时候,需要更新下Post对应记录的评论数字段。下面演示下具体实现。
public class CommentCountAuditListener {
/**
* 当Comment表有新增数据的操作时,触发此方法的调用
*/
@PostPersist
public void postPersist(CommentEntity entity) {
// 执行Post表中评论数字段的更新
// do something here...
}
/**
* 当Comment表有删除数据的操作时,触发此方法的调用
*/
@PostRemove
public void postRemove(CommentEntity entity) {
// 执行Post表中评论数字段的更新
// do something here...
}
/**
* 当Comment表有更新数据的操作时,触发此方法的调用
*/
@PostUpdate
public void postUpdate(CommentEntity entity) {
// 执行Post表中评论数字段的更新
// do something here...
}
}@Entity
@Table("t_comment")
// 指定前面定制的Listener
@EntityListeners({CommentCountAuditListener.class})
public class CommentEntity extends AbstractAuditable {
// ...
}这样就搞定了。
自定义Listener还有个典型的使用场景,就是可以统一的记录DB数据的操作日志。
JPA提供@Query注解,可以实现自定义SQL语句的能力。比如:
@Query(value = "select * from user " +
"where work_id in (?1) " +
"and department_id = 0 " +
"order by CREATE_TIME desc ",
nativeQuery = true)
List queryUsersByWorkIdIn(List workIds); 如果需要执行写操作SQL的时候,需要额外增加@Modifying注解标识,如下:
@Modifying
@Query(value = "insert into user (work_id, user_name) values (?1, ?2)",
nativeQuery = true)
int createUser(String workId, String userName);其中, nativeQuery = true 表示 @Query 注解中提供的value值为原生SQL语句。如果 nativeQuery 未设置或者设置为false,则表示将使用 JPQL 语言来执行。所谓JPQL,即JAVA持久化查询语句,是一种类似SQL的语法,不同点在于其使用类名来替代表名,使用类字段来替代表字段名。比如:
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
public UserInfo getUserInfoByName(String name);几个关注点要特别阐述下:
// like 需要手动添加百分号
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName like %?1")
public UserInfo getUserInfoByName(String name);// 错误示范: 自定义sql与API中Sort参数不可同时混用
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1", nativeQuery=true)
public UserInfo getUserInfoByName(String name, Sort sort);
// 正确示范: 自定义SQL完成对应sort操作
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
public UserInfo getUserInfoByName(String name, String sortColumn);// 正确:自定义jpql与API中Sort参数不可同时混用
@Query("SELECT u FROM com.vzn.demo.UserInfo u WHERE u.userName = ?1")
public UserInfo getUserInfoByName(String name, Sort sort);默认情况下,参数是通过顺序绑定在自定义执行语句上的,这样如果API接口传参顺序或者位置改变,极易引起自定义查询传参出问题,为了解决此问题,我们可以使用 @Param 注解来绑定一个具体的参数名称,然后以参数名称的形式替代位置顺序占位符,这也是比较推荐的一种做法。
// 默认的顺序位置传参
@Query("SELECT * FROM t_user u WHERE u.user_name = ?1 order by ?2", nativeQuery=true)
public UserInfo getUserInfoByName(String name, String sortColumn);
// 使用参数名称传参
@Query("SELECT * FROM t_user u WHERE u.user_name = :name order by :sortColumn", nativeQuery=true)
public UserInfo getUserInfoByName(@Param("name") String name, @Param("sortColumn") String sortColumn);一般而言,JAVA的编码规范都要求filed字段命名需要遵循小驼峰命名的规范,比如userName,而DB中column命名的时候,很多人习惯于使用下划线分隔的方式命名,比如 user_name 这种。这样就涉及到一个映射的策略问题,需要让JPA知道代码里面的userName就对应着DB中的 user_name 。
这里就会涉及到对命名映射策略的映射。主要有两种映射配置,下面分别阐述下。
配置项key值:
spring.jpa.hibernate.naming.implicit-strategy=xxxxx取值说明:
值 | 映射规则说明 |
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImp | 默认的命名策略,兼容JPA2.0规范 |
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl | 兼容老版本Hibernate的命名规范 |
org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl | 与ImplicitNamingStrategyJpaCompliantImp基本相同 |
org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl | 兼容JPA 1.0规范中的命名规范。 |
org.hibernate.boot.model.naming.SpringImplicitNamingStrategy | 继承ImplicitNamingStrategyJpaCompliantImpl,对外键、链表查询、索引如果未定义,都有下划线的处理策略,而table和column名字都默认与字段一样 |
配置项key值:
spring.jpa.hibernate.naming.physical-strategy=xxxxx取值说明:
值 | 映射规则说明 |
org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl | 默认字符串一致映射,不做任何转换处理,比如java类中userName,映射到table中列名也叫userName |
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy | java类中filed名称小写字母进行映射到DB表column名称,遇大写字母时转为分隔符"_"命名格式,比如java类中userName字段,映射到DB表column名称叫user_name |
SpringData JPA只是对JPA规范的二次封装,其底层使用的是 Hibernate ,所以此处涉及到Hibernate提供的一些处理策略。Hibernate将对象模型映射到关系数据库分为两个步骤:
这里, implicit-strategy 用于第一步隐式指定逻辑名称,而 physical-strategy 则用于第二步中逻辑名称到物理名称的映射。
注意:
当没有使用 @Table 和 @Column 注解时, implicit-strategy 配置项才会被使用,即 implicit-strategy 定义的是一种缺省场景的处理策略;而 physical-strategy 属于一种高优先级的策略,只要设置就会被执行,而不管是否有 @Table 和 @Column 注解。
好啦,本篇内容就介绍到这里。
通过本篇的内容,我们对于如何在项目中使用 Spring Data JPA 来进行一些较为复杂场景的处理方案与策略有了进一步的了解,再结合本系列此前的内容,到此掌握的JPA的相关技能已经足以应付大部分项目开发场景。
在实际项目中,为了保障数据操作的可靠、避免脏数据的产生,需要在代码中加入对数据库操作的事务控制。在下一篇文档中,我们将一起聊一聊Spring Data JPA业务代码开发中关于数据库事务的控制,以及编码中存在哪些可能会导致事务失效的场景等等。
| 留言与评论(共有 0 条评论) “” |