Preface
JPA在国内的使用频率较小, 但也是一个值得学习的极为优秀的ORM框架, DDD的思想在里面体现得淋漓尽致.
结构图
配置
1 | spring: |
默认驼峰模式
Spring Data Jpa 使用的默认策略是 SpringPhysicalNamingStrategy
与 SpringImplicitNamingStrategy
, 就是驼峰模式的实现.
可以这样修改命名策略:
1 | #PhysicalNamingStrategyStandardImpl |
如果需要指定某个字段不使用驼峰模式可以直接使用@Column(name = "aaa")
基础CRUD操作
集成 JpaRepository<T, ID>
, T为实体, ID为实体id:
1 | public interface UserRepository extends JpaRepository<User, Long> { |
Controller:
1 |
|
JpaRepository
的默认实现类是 SimpleJpaRepository
, 可以看到提供了大部分通用的方法.
定义查询方法
方法的查询策略设置
通过下面的命令来配置方法的查询策略(在JpaRepositoriesAutoConfigureRegistrar
中已经自动配置, 实际Spring Boot项目中我们只需要引入JPA依赖即可, 不需要手动显示配置):
1 | (queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND) |
QueryLookupStrategy.Key
的值一共就三个:
Create
:直接根据方法名进行创建,规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则,启动的时候会报异常。USE_DECLARED_QUERY
:声明方式创建,即本书说的注解的方式。启动的时候会尝试找到一个声明的查询,如果没有找到将抛出一个异常,查询可以由某处注释或其他方法声明。CREATE_IF_NOT_FOUND
:这个是默认的,以上两种方式的结合版。先用声明方式进行查找,如果没有找到与方法相匹配的查询,那用 Create 的方法名创建规则创建一个查询。
查询方法的创建
Spring Data 中有一套自己的方法命名查询规范, 一般是前缀 find…By、read…By、query…By、count…By 和 get…By等, org.springframework.data.repository.query.parser.PartTree
:
Ex:
1 | interface PersonRepository extends Repository<User, Long> { |
使用的时候要配合不同的返回结果进行使用:
1 | interface UserRepository extends CrudRepository<User, Long> { |
##方法命名查询关键字列表
Keyword | Sample | JPQL snippet |
---|---|---|
And |
findByLastnameAndFirstname |
… where x.lastname = ?1 and x.firstname = ?2 |
Or |
findByLastnameOrFirstname |
… where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals |
findByFirstname ,findByFirstnameIs ,findByFirstnameEquals |
… where x.firstname = ?1 |
Between |
findByStartDateBetween |
… where x.startDate between ?1 and ?2 |
LessThan |
findByAgeLessThan |
… where x.age < ?1 |
LessThanEqual |
findByAgeLessThanEqual |
… where x.age <= ?1 |
GreaterThan |
findByAgeGreaterThan |
… where x.age > ?1 |
GreaterThanEqual |
findByAgeGreaterThanEqual |
… where x.age >= ?1 |
After |
findByStartDateAfter |
… where x.startDate > ?1 |
Before |
findByStartDateBefore |
… where x.startDate < ?1 |
IsNull |
findByAgeIsNull |
… where x.age is null |
IsNotNull,NotNull |
findByAge(Is)NotNull |
… where x.age not null |
Like |
findByFirstnameLike |
… where x.firstname like ?1 |
NotLike |
findByFirstnameNotLike |
… where x.firstname not like ?1 |
StartingWith |
findByFirstnameStartingWith |
… where x.firstname like ?1 (parameter bound with appended % ) |
EndingWith |
findByFirstnameEndingWith |
… where x.firstname like ?1 (parameter bound with prepended % ) |
Containing |
findByFirstnameContaining |
… where x.firstname like ?1 (parameter bound wrapped in % ) |
OrderBy |
findByAgeOrderByLastnameDesc |
… where x.age = ?1 order by x.lastname desc |
Not |
findByLastnameNot |
… where x.lastname <> ?1 |
In |
findByAgeIn(Collection<Age> ages) |
… where x.age in ?1 |
NotIn |
findByAgeNotIn(Collection<Age> ages) |
… where x.age not in ?1 |
True |
findByActiveTrue() |
… where x.active = true |
False |
findByActiveFalse() |
… where x.active = false |
IgnoreCase |
findByFirstnameIgnoreCase |
… where UPPER(x.firstame) = UPPER(?1) |
最全支持关键字可查看: org.springframework.data.repository.query.parser.Type
查询结果的处理
参数选择(Sort/Pageable)分页和排序
1 | Page<User> findByLastname(String lastname, Pageable pageable); |
限制查询结果
在查询方法上加限制查询结果的关键字 First 和 Top:
1 | User findFirstByOrderByLastnameAsc(); |
查询结果的不同形式(List/Stream/Page/Future)
1 | "select u from User u") ( |
关闭流:
1 | Stream<User> stream; |
异步结果:
1 |
|
支持的返回结果:
返回值类型 | 描述 |
---|---|
void |
不返回结果,一般是更新操作 |
Primitives |
Java 的基本类型,一般常见的是统计操作(如 long 、boolean 等)Wrapper types Java 的包装类 |
T |
最多只返回一个实体,没有查询结果时返回 null。如果超过了一个结果会抛出 IncorrectResultSizeDataAccessException 的异常。 |
Iterator |
一个迭代器 |
Collection |
集合 |
List |
List 及其任何子类 |
Optional |
返回 Java 8 或 Guava 中的 Optional 类。查询方法的返回结果最多只能有一个,如果超过了一个结果会抛出 IncorrectResultSizeDataAccessException 的异常 |
Option |
Scala 或者 javaslang 选项类型 |
Stream |
Java 8 Stream |
Future |
Future,查询方法需要带有 @Async 注解,并开启 Spring 异步执行方法的功能。一般配合多线程使用。关系数据库,实际工作很少有用到. |
CompletableFuture |
返回 Java8 中新引入的 CompletableFuture 类,查询方法需要带有 @Async 注解,并开启 Spring 异步执行方法的功能 |
ListenableFuture |
返回 org.springframework.util.concurrent.ListenableFuture 类,查询方法需要带有 @Async 注解,并开启 Spring 异步执行方法的功能 |
Slice |
返回指定大小的数据和是否还有可用数据的信息。需要方法带有 Pageable 类型的参数 |
Page |
在 Slice 的基础上附加返回分页总数等信息。需要方法带有 Pageable 类型的参数 |
GeoResult |
返回结果会附带诸如到相关地点距离等信息 |
GeoResults |
返回 GeoResult 的列表,并附带到相关地点平均距离等信息 |
GeoPage |
分页返回 GeoResult ,并附带到相关地点平均距离等信息 |
实现机制
通过 QueryExecutorMethodInterceptor
这个类的源代码,我们发现,该类实现了 MethodInterceptor 接口,也就是说它是一个方法调用的拦截器, 当一个 Repository 上的查询方法,譬如说 findByEmailAndLastname 方法被调用,Advice 拦截器会在方法真正的实现调用前,先执行这个 MethodInterceptor 的 invoke 方法。这样我们就有机会在真正方法实现执行前执行其他的代码了。
然而对于 QueryExecutorMethodInterceptor
来说,最重要的代码并不在 invoke 方法中,而是在它的构造器 QueryExecutorMethodInterceptor(RepositoryInformationr、Object customImplementation、Object target)
中。
最重要的一段代码是这段:
1 | for (Method method : queryMethods) { |
注解查询
@Query
1 | public Query { |
用法
1 | public interface UserRepository extends JpaRepository<User, Long>{ |
原生SQL:
1 | public interface UserRepository extends JpaRepository<User, Long> { |
注意: nativeQuery
不支持直接 Sort
的参数查询, 需要类似上面一样使用原生的order by
。
排序
@Query
的 JPQL 情况下,想实现排序,方法上面直接用 PageRequest
或者直接用 Sort
参数都可以做到。
在排序实例中实际使用的属性需要与实体模型里面的字段相匹配,这意味着它们需要解析为查询中使用的属性或别名。这是一个state_field_path_expression JPQL
定义,并且 Sort 的对象支持一些特定的函数。
1 | public interface UserRepository extends JpaRepository<User, Long> { |
分页
直接用 Page 对象接受接口,参数直接用 Pageable
的实现类即可。
1 | public interface UserRepository extends JpaRepository<User, Long> { |
对原生 SQL 的分页支持,案例如下,但是支持的不是特别友好,以 MySQL 为例。
1 | public interface UserRepository extends JpaRepository<UserInfoEntity, Integer>, JpaSpecificationExecutor<UserInfoEntity> { |
@Param
默认情况下,参数是通过顺序绑定在查询语句上的,这使得查询方法对参数位置的重构容易出错。为了解决这个问题,可以使用 @Param
注解指定方法参数的具体名称,通过绑定的参数名字做查询条件,这样不需要关心参数的顺序,推荐这种做法,比较利于代码重构。
1 | public interface UserRepository extends JpaRepository<User, Long> { |
根据参数进行查询,top 10 前面说的 query method 关键字照样有用,如下:
1 | public interface UserRepository extends JpaRepository<User, Long> { |
提醒:大家通过 @Query 定义自己的查询方法时,建议也用 Spring Data JPA 的 name query 的命名方法,这样下来风格就比较统一了。
Spel 表达式的支持
在 Spring Data JPA 1.4 以后,支持在 @Query
中使用 SpEL 表达式(简介)来接收变量。
SpEL 支持的变量
有两种方式能被解析出来:
- 如果定了
@Entity
注解,直接用其属性名。- 如果没定义,直接用实体的类的名称。
在以下的例子中,我们在查询语句中插入表达式:
1 | "User") ( |
这个 SPEL 的支持,比较适合自定义的 Repository,如果想写一个通用的 Repository 接口,那么可以用这个表达式来处理:
1 |
|
MappedTypeRepository
作为一个公用的父类,自己的 Repository 可以继承它,当调用 ConcreteRepository
执行 findAllByAttribute
方法的时候执行结果如下:
1 | select t from ConcreteType t where t.attribute = ?1 |
@Modifying 修改查询
可以通过在 @Modifying
注解实现只需要参数绑定的 update 查询的执行,我们来看个例子根据 lastName 更新 firstname 并且返回更新条数如下:
1 |
|
简单的针对某些特定属性的更新,也可以直接用基类里面提供的通用 save 来做更新(即继承 CrudRepository
接口)。
还有第三种方法就是自定义 Repository 使用 EntityManager 来进行更新操作。
对删除操作的支持如下:
1 | interface UserRepository extends Repository<User, Long> { |
所以现在我们一共有四种方式来做更新操作:
- 通过方法表达式;
- 还有一种就是
@Modifying
注解; @Query
注解也可以做到;- 继承
CrudRepository
接口。
@Query 的优缺点与实践
分类 | 描述 |
---|---|
优点 | (1)可以灵活快速的使用 JPQL 和 SQL |
(2)对返回的结果和字段记性自定义 | |
(3)支持连表查询和对象关联查询,可以组合出来复杂的 SQL 或者 JPQL | |
(4)可以很好的表达你的查询思路 | |
(5)灵活性非常强,快捷方便 | |
缺点 | (1)不支持动态查询条件,参数个数如果是不固定的不支持 |
(2)有些读者会将返回结果用 Map 或者 Object[] 数组接收结果,会导致调用此方法的开发人员不知道返回结果里面到底有些什么数据 | |
最佳实践 | (1)当出现很复杂的 SQL 或者 JPQL 的时候建议用视图 |
(2)返回结果一定要用对象接收,最好每个对象里面的字段和你返回的结果一一对应 | |
(3)动态的 Query Param 会在后面的章节中讲到 | |
(4)能用 JPQL 的就不要用 SQL |
实例中的常用注解
更多注解请查看
javax.persist
包.
@Entity
@Entity
用于定义对象将会成为被 JPA 管理的实体,将字段映射到指定的数据库表中
@Table
@Table
用于指定数据库的表名:
1 | public Table { |
@Id
@Id
定义属性为数据库的主键,一个实体里面必须有一个,并且必须和 @GeneratedValue
配合使用和成对出现.
@IdClass
@IdClass
利用外部类的联合主键。
@Basic & @Transient
@Basic
表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为 @Basic
。@Transient
表示该属性并非一个到数据库表的字段的映射,表示非持久化属性。JPA 映射数据库的时候忽略它,与 @Basic
相反的作用。
@Column
@Column
定义该属性对应数据库中的列名。
1 | public Column { |
@Temporal
@Temporal
用来设置 Date
类型的属性映射到对应精度的字段。
@Temporal(TemporalType.DATE)
映射为日期 // date (只有日期)@Temporal(TemporalType.TIME)
映射为日期 // time (是有时间)@Temporal(TemporalType.TIMESTAMP)
映射为日期 // date time (日期+时间)
@Enumerated
@Enumerated
这个注解很好用,直接映射 enum
枚举类型的字段。
1 | public Enumerated { |
@MappedSuperclass
@MappedSuperclass
注解使用在父类上面, 是用来标识父类的, @MappedSuperclass
标识的类表示其不能映射到数据库表,因为其不是一个完整的实体类,但是它所拥有的属性能够隐射在其子类对用的数据库表中.
@PrePersist… & @PostPersist…
@PrePersist
, @PreUpdate
, @PreRemove
, @PostLoad
, @PostPersist
, @PostRemove
, PostUpdate
: 如字面理解的都是更新前, 更新后等回调的方法.
1 |
|
这里可以配合Auditing实现一些审计功能, 参考AuditingEntityListener
:
1 |
|
1 |
|
@JoinColumn
@JoinColumn
主要配合 @OneToOne
、@ManyToOne
、@OneToMany
一起使用,单独使用没有意义, 用来定义多个字段的关联关系。
1 | public JoinColumn { |
@OneToOne
1 | public OneToOne { |
@OneToOne
需要配合 @JoinColumn
一起使用。注意:可以双向关联,也可以只配置一方,看实际需求。
案例:假设一个部门只有一个员工,Department 的内容如下:
1 |
|
注意:
employee_id
指的是 Department 里面的字段,而 referencedColumnName=”id” 指的是 Employee 表里面的字段。
如果需要双向关联,Employee 的内容如下:
1 | "employeeAttribute") (mappedBy= |
当然了也可以不选用 mappedBy 和下面效果是一样的:
1 |
|
@OneToMany & @ManyToOne
1 | public OneToMany { |
1 |
|
@ManyToMany & @JoinTable
1 | public ManyToMany { |
1 |
|
@SQLDelete&@Where
这两个注解可以配合完成逻辑删除
1 | "update user set delete_flag = 1 where id = ?") (sql = |
QueryByExampleExecutor基本用法
这个使用比较少
多种条件组合:
1 | //创建查询条件数据对象 |
查询 Null 值:
1 | //创建查询条件数据对象 |
JpaSpecificationExecutor使用
JpaSpecificationExecutor
是 Repository
要继承的接口,而 SimpleJpaRepository
是其默认实现:
1 | public interface JpaSpecificationExecutor<T> { |
这个接口基本是围绕着 Specification
接口来定义的:
1 | public interface Specification<T> { |
Criteria 的概念简单介绍:
(1)Root root
代表了可以查询和操作的实体对象的根,如果将实体对象比喻成表名,那 root 里面就是这张表里面的字段,这不过是 JPQL 的实体字段而已。通过里面的 Path get(String attributeName),来获得我们想操作的字段。
(2)CriteriaQuery query
代表一个 specific 的顶层查询对象,它包含着查询的各个部分,比如 select 、from、where、group by、order by 等。CriteriaQuery 对象只对实体类型或嵌入式类型的 Criteria 查询起作用,简单理解,它提供了查询 ROOT 的方法。常用的方法有:
1 | CriteriaQuery<T> where(Predicate... restrictions); |
(3)CriteriaBuilder cb
用来构建 CritiaQuery 的构建器对象,其实就相当于条件或者是条件组合,并以 Predicate 的形式返回。下面是构建简单的 Predicate 示例:
1 | Predicate p1=cb.like(root.get(“name”).as(String.class), “%”+uqm.getName()+“%”); |
构建组合的 Predicate 示例:
Predicate p = cb.and(p3,cb.or(p1,p2));
用法:
1 |
|
JPA Spec封装
1 | public final class SpecificationFactory { |
调用:
1 | userRepository.findAll( |
这样一来可读性以及代码优雅度都提高了.
推荐一个对Specification的封装库: https://github.com/wenhao/jpa-spec
EntityManager与自定义Repository
EntityManager的两种获取方式
获取EntityManager
有两种方式.
方式一: @PersistenceContext
1 |
|
方式二: 继承 SimpleJpaRepository
1 | public class BaseRepositoryCustom<T, ID> extends SimpleJpaRepository<T, ID> { |
自定义 Repository
自定义个别的特殊场景私有的 Repository
定义接口:
1 | public interface UserRepositoryCustom { |
实现接口:
1 | /** |
上面除了entityManager, 也可以使用JdbcTemplate来自己实现逻辑
继承接口:
1 | public interface UserRepository extends Repository<User, Long>,UserRepositoryCustom { |
然后直接调用就行了:
1 | userRepository.customerMethodNamesLike("jack"); |
我们还可以覆盖 JPA 里面的默认实现方法:
1 | //假设我们要覆盖默认的save方法的逻辑 |
实际工作中应用于逻辑删除场景:
在实际工作的生产环境中,我们可能经常会用到逻辑删除,所以做法是一般自定义覆盖 Data JPA 帮我们提供 remove 方法,然后实现逻辑删除的逻辑即可。
公用的通用的场景替代默认的 SimpleJpaRepository
声明定制共享行为的接口,用 @NoRepositoryBean
:
1 |
|
继承 SimpleJpaRepository 扩展自己的方法实现逻辑:
1 | public class MyRepositoryImpl<T, ID extends Serializable> |
使用 JavaConfig 配置自定义 MyRepositoryImpl 作为其他接口的动态代理的实现基类:
1 |
|
具有全局的性质,即使没有继承它所有的动态代理类也会变成它.
使用Tips
使用 @Embedded 关联一对一的值对象
可理解为DDD中的值对象
1 |
|
使用 @Convert 关联一对多的值对象
有时候在实体当中有某些字段是一个值对象的集合,我们又不想(也没必要)为其另起一张表,打个比方:订单里面的商品列表(只是打个比方,实际上应该是一张独立的表)。
例如设计一个访问日志对象,我们需要记录访问方法的行参与接收值:
1 |
|
属性转换器:
1 | //@Converter(autoApply = true) |
@Convert
声明使用某个属性转换器(ReqReceiveDataConverter
)ReqReceiveDataConverter
需要实现AttributeConverter<X,Y>
,X
为实体的字段类型,Y
对应需要持久化到DB的类型@Converter(autoApply = true)
注解作用,如果有多个实体需要用到此属性转换器,不需要每个实体都的字段加上@Convert
注解,自动对全部实体生效
发布领域事件
一般基于DDD的设计,在实体状态改变时(保存或更新实体),为了保证其他边缘服务与之状态的统一,我们需要通过发布实体保存或更新事件,其他服务监听后做出相应的处理,大概像这样:
1 |
|
通过JPA我们可以优雅地发布领域事件,有以下两种实现方式:
继承
AbstractAggregateRoot
,并使用其registerEvent()
方法注册发布事件1
2
3
4
5
6
7
8
9
10
11public class BankTransfer extends AbstractAggregateRoot {
...
public BankTransfer complete() {
id = UUID.randomUUID().toString();
registerEvent(new BankTransferCompletedEvent(id));
return this;
}
...
}1
2
3
4
5
6
7
8
9
10
11
12
public class BankTransferService {
...
public String completeTransfer(BankTransfer bankTransfer) {
return repository.save(bankTransfer.complete()).getId();
}
...
}但此方式拿不到实体id,因为是在生成id之前生成的event
使用
@DomainEvents
注解方法发布事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class MessageEvent implements Serializable {
private static final long serialVersionUID = -3843381578126175380L;
....
private transient List<Object> domainEvents = new ArrayList<>(16);
Collection<Object> domainEvents() {
log.info("publish domainEvents......");
domainEvents.add(new SaveMsgEvent().setId(this.id));
return Collections.unmodifiableList(domainEvents);
}
void callbackMethod() {
log.info("AfterDomainEventPublication..........");
domainEvents.clear();
}
}这种方式可以拿到实体id
监听:
1
2
3
4
5
6
7
8
9
10
11
4j
public class DomainEventListener {
.class) (SaveMsgEvent
public void processSaveMsgEvent(SaveMsgEvent saveMsgEvent) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(100);
log.info("Listening SaveMsgEvent..................saveMsgEvent id: {}", saveMsgEvent);
}
}用
@EventListener
也可以,但是@TransactionalEventListener
可以在事务之后执行。使用前者的话,程序异常事务会滚监听器照样会执行,而后者必须等事务正确提交之后才会执行。
踩坑
索引超长
1 | com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Specified key was too long; max key length is 1000 bytes |
如果设置了索引:
1 | @Table(indexes = {@Index(name = "idx_server_name", columnList = "serverName")}) |
上面注解指定了serverName
这一列为普通索引,如果此列不做限制,默认的长度是为255,默认的字符编码为utf8mb4
,最大字符长度为4字节,255 * 4 = 1020,所以超过了索引长度。
在MyISAM
表中,创建索引时,创建的索引长度不能超过1000bytes,在InnoDB
表中,创建索引时,索引的长度不成超过767byts 。
建立索引时,数据库计算key的长度是累加所有Index用到的字段的char长度后再按下面比例乘起来不能超过限定的key长度:
1 | latin1 = 1 byte = 1 character |
insert后update
使用AttributeConverter转换JSON字符串时,Hibernate执行insert之后再执行update
如上图,这是利用AOP实现的操作日志记录,使用AttributeConverter
与Fastjson实现ReqReceiveData
转换成JSON字符串,可以看到在执行insert之后接着执行了一次update,那是因为JSON字符串字段顺序居然发生了变化!
不过后来折腾一下把顺序统一了,但还是会出现这种问题,百思不得其解,一样的字符串Hibernate也会认为这是Dirty的数据?
百般折腾得以解决(但还是搞不懂原因):
value是Object类型,在set的时候调用JSONObject.toJSON(value)
转成Object再set进去…