前言:
JPA (java persistence API)作为数据库ORM规范, 具体实现代表:Hibernate、EclipseLink。Hibernate比较常用。
JDBC作为Java 与数据库连接的桥梁,JPA 也不例外,因此在一些特殊情况下想要查看最终生成的SQL语句执行过程
可加断点在下面方法,debug查看执行SQL 过程:
com.mysql.cj.NativeSession#execSQL (在执行任意SQL 时都会进入该方法)
参考学习:
springData JPA:blog.csdn.net/qq_40161813…
Hibernate官网:docs.jboss.org/hibernate/o…
Hibernate
源码: 5.4.20
核心类:sessionImpl:
JPA 中 DML 操作首先都是系列的DefaultXXXEventListener 进行处理:
DefaultDeleteEventListener, DefaultMergeEventListener, DefaultPersistEventListener
实体状态关系
new的一个实体类 :
- 在调用merge的时候 会作为Datached 状态;
- 在调用persist的时候 会作为Transient 状态;
基本使用
其中的em 为EntityManger对象, 在spring data jpa 中可以直接@PersistenceContext 注解进行引入。
Spring Data JPA 底层也是对下面方式进行的一些列封装。
@PersistenceContext(unitName = "name")
private EntityManager entityManager;
// HQL,
Query query = em.createQuery("from User where userIdx.id = :id"); // 任何DML SQL
query.setParameter("id", 1);
User user = (User) query.getSingleResult();
// native
Query nativeQuery = em.createNativeQuery("select * from users"); // 任何DML SQL
// em.find()
// em.persist();
// em.merge()
核心类
SessionImpl: 定义了常见的 DML 方法,
PersistenceContext: 记录的会话中的对象信息,缓存等。
SingleTableEntityPersister: 数据库实体 类信息, 包含相关DML SQL, save,update方法的处理等,他跟底层JDBC最近。
继承关系
下面介绍sessionImpl 中的一些API
Query
直接构造SQL 进行查询,不会查询缓存。在查询前会尝试flush 缓存中的内容, 防止前面修改了数据,没有刷新到数据库,导致没有查到最新值。
在查询结束时:
- 如果是JPQL查询,会将结果 放入到缓存中,即:entitiesByKey, EntityEntryContext。
查询结束会判断缓存中是否存在,如果不存在那么写入缓存。如果缓存存在,直接返回缓存的值,丢弃新的查询。即:代码层面保证了可重复读。
- 如果是native 查询,查询结束不会放入缓存。
-
- debug 发现: jpql CustomLoader#entityPersisters 不会空;而nativeSQL 是空导致不会写入缓存。
demo:
// @Transactional 管理
// nativeSQL, entityManager.createNativeQuery 等价于session.createSQLQuery()
Query nativeQuery = entityManager.createNativeQuery("select * from student where id = ?");
nativeQuery.setParameter(1, 1);
List resultList = nativeQuery.getResultList();
resultList.forEach(System.out::println);
// jpql
Query nativeQuery = entityManager.createQuery("from Student where id = ?1");
nativeQuery.setParameter(1, 1);
List<Student> resultList = nativeQuery.getResultList();
resultList.forEach(System.out::println);
createQuery:
这里会构造一个HQLQueryPlan 对象出来,后续缓存起来。 该对象包含SQL语句、参数绑定类型等。
在使用Spring Data JPA 的@Query 注解编写的JPAL 或HQL,项目初始化时都会构造该对象。
调用getSingleResult:
不管是单个还是多个 都会执行list():
PS 对象获取结果集:
最终执行到MySQL的执行代码:
处理结果集:
创建实例对象,如果entitiesByKey 中已经存在,直接返回老的对象。
添加EntityEntry 到缓存中
分别添加: entitiesByKey, EntityEntryContext
doQuery
上面简单介绍了Query 查询过程,这里继续详细分析下:
在调用list 查询时、以及缓存中找不到查询DB, 最终都会走入下面堆栈:
getRowFromResultSet 
extractKeysFromResultSet
提取key
有查询参数: findById 这种查询、以及使用查询参数构造。HQL 除外
没有查询参数的时候:通过结果集构造
添加缓存--getRow
参数key: 即主键
当缓存中有 执行instanceAlreadyLoaded,做一些额外操作,直接返回缓存中的对象。保证了可重复读
instanceNotYetLoaded
缓存中没有的时候, 会构造参数写入缓存中。
loadFromResultSet
addUninitializedEntity
下面依次向 entitiesByKey、entityEntryContext 中添加对象。此时entity 不值包含 key 对应的字段,其余字段为null。
addEntity:
addEntry:
EntityEntryContext 添加EntityEntry
hydrate
ResultSet 中提取的值仅仅保存在EntityEntry的loadedState中
postHydrate
该方法会重新替换将EntityEntryContext 中的EntityEntry, values 作为 loadedState
initializeEntitiesAndCollections
前面得到的result 对象只包含了key对应的字段, 这里会实例化key 之外的字段
find
会使用JPA 的缓存进行查找: User user = session.find(User.class, new UserIdx(1,1));
依次向下跟踪到:fireLoad
缓存查询:
一级缓存:
这里的keyToLoad: new EntityKey( id, persister )
从EntityEntryContext 中获取EntityEntry, 实际上是从 nonEnhancedEntityXref中获取。
// 这里的Object 对应 entitiesByKey 中的Obj, 即实体对象。
// ManagedEntity 用来记录 实体对象的状态信息。 注意这里是IdentityHashMap
private transient IdentityHashMap<Object,ManagedEntity> nonEnhancedEntityXref;
delete
缓存中不存在
当删除一个缓存中不存在(detached)的对象时,会抛出异常,JPA 为了事务一致性等,默认禁止这样操作。
可以直接调用merge 方法,会将对象作为maneged。然后在调用delete
调用isTransient 没有查到的话,会直接return。
抛出异常:
缓存中存在
当删除一个缓存中存在的对象时,会设置EntityEntry 为deleted。 同时添加deleteAction 到ActionQueue。
save
调用save 后会执行DefaultSaveEventListener, 在springboot jpa的save方法会先调用查询接口将数据存入缓存中。
当缓存中已经存在的时候,内部仅仅作一些校验。
entityIsTransient:缓存中没有
persist
持久化对象
- 当缓存中没有对象,即new 一个对象时,返回TRANSIENT,会直接向缓存中加入EntityEntry,成为managed, 然后注册InsertActionEvent
- 当缓存中有这个对象, 状态为PERSISTENT, 会调用 entityIsPersistent,不会有什么处理。 事务提交的时候,会检查对象状态进行merge,处理更新
这里会向缓存中加入EntityEntry, 注册InsertActionEvent
判断是否需要生成Version
Integer 类型,默认0
onMerge:
如果从缓存、db 查不到这个对象(deleted 状态视为查不到),那么执行 save 逻辑。
- 如果当前delete 的ActionEvent 进行执行,即发出delete 的SQL,然后在向ActionQueue 中添加InsertActionEvent
如果能查到当前对象,那么会处理merge 对象的操作
核心逻辑:
根据不同的对象状态进入不同的逻辑
entityIsDetached
上面如果查到了结果,则会处理merge 逻辑:新的对象属性覆盖缓存中的对象属性(仅仅处理非key)。 同时会将对象 作为managed 状态。 在事务提交的时候,会调用flush 处理该EntityEntry。
entityIsTransient
执行entityIsTransient: 下面会分两个逻辑进行处理
flush 调delete 的事件
EntityDeleteAction#execute:
会执行delete 操作,然后在清理 entityEntryContext、 以及entitiesByKey、nullifiableEntityKeys
delete 入口:
执行save 的逻辑
update
先查询一个数据库的对象,然后修改字段。即使不调用update 方法, 最后在处理缓存中的EntityEntry时,事务提交的时候会处理对应的EntityEntry,检查到被修改就会发出update event。
一旦检查出某个字段被修改,发起的update 语句是所有的字段都会重新Set。
缓存中没有:
添加EntityEntry
查询缓存
QueryResultsCache: 默认关闭
1、需要同时打开下面两个参数:
hibernate.cache.use_second_level_cache: 二级缓存, 跨session
hibernate.cache.use_query_cache: 查询缓存
2、 需要指定缓存的具体实现
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
大概是下面实现:
flush
下面场景会触发flush操作
- 提交事务前,flush
- 执行JPQL/HQL query、any nativeSQL 前 都会尝试 flush。
当flush一旦完成,ActionQueue 中的ExecutableList 都会清空,缓存中依然会保存最新的值。
查询接口 flush 检查
不管是 JPQL,还是HQL 都会执行到下面方法
下面方法判断缓存中是否有实体。 缓存中有内容 则执行:performExecutions
通过上面看到核心是调用autoFlushIfRequired方法来处理flush操作。下面简单看下其他操作是如何flush 的
HQL
update 操作:
delete 操作:
入口没啥区别
native SQL
update
delete:
跟update 都是同一个入口
提交事务
beforeCompletionCallback
在事务执行前,需要先将ActionQueue 中的操作进行处理。
flush执行
执行SessionImple# doFlush -- > DefaultFlushEventListener#onFlush
flush ->flushEverythingToExecutions
事务提交的时候会调用flush 后最终会执行到这里。
该方法还有其他几个调用入口:
prepareEntityFlushes
核心: 将ManagedEntity 保存到reentrantSafeEntries(为了线程安全), 后续flushEntities方法获取该对象处理。
当addEntityEntry, removeEntityEntry, dirty 都会设置为true, 下面将ManagedEntity 保存到reentrantSafeEntries。
flushEntities:
这里会检查reentrantSafeEntries, 即当前缓存中的对象是否有变化(跟loadedState 比较)。如果发送了变化 会创建 update event 到ActionQueue。
dirty check
检查实体是否跟缓存中的对象发生了变化(跟loadedState 对比), loadedState 表示刷新到数据库后的最新值
对比不同的字段
实际上就是调用equals 进行比较。
scheduleUpdate
当上面检查通过,那么执行下面逻辑:
根据需要递增Version(即使手动指定也无用),添加EntityUpdateAction
performExecutes
执行相关的Actions
executeActions 方法内部,在最后会清理当前的ExecutableList
执行executeActions
EntityInsertAction
insert 操作,在执行该方法前,缓存中已经添加了相应的EntityEntry
执行insert 方法
支持批处理,执行addToBatch。
当数量达到batchSize,执行performExecution,发出SQL
批处理行数配置:
在上面执行完ExecutionList后,继续执行下executeBatch,内部会检查是否有还没有执行performExecution的数据。如果有会继续调用performExecution。
EntityUpdateAction
在进入veto分支:
会执行AbstractEntityPersister#updateOrInsert, 跟insert 类似,同样会判断是否需要使用batch 操作。
执行结束更新EntityEntry,也就是执行一次Action 就得更新下缓存中的值。
没有开启事务
在一些update操作,JPA 会自动检查是否运行事务外执行。默认是不允许的,如果没有开启事务,会抛出异常。
可以手动打开选项:hibernate.allow_update_outside_transaction = true
**
此时执行update 的SQL 会自动向datasource 获取一个连接,然后发起 update 的sql 语句
其他api
EntityManager.flush: 刷新ActionQueue中的任务。
EntityManager.clear: 清空缓存中的所有内容,包括ActionQueue中的任务。
批处理
mysql 开启批处理需要设置URL参数: rewriteBatchedStatements=true,
最终insert sql 如下数据包:
同样批处理update 也会合并一个数据包:
Spring Data JPA
分析版本: 2.3.3.RELEASE
Spring Data Jpa 依赖与Spring data common 包,SpringData Common 包下面还有一些其他项目:
介绍SpringData Jpa 对外提供的方法,对应JPA底层怎么执行的
初始化
具体实现: zhuanlan.zhihu.com/p/520510314
HibernateJpaConfiguration
由spring boot 的自动装配机制,首先执行@Import HibernateJpaConfiguration.class
HibernateJpaConfiguration 继承 JpaBaseConfiguration
在JpaBaseConfiguration 中会初始化JPA 核心的一些对象
通过EntityManagerFactoryBuilder构造LocalContainerEntityManagerFactoryBean
, 下面的packagesToScan 可以通过 @EntityScan 指定。默认是启动类的包
afterPropertiesSet
LocalContainerEntityManagerFactoryBean 对象初始化完成后 ,执行其:#afterPropertiesSet
Entity 扫描
由于之前在启动类 使用 @EntityScan 指定了包路径,因此这里为com.demo.entity。 默认没有指定为启动类同路径:
仅支持如下四种类型:
创建EntityManagerFactory
执行父类:#afterPropertiesSet
最终创建一个EntityManagerFactory的代理对象出来, 通过EntityManagerFactoryBean 获取的对象即该代理对象
代理对象拦截器即:持有EntityManagerFactoryBean对象
PersistenceAnnotationBeanPostProcessor
为 PersistenceContext、 PersistenceUnit 注解 注入对应的属性
注入EntityManager对象给 @PersistenceContext 属性时:
Repositories 注册
JPARepositoriesRegistrar
将自定义的Repository 类 转换为一个JpaRepositoryFactoryBean ****对象
JpaRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport
最终执行:RepositoryBeanDefinitionRegistrarSupport#registerBeanDefinitions
会将用户定义的Repository 转换成一个 JpaRepositoryFactoryBean 对象。 在截图中间的for 会调用build 方法构建BeanDefinition对象,lazyInit 也会在这里设置(即 BootstrapMode#LAZY)
在上面扫描Bean时:
这里会自定排除NoRepositoryBean的注解对象(JPA 的一些内置接口有这个定义:JpaRepositoryImplementation)。 只扫描Repository接口的、以及注解RepositoryDefinition的类
JpaRepositoryFactory
由于实现了InitializingBean,当bean初始化完成后 会调用afterPropertiesSet方法:
首先会创建一个JpaRepositoryFactory对象,然后调用其getRepository 获取真正的代理对象。
(当代码中注入Respository 对象时, 也就是该repository对象, FactoryBean# getObject)
JpaRepositoryFactory extends RepositoryFactorySupport
public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
// 获取Repository接口的 相关信息,包括泛型ID 之类的
RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface);
RepositoryComposition composition = getRepositoryComposition(metadata, fragments);
// 通过接口信息获取接口实现基类: 默认SimpleJpaRepository
RepositoryInformation information = getRepositoryInformation(metadata, composition);
// 实例化SimpleJpaRepository 类
Object target = getTargetRepository(information);
// Create proxy,
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
// 添加各种的advisor:
result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
postProcessors.forEach(processor -> processor.postProcess(result, information));
.......
// 调用底层的代理工厂 创建代理类,默认JDK
T repository = (T) result.getProxy(classLoader);
return repository;
最终添加的Advisor 如下:
@Repository
该注解内部被标识为@Componet。
通过上面源码分析,JPA 会自动扫描Repository 接口的实现类,因此该注解可以直接忽略。
JPA 的实现类处理过程
默认 以Impl 结尾的类
前面提到在转换用户的Repository 到JpaRepositoryFactoryBean 对象时,会在下面代码构建JpaRepositoryFactoryBean的beanDefinition
registerCustomImplementation
toLookupConfiguration
DefaultImplementationLookupConfiguration:通过名称拼接类名
detectCustomImplementation
查找imple 的类
最终走到如下: 将该目录所有的Impl 结尾的类都扫描出来
构造拦截器:
将自定义的实现类作为ImplementationMethodExecutionInterceptor 对象添加到advisor:
ImplementationMethodExecutionInterceptor:
执行的时候会走入该拦截器
前面会查找该method 属于哪个fragments,然后执行对应的目标:
也不需要加注解:
自定义接口 fragment interface
InsertRepository为自定义的接口:
registerRepositoryFragmentsImplementation
会过滤掉@NoRepositoryBean, JpaRepository 被标记过
detectRepositoryFragmentConfiguration --->detectCustomImplementation
implementationCandidates 定义为一个Lazy, 在扫描自定义 Impl的时候,如果扫描过了,后面处理InsertRepositoryImpl 将不会触发扫描。因此这里只会扫描一次。
这里就不会调用findCandidateBeanDefinitions
要解决上诉问题有如下方案:
- 将fragment 相关内容放入Repository同一个包下
- 直接添加一个新的 basePackge 扩展路径
potentiallyRegisterFragmentImplementation
注册实现类的BeanDefinition
potentiallyRegisterRepositoryFragment
注册repositoryFragment 的beanDefinition
构造拦截器
可以看到实现类都添加进了composition, 当调用目标方法时,会在这几个实现类中查找 具体方法。
调用JPA 方法执行流程
见本地:SpringDataJpa 源码.md
核心:TransactionAspectSupport#invokeWithinTransaction
JPA 的接口方法默认都会执行该方法,但是事务的是否开启只取决于 执行目标类中是否有 @Transactional注解、 以及是否设置 enableDefaultTransactions = false (默认true)有关系。
解析@Transactional 注解
查看@Transactional 注解设置的信息:
ClassUtils.getMostSpecificMethod: 会查找SimpleJapRepository 相关的所有父类。
开启事务
createTransactionIfNecessary: 会根据txAttr的状态来判断
根据不同的传递模式进入不同的 分支:
doBegin
beginTransaction
getSession: 实际上这里就是返回的EntityManager对象本身。
获取真实的Connection:会通过dataSource获取Connection
底层readOnly
prepareSynchronization
绑定状态信息
事务信息管理
会将当前线程的事务信息放入ThreadLocal
下面方法就是获取上面放入ThreadLocal的连接对象:
执行JPA 目标方法
在JPA 实现类处理过程中分析到,最终生成的advisor 中有一个:ImplementationMethodExecutionInterceptor
该类会判断目标方法数据在哪一个实现类中存在: 即会在 fragments 类中查找 调用目标方法,当找到了会缓存到fragmentCache 中。方便下一次快速调用目标方法。
JPQL 执行 过程
Query
当执行上面方法时,首先通过拦截器链进入 JpaQueryExecution#execute
通过createQuery 创建Query对象, 然后调用getSingleResult 获取结果
Spring data jpa 自带方法
注解:
@Query: 不会查询缓存,直接查询数据库,但是结果会放入缓存。底层:session#createQuery
@Modify: 使用该注解的时候,也是直接 执行SQL 到数据库,不会影响缓存中的数据。
内置方法:实现基本都在SimpleJpaRepository,核心都是通过EntityManager调用目标方法
findById: 底层调用session#find
deleteById: 底层: session# delete(findById(id))
save:id字段有值 调用persist, 无则调用merge
SharedEntityManagerInvocationHandler
该对象作为EntityManager的代理对象,即当通过EntityManager 调用目标方法时,会首先在这里进行处理。
在SimpleJpaRepository 中核心也是通过EntityManager代理对象来调用目标方法。
核心即通过ThreadLocal 获取当前事务的EntityManager对象来执行相关的目标方法: 即执行SessionImpl 中的方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Invocation on EntityManager interface coming in...
if ...
else if (method.getName().equals("getEntityManagerFactory")) {
// JPA 2.0: return EntityManagerFactory without creating an EntityManager.
return this.targetFactory;
}
else if (method.getName().equals("unwrap")) {
// JPA 2.0: handle unwrap method - could be a proxy match.
Class<?> targetClass = (Class<?>) args[0];
if (targetClass != null && targetClass.isInstance(proxy)) {
return proxy;
}
}
// 当JPA开启事务后,这里会返回缓存中的EntityManager对象,否则返回空
EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(
this.targetFactory, this.properties, this.synchronizedWithTransaction);
if (method.getName().equals("getTargetEntityManager")) {
return target;
}
else if (method.getName().equals("unwrap")) {
...
}
else if (transactionRequiringMethods.contains(method.getName())) {
// 如果是需要事务的方法,这里进行一些校验是否有事务(即上面target 不为空)
if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive() &&
!target.getTransaction().isActive())) {
throw new TransactionRequiredException("No EntityManager with actual transaction available " +
"for current thread - cannot reliably process '" + method.getName() + "' call");
}
}
// Regular EntityManager operations.
boolean isNewEm = false;
if (target == null) { // 没有开启事务,创建一个新的EntityManager
logger.debug("Creating new EntityManager for shared EntityManager invocation");
target = (!CollectionUtils.isEmpty(this.properties) ?
this.targetFactory.createEntityManager(this.properties) :
this.targetFactory.createEntityManager());
isNewEm = true;
}
// Invoke method on current EntityManager.
try {
Object result = method.invoke(target, args);
if (result instanceof Query) {
if (isNewEm) {
Query query = (Query) result;
为result 生成代理对象:DeferredQueryInvocationHandler
...
isNewEm = false;
}
}
return result;
}
finally {
if (isNewEm) { // 如果是新创的EntityManager对象,结束的时候关闭连接
EntityManagerFactoryUtils.closeEntityManager(target);
}
}
}
从上面代码可以知道,在操作EntityManager代理对象的非事务方法时,如果当前没有被事务进行管理,会在 SharedEntityManagerInvocationHandler 中创建一个底层的EntityManager对象。
同时如果执行的是创建Query对象时,在返回的时候会为Query对象也生成一个代理对象DeferredQueryInvocationHandler, 此时不会关闭刚创建的EntityManager对象。
对于非Query对象的创建,则会立即关闭底层EntityManager对象
SimpleJpaRepository中执行的操作同样是通过EntityManager进行执行,默认请求每个方法都会开启事务,因此不会存在调用createQuery后立即关闭,造成不必要的额外开销。
实际上普通的查询使用事务也完全没必要(JPA 这么设计是为啥呢)。
DeferredQueryInvocationHandler
当EntityManager对象调用createXX,返回Query对象时,会创建该代理对象返回。
Query对象执行的是需要终止的方法时,则执行结束会尝试关闭底层EntityManager。
因此,下面demo,在调用getResultList时就会自动关闭链接。
@PersistenceContext
private EntityManager entityManager;
public void testConnection() {
Query nativeQuery = entityManager.createNativeQuery("select * from cloud_eu_invoice_setting where reseller_no = 110486 limit 1");
List resultList = nativeQuery.getResultList();
System.out.println(resultList);
}
自定义 Repository
自定义update,防止对象处于托管态时,save的时候会查询数据库。 通过自定义逻辑调用底层连接对象,直接发起更新SQL 到数据库。
先看下Hibernate内部是如何获取的底层连接对象来执行SQL 的。
SingleTableEntityPersister
该类是调用底层JDBC 方法最核心的一个类,提供的 insert,update 相关SQL 的生成,以及参数绑定。最终调用JDBC执行对应的SQL。
这里简单跟踪下执行insert 方法:
自定义实现
通过上面可以看到我们可以通过session 对象直接获取PreparementStatement对象:
即我们可以写出下面代码来获取:
SessionImpl session = entityManager.unwrap(SessionImpl.class);
@Cleanup PreparedStatement preparedStatement = session.getJdbcCoordinator().getStatementPreparer()
.prepareStatement("select * from users", Statement.NO_GENERATED_KEYS);
boolean ints = preparedStatement.execute();
ResultSet resultSet = preparedStatement.getResultSet();
resultSet.next();
String string = resultSet.getString(1);
System.out.println(string);
具体实现参考仓库:
JPA 遇到的问题
保持丢失
在同一个事务中, 先查询出对象a,然后在删除a, 最后新增一个对象a。 事务提交后,a 对象没有新增成功,也没有任务错误信息。
demo:
意图是新增一个相同的对象,单实际上是不同的对象了(key 不同)
异常分析:
经debug 发现数据库对char类型有不同的处理:
当前数据库为sybase, 有一个联合索引作为聚集索引,该聚集索引中有一个字段 xref_type 为char(10)
- sybase 默认会将char类型字段的结果用空格补齐到指定长度。
- mysql 在处理结果集时会清理末尾空格。因此不会有问题
当调用saveAll 方法时,最终执行到下面方法
因为new 处理的对象手动指定了key,同时xrefType不带空格,因此缓存中是取不到对象的
继续查询DB,生成一个缓存对象,由于这里是指定的对象key(会调用 find api),那么将手动指定的对象key 作为缓存的key。
查询出了对象,因此走merge 分支,即使用代码中new 出来的对象覆盖 缓存中的对象。
现在缓存中对象如下:
提交事务后:
处理缓存中的对象信息。生成对于的EventAction
这里会发现有两个事件,但是执行顺序是先执行 updateAction、然后执行deleteAction。 因此数据最终被删除。
正常执行情况
如果new 对象的时候手动指定与 查询结果相同的内容: 即手动补充空格
那么在最终执行save的时候,会获取到缓存中的对象,状态为deleted 。进入save 逻辑
最终执行到这里: 会判断缓存中是否有值, 这里缓存中已经为deleted,就先flush, 生成delete的action,同时发出delete的sql。
方法末尾再次添加一个insert的ActionQueue ,事务提交的时候就直接执行insert 。
解决方案
- 更改数据表字段类型为varchar
- 调用deleteById 后,手动调用flush,强制先执行delete。
生成Version 报错:
由于手动创建数据的时候没有手动指定version的字段值,JPA 在更新的时候递增version字段发生异常:
java.lang.NullPointerException
at org.hibernate.type.IntegerType.next(IntegerType.java:70)
at org.hibernate.type.IntegerType.next(IntegerType.java:22)
at org.hibernate.engine.internal.Versioning.increment(Versioning.java:92)
at org.hibernate.event.internal.DefaultFlushEntityEventListener.getNextVersion(DefaultFlushEntityEventListener.java:425)
at org.hibernate.event.internal.DefaultFlushEntityEventListener.scheduleUpdate(DefaultFlushEntityEventListener.java:302)
at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:170)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:232)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:92)
在save 的时候会自动递增Version:
具体可添加断点查看具体如何设值:
首先是写入的数据库对应的Version 值,在update 的时候设置新的递增值。