JPA(七)-原理
参考文献
- 拉钩教育–Spring Data JPA原理与实战
JpaProperties
属性
1 | # 可以配置JPA的实现者的原始属性的配置,如:这里我们用的JPA的实现者是hibernate |
Debug 时日志的配置
1 | ### 日志级别的灵活运用 |
@PersistenceUnit @PersistenceContext
-
EntityManagerFactory 和 Persistence Unit 是什么?
- 按照 JPA 协议里面的定义:persistence unit 是一些持久化配置的集合,里面包含了数据源的配置、EntityManagerFactory 的配置,spring 3.1 之前主要是通过 persistence.xml 的方式来配置一个 persistence unit。
-
EntityManager 和 PersistenceContext 是什么?
- 按照 JPA 协议的规范,我们先理解一下 PersistenceContext,它是用来管理会话里面的 Entity 状态的一个上下文环境,使 Entity 的实例有了不同的状态,也就是我们所说的实体实例的生命周期。
- 而这些实体在 PersistenceContext 中的不同状态都是通过 EntityManager 提供的一些方法进行管理的,也就是说:
- PersistenceContext 是持久化上下文,是 JPA 协议定义的,而 Hibernate 的实现是通过 Session 创建和销毁的,也就是说一个 Session 有且仅有一个 PersistenceContext;
- PersistenceContext 既然是持久化上下文,里面管理的是 Entity 的状态;
- EntityManager 是通过 PersistenceContext 创建的,用来管理 PersistenceContext 中 Entity 状态的方法,离开PersistenceContext 持久化上下文,EntityManager 没有意义;
- EntityManger 是操作对象的唯一入口,一个请求里面可能会有多个 EntityManger 对象。
实体对象的生命周期
-
既然 PersistenceContext 是存储 Entity 的,那么 Entity 在 PersistenceContext 里面肯定有不同的状态。对此,JPA 协议定义了四种状态:new、manager、detached、removed。
第一种:New 状态的对象
-
当我们使用关键字 new 的时候创建的实体对象,称为 new 状态的 Entity 对象。它需要同时满足两个条件:new 状态的实体 Id 和 Version 字段都是 null;new 状态的实体没有在 PersistenceContext 中出现过。
-
那么如果我们要把 new 状态的 Entity 放到 PersistenceContext 里面,有两种方法:执行 entityManager.persist(entity) 方法;通过关联关系的实体关系配置 cascade=PERSIST or cascade=ALL 这种类型,并且关联关系的一方,也执行了 entityManager.persist(entity) 方法。
第二种:Detached(游离)的实体对象
-
Detached 状态的对象表示和 PersistenceContext 脱离关系的 Entity 对象。它和 new 状态的对象的不同点在于:
-
Detached 是 new 状态的实体对象没有持久化 ID(即没有 ID 和 version);
-
变成持久化对象需要进行 merger 操作,merger 操作会 copy 一个新的实体对象,然后把新的实体对象变成 Manager 状态。
-
-
而 Detached 和 new 状态的对象相同点也有两个方面:
- 都和 PersistenceContext 脱离了关系;
-
当执行 flush 操作或者 commit 操作的时候,不会进行数据库同步。
第三种:Manager(persist) 状态的实体
- Manager 状态的实体,顾名思义,是指在 PersistenceContext 里面管理的实体,而此种状态的实体当我们执行事务的 commit(),或者 entityManager 的 flush 方法的时候,就会进行数据库的同步操作。可以说是和数据库的数据有映射关系。
第四种:Removed 的实体状态
-
Removed 的状态,顾名思义就是指删除了的实体,但是此实体还在 PersistenceContext 里面,只是在其中表示为 Removed 的状态,它和 Detached 状态的实体最主要的区别就是不在 PersistenceContext 里面,但都有 ID 属性。
-
MyBatis 是对数据库的操作所见即所得的模式;而使用 JPA,你的任何操作都不会产生 DB 的sql。
Flush 的作用
-
flush 重要的、唯一的作用,就是将 Persistence Context 中变化的实体转化成 sql 语句,同步执行到数据库里面。换句话来说,如果我们不执行 flush() 方法的话,通过 EntityManager 操作的任何 Entity 过程都不会同步到数据库里面。
-
Flush 的机制是什么?
- JPA 协议规定了 EntityManager 可以通过如下方法修改 FlushMode。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//entity manager 里面提供的修改FlushMode的方法
public void setFlushMode(FlushModeType flushMode);
//FlushModeType只有两个值,自动和事务提交之前
public enum FlushModeType {
//事务commit之前
COMMIT,
//自动规则,默认
AUTO
}
而 Hiberbernate 还提供了一种手动触发的机制,可以通过如下代码的方式进行修改。
private EntityManager entityManager; -
Flush 的自动机制
- 事务 commit 之前,即指执行 transactionManager.commit() 之前都会触发,这个很好理解;
- 执行任何的 JPQL 或者 native SQL(代替直接操作 Entity 的方法)都会触发 flush。
-
Flush 的时候会改变 SQL 的执行顺序
- flush() 方法调用之后,同一个事务内,sql 的执行顺序会变成如下模式:insert 的先执行、delete 的第二个执行、update 的第三个执行。
- 这种会改变顺序的现象,主要是由 persistence context 的实体状态机制导致的,所以在 Hibernate 的环境中,顺序会变成如下的 ActionQueue 的模式:
OrphanRemovalAction
EntityInsertAction
orEntityIdentityInsertAction
EntityUpdateAction
CollectionRemoveAction
CollectionUpdateAction
CollectionRecreateAction
EntityDeleteAction
-
Flush 与事务 Commit 的关系
- 在当前的事务执行 commit 的时候,会触发 flush 方法;
- 在当前的事务执行完 commit 的时候,如果隔离级别是可重复读的话,flush 之后执行的 update、insert、delete 的操作,会被其他的新事务看到最新结果;
- 假设当前的事务是可重复读的,当我们手动执行 flush 方法之后,没有执行事务 commit 方法,那么其他事务是看不到最新值变化的,但是最新值变化对当前没有 commit 的事务是有效的;
- 如果执行了 flush 之后,当前事务发生了 rollback 操作,那么数据将会被回滚(数据库的机制)
Session、EntityManager、Connection 和 Transaction 的关系
- Connection 和 Transaction 的关系
- 事务是建立在 Connection 之上的,没有连接就没有事务。
- 以 MySQL InnoDB 为例,新开一个连接默认开启事务,默认每个 SQL 执行完之后自动提交事务。
- 一个连接里面可以有多次串行的事务段;一个事务只能属于一个 Connection。
- 事务与事务之间是相互隔离的,那么自然不同连接的不同事务也是隔离的。
- EntityManager、Connection 和 Transaction 的关系
- EntityManager 里面有 DataSource,当 EntityManager 里面开启事务的时候,先判断当前线程里面是否有数据库连接,如果有直接用。
- 开启事务之前先开启连接;关闭事务,不一定关闭连接。
- 开启 EntityManager,不一定立马获得连接;获得连接,不一定立马开启事务。
- 关闭 EntityManager,一定关闭事务,释放连接;反之不然。
- Session、EntityManager、Connection 和 Transaction 的关系
- Session 是 EntityManager 的子类,SessionImpl 是 Session 和 EntityManager 的实现类。那么自然 EntityManager 和 Connection、Transaction 的关系同样适用 Session、EntityManager、Connection 和 Transaction 的关系。
- Session 的生命周期决定了 EntityManager 的生命周期。
- Session 和 Transaction 的关系
- 在 Hibernate 的 JPA 实现里面,开启 Transaction 之前,必须要先开启 Session。
- 默认情况下,Session 的生命周期由 open-in-view 决定是请求之前开启,还是事务之前开启。
- 事务关闭了,Session 不一定关闭。
- Session 关闭了,事务一定关闭。
N+1SQL问题
1 | //UserInfo实体对象如下: |
-
解决方法一:
-
hibernate.default_batch_fetch_size 配置在 AvailableSettings.class 里面,指的是批量获取数据的大小,默认是 -1,表示默认没有匹配取数据。
1
2
3
4
5
6# 更改批量取数据的大小为20
spring.jpa.properties.hibernate.default_batch_fetch_size= 20
在实际工作中,一定要知道我们一次操作会产生多少 SQL,有没有预期之外的 SQL 参数,这是需要关注的重点,这种情况可以利用我们之前说过的如下配置来开启打印 SQL
## 显示sql的执行日志,如果开了这个,show_sql就可以不用了,show_sql没有上下文,多线程情况下,分不清楚是谁打印的,所有我推荐如下配置项:
logging.level.org.hibernate.SQL=debug -
但是这种配置也有个缺陷,就是只能全局配置,没办法针对不通过的实体管理关系配置不同的 Fetch Size 的值。
-
-
解决方法二:
- @BatchSize 注解是 Hibernate 提供的用来解决查询关联关系的批量处理大小,默认无,可以配置在实体上,也可以配置在关联关系上面。此注解里面只有一个属性 size,用来指定关联关系 LAZY 或者是 EAGER 一次性取数据的大小。
- @BatchSize 只能作用在 @ManyToMany、@OneToMany、实体类这三个地方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//实体类上加@BatchSize注解,用来设置当被关联关系的时候一次查询的大小,我们设置成2,方便演示Address关联UserInfo的时候的效果
public class UserInfo extends BaseEntity {
private String name;
private String telephone;
//关联关系的属性上加@BatchSize注解,用来设置当通过UserInfo加载Address的时候一次取数据的大小
private List<Address> addressList;
}-
注意事项:@BatchSize 的使用具有局限性,不能作用于 @ManyToOne 和 @OneToOne 的关联关系上,那样代码是不起作用的,如下所示。
1
2
3
4
5
6public class Address extends BaseEntity {
private String city;
//由于是@ManyToOne的关联关系所有没有作用
private UserInfo userInfo;
}
-
解决方法三:
-
Hibernate 中还提供了一种 FetchMode 的策略,包含三种模式,分别为 FetchMode.SELECT、FetchMode.JOIN,以及 FetchMode.Subselect。
-
Hibernate 中 @Fetch 数据的策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// fetch注解只能用在方法和字段上面
public Fetch {
//注解里面,只有一个属性获取数据的模式
FetchMode value();
}
//其中FetchMode的值有如下几种:
public enum FetchMode {
//默认模式,就是会有N+1 sql的问题;
SELECT,
//通过join的模式,用一个sql把主体数据和关联关系数据一口气查出来
JOIN,
//通过子查询的模式,查询关联关系的数据
SUBSELECT
} -
需要注意的是,不要把这个注解和 JPA 协议里面的 FetchType.EAGER、FetchType.LAZY 搞混了,JPA 协议的关联关系中的 FetchTyp 解决的是取关联关系数据时机的问题,也就是说 EAGER 代表的是立即获得关联关系的数据,LAZY 是需要的时候再获得关联关系的数据。
-
FetchMode.SELECT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserInfo extends BaseEntity {
private String name;
private String telephone;
private List<Address> addressList;
} -
FetchMode.JOIN
1
2
3
4
5
6
7public class UserInfo extends BaseEntity {
private String name;
private String telephone;
//唯一变化的地方采用JOIN模式
private List<Address> addressList;
} -
FetchMode.SUBSELECT
1
2
3
4
5public class UserInfo extends BaseEntity {
//我们这里测试一下LAZY情况
//唯一变化之处
private List<Address> addressList;
} -
FetchMode.SUBSELECT 支持 ID 查询和各种条件查询,唯一的缺点是只能配置在 @OneToMany 和 @ManyToMany 的关联关系上,不能配置在 @ManyToOne 和 @OneToOne 的关联关系上
-
@Fetch 的不同模型,都有各自的优缺点:FetchMode.SELECT 默认,和不配置的效果一样;FetchMode.JOIN 只支持类似 findById(id) 的方法,只能根据 ID 查询才有效果;FetchMode.SUBSELECT 虽然不限使用方式,但是只支持 ToMany 的关联关系。
-
-
-
解决方法四:
-
JPA 协议企图通过 @NamedEntityGraph 注解来描述实体之间的关联关系,当被 @EntityGraph 使用的时候进行 EAGER 加载,以减少 N+1 的 SQL
-
@NamedEntityGraph 和 @EntityGraph 用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74//可以被@NamedEntityGraphs注解重复使用,只能配置在类上面,用来声明不同的EntityGraph;
public NamedEntityGraph {
//指定一个名字
String name() default "";
//哪些关联关系属性可以被EntityGraph包含进去,默认一个没有。可以配置多个
NamedAttributeNode[] attributeNodes() default {};
//是否所有的关联关系属性自动包含在内,默认false;
boolean includeAllAttributes() default false;
//配置subgraphs,子实体图(可以理解为关联关系实体图,即如果算层级,可以配置第二层级),可以被NamedAttributeNode引用
NamedSubgraph[] subgraphs() default {};
//配置subclassSubgraphs的namedSubgraph有哪些。即如果算层级,可以配置第三层级
NamedSubgraph[] subclassSubgraphs() default {};
}
// @NamedEntityGraphs 能够配置多个 @NamedEntityGraph 只能使用在实体类上面
public NamedEntityGraphs{
NamedEntityGraph[] value();//可以同时指定多个NamedEntityGraph
}
// 用来进行属性节点的描述
public NamedAttributeNode {
//要包含的关联关系的属性的名字,必填
String value();
//如果我们在@NamedEntityGraph里面配置了子关联关系,这个是配置subgraph的名字
String subgraph() default "";
//当关联关系是被Map结构引用的时候,我们可以指定key的方式,一般很少用
String keySubgraph() default "";
}
public NamedSubgraph {
//指定一个名字
String name();
//子关联关系的类的class
Class type() default void.class;
//二层关联关系的要包含的关联关系的属性的名字
NamedAttributeNode[] attributeNodes();
}
//EntityGraph 作用在Repository的接口里面的方法上面
public EntityGraph {
//指@EntityGraph注解引用的@NamedEntityGraph里面定义的name,如果是空EntityGraph就不会起作用,如果为空相当于没有配置;
String value() default "";
//EntityGraph的类型,默认是EntityGraphType.FETCH类型,我们接着往下看EntityGraphType一共有几个值
EntityGraphType type() default EntityGraphType.FETCH;
//可以指定attributePaths用来覆盖@NamedEntityGraph里面的attributeNodes的配置,默认配置是空,以@NamedEntityGraph里面的为准;
String[] attributePaths() default {};
//JPA 2.1支持的EntityGraphType对应的枚举值
public enum EntityGraphType {
//LOAD模式,当被指定了这种模式、被@EntityGraph管理的attributes的时候,原来的FetchType的类型直接忽略变成Eager模式,而不被@EntityGraph管理的attributes还是保持默认的FetchType
LOAD("javax.persistence.loadgraph"),
//FETCH模式,当被指定了这种模式、被@EntityGraph管理的attributes的时候,原来的FetchType的类型直接忽略变成Eager模式,而不被@EntityGraph管理的attributes将会变成Lazy模式,和LOAD的区别就是对不被@NamedEntityGraph配置的关联关系的属性的FetchType不一样;
FETCH("javax.persistence.fetchgraph");
private final String key;
private EntityGraphType(String value) {
this.key = value;
}
public String getKey() {
return key;
}
}
} -
@EntityGraph 使用实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54第一步:在实体里面配置
//这里我们直接使用@NamedEntityGraph,因为只需要配置一个@NamedEntityGraph,我们指定一个名字getAllUserInfo,指定被这个名字的实体试图关联的关联关系属性是userInfo
public class Address extends BaseEntity {
private String city;
//防止JSON死循环
//采用默认的lazy模式
private UserInfo userInfo;
}
//UserInfo对应的关联关系,我们利用@NamedEntityGraphs配置了两个,一个是针对Address的关联关系,一个是name叫rooms的实体图包含了rooms属性;我们在UserInfo里面增加了两个关联关系;
public class UserInfo extends BaseEntity {
private String name;
private String telephone;
private Integer ages;
//默认LAZY模式
private List<Address> addressList;
//默认EAGER模式
private List<Room> rooms;
}
第二步:在我们需要的 Repository 的方法上面直接使用
//因为要用findAll()做测试,所以可以覆盖JpaRepository里面的findAll()方法,加上@EntityGraph注解
public interface UserInfoRepository extends JpaRepository<UserInfo, Long>{
//我们指定EntityGraph引用的是,在UserInfo实例里面配置的name=addressGraph的NamedEntityGraph;
// 这里采用的是LOAD的类型,也就是说被addressGraph配置的实体图属性address采用的fetch会变成 FetchType.EAGER模式,而没有被addressGraph实体图配置关联关系属性room还是采用默认的EAGER模式
List<UserInfo> findAll();
}}
public interface AddressRepository extends JpaRepository<Address, Long>{
//可以覆盖原始方法,添加上不同的@EntityGraph策略
//使用@EntityGraph查询所有Address的时候,指定name = "getAllUserInfo"的@NamedEntityGraph,采用默认的EntityGraphType.FETCH,如果Address里面有多个关联关系的时候,只有在name = "getAllUserInfo"的实体图配置的userInfo属性上采用Eager模式,其他关联关系属性没有指定,默认采用LAZY模式;
List<Address> findAll();
}
-
JPA @Query 中SpEL的应用场景
-
通过 SpEL 取被 @Query 注解的方法参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49//用法一:根据下标取方法里面的参数
List<User> findUsersByAge(int age);
//用法二:#customer取@Param("customer")里面的参数
List<User> findUsersByCustomersFirstname(; Customer customer)
//用法三:用JPA约定的变量entityName取得当前实体的实体名字
List<UserInfo> findAllByEntityName();
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {
// JPA约定的变量entityName取得当前实体的实体名字
List<UserInfo> findAllByEntityName();
//一个查询中既可以支持SpEL也可以支持普通的:ParamName的方式
void updateUserActiveState(; String name, Long id)
//演示SpEL根据数组下标取参数,和根据普通的Parma的名字:name取参数
List<UserInfo> findContainingEscaped(; String name)
//SpEL取Parma的名字customer里面的属性
List<UserInfo> findUsersByCustomersFirstname(; UserInfo customer)
//利用SpEL根据一个写死的'jack'字符串作为参数
List<UserInfo> findOliverBySpELExpressionWithoutArgumentsWithQuestionmark();
//同时SpEL支持特殊函数escape和escapeCharacter
List<UserInfo> findByNameWithSpelExpression(String name);
// #entityName和#[]同时使用
List<UserInfo> findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntityExpression(String name, String lastName);
//对于 native SQL同样适用,并且同样支持取pageable分页里面的属性值
Page<UserInfo> findUsersInNativeQueryWithPagination(Pageable pageable);
} -
Spring-Security-Data 在 @Query 中的用法
1
2
3
4
5
6
7// 根据当前用户email取当前用户的信息
List<UserInfo> findCurrentUserWithCustomQuery();
//如果当前用户是admin,我们就返回某业务的所有对象;如果不是admin角色,就只给当前用户的某业务数据
List<BusinessObject> findBusinessObjectsForCurrentUser(); -
SpEL 在 @Cacheable 中的应用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14//缓存key取当前方法名,判断一下只有返回结果不为null或者非empty才进行缓存
public Map<String, Map<String, String>> getAppGlobalSettings() {}
//evict策略的key是当前参数customer里面的name属性
public String getAddress(Customer customer) {...}
//在condition里面使用,当参数里面customer的name属性的值等于字符串Tom才放到缓存里面
public String getAddress(Customer customer) {...}
//用在unless里面,利用SpEL的条件表达式判断,排除返回的结果地址长度小于64的请求
public String getAddress(Customer customer) {...}支持的属性 作用域 功能描述 使用方法 methodName root 对象 当前被调用的方法名 #root.methodName method root 对象 当前被调用的方法 #root.method.name target root 对象 当前被调用的目标对象 #root.target targetClass root 对象 当前被调用的目标对象类 #root.targetClass args root 对象 当前被调用的方法的参数列表 #root.args[0] caches root 对象 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个 cache #root.caches[0].name argument name 执行上下文 当前被调用的方法的参数,如 findById(Long id),我们可以通过 #id 拿到参数 #user.id
表示参数 user 里面的 idresult 执行上下文 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache evict’的 beforeInvocation=false) #result
解决 In 查询条件内存泄漏的方法
-
第一种方法:修改缓存的最大条数限制
1
2
3
4
5默认 DEFAULT_QUERY_PLAN_MAX_COUNT = 2048,也就是 query plan 的最大条数限制是 2048。这样默认值可能有点大了,我们可以通过如下方式修改默认值
#修改 默认的plan_cache_max_size,太小会影响JPQL的执行性能,所以根据实际情况可以自由调整,不宜太小,也不宜太大,太大可能会引发内存溢出
spring.jpa.properties.hibernate.query.plan_cache_max_size=512
#修改 默认的native query的cache大小
spring.jpa.properties.hibernate.query.plan_parameter_metadata_max_size=128 -
第二种方法:根据 max plan count 适当增加堆内存大小
1
因为 QueryPlanMaxCount 是有限制的,那么肯定最大堆内存的使用也是有封顶限制的,我们找到临界值修改最小、最大堆内存即可。
-
第三种方法:减少 In 的查询 SQL 生成条数
1
2### 默认情况下,不同的in查询条件的个数会生成不同的plan query cache,我们开启了in_clause_parameter_padding之后会减少in生成cache的个数,会根据参数的格式运用几何的算法生成QueryCache;
spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true