spring boot jpa @PreUpdate结合@DynamicUpdate使用的局限性
通常给实体添加audit审计字段是一种常用的重构方法,如下:
@Embeddable
@Setter
@Getter
@ToString
public class Audit { /**
* 操作人
*/
private String operName; /**
* 操作、更新时间
*/
private LocalDateTime operDate; }
public interface Auditable {
Audit getAudit();
void setAudit(Audit audit);
}
/**
* 监听器 回调方法
*/
@Slf4j
@Transactional
public class AuditListener { @PrePersist
@PreUpdate
public void setCreatedOn(Auditable auditable) { Audit audit = auditable.getAudit();
if(audit == null) {
audit = new Audit();
auditable.setAudit(audit);
} audit.setOperName("hkk");
audit.setOperDate(LocalDateTime.now());
} }
实体类的定义
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "person")
@EntityListeners(value = AuditListener.class)
public class Person implements Auditable { @Embedded
@JsonUnwrapped
private Audit audit; @Id
@GeneratedValue(strategy = GenerationType.AUTO)
private BigDecimal id; private String name; }
测试代码:
@RequestMapping("/")
public List<Person> getPersons() {
Optional<Person> byId = personRepository.findById(BigDecimal.ONE);
if (byId.isPresent()) {
Person person = byId.get();
person.setName("hkk+" + LocalDateTime.now().toString());
personRepository.save(person);
}
else {
Person person = Person.builder()
.name("hkk")
.build();
personRepository.save(person);
}
List<Person> persons = personRepository.findAll();
System.out.println(persons);
return persons;
}
我们主要关注更新update时生成的sql:
update person set oper_date=?, oper_name=?, name=? where id=?
可以看到默认是把表中的所有字段都进行了更新。
如果一个表中字段数很多,就会影响更新效率。
所以通常我们需要在实体上添加@DynamicInsert 和@DynamicUpdate,如下:
@DynamicInsert
@DynamicUpdate
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "person")
@EntityListeners(value = AuditListener.class)
public class Person implements Auditable { @Embedded
@JsonUnwrapped
private Audit audit; @Id
@GeneratedValue(strategy = GenerationType.AUTO)
private BigDecimal id; private String name; }
这时更新SQL如下:
update person set name=? where id=?
我们发现,我们的审计字段并没有更新,也就是说生成的JPQL并不是我们想要的。
生成JPQL语句的代码是:org.hibernate.sql.Update.toStatementString
public String toStatementString() {
StringBuilder buf = new StringBuilder( (columns.size() * 15) + tableName.length() + 10 );
if ( comment!=null ) {
buf.append( "/* " ).append( comment ).append( " */ " );
}
buf.append( "update " ).append( tableName ).append( " set " );
boolean assignmentsAppended = false;
Iterator iter = columns.entrySet().iterator();
while ( iter.hasNext() ) {
Map.Entry e = (Map.Entry) iter.next();
buf.append( e.getKey() ).append( '=' ).append( e.getValue() );
if ( iter.hasNext() ) {
buf.append( ", " );
}
assignmentsAppended = true;
}
if ( assignments != null ) {
if ( assignmentsAppended ) {
buf.append( ", " );
}
buf.append( assignments );
}
boolean conditionsAppended = false;
if ( !primaryKeyColumns.isEmpty() || where != null || !whereColumns.isEmpty() || versionColumnName != null ) {
buf.append( " where " );
}
iter = primaryKeyColumns.entrySet().iterator();
while ( iter.hasNext() ) {
Map.Entry e = (Map.Entry) iter.next();
buf.append( e.getKey() ).append( '=' ).append( e.getValue() );
if ( iter.hasNext() ) {
buf.append( " and " );
}
conditionsAppended = true;
}
if ( where != null ) {
if ( conditionsAppended ) {
buf.append( " and " );
}
buf.append( where );
conditionsAppended = true;
}
iter = whereColumns.entrySet().iterator();
while ( iter.hasNext() ) {
final Map.Entry e = (Map.Entry) iter.next();
if ( conditionsAppended ) {
buf.append( " and " );
}
buf.append( e.getKey() ).append( e.getValue() );
conditionsAppended = true;
}
if ( versionColumnName != null ) {
if ( conditionsAppended ) {
buf.append( " and " );
}
buf.append( versionColumnName ).append( "=?" );
}
return buf.toString();
}
}
这里的column是我们想找的,是谁给它赋值的呢?
经常半天的调度,最终定位到这个方法:org.hibernate.event.internal.DefaultFlushEntityEventListener#onFlushEntity
/**
* Flushes a single entity's state to the database, by scheduling
* an update action, if necessary
*/
public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
final Object entity = event.getEntity();
final EntityEntry entry = event.getEntityEntry();
final EventSource session = event.getSession();
final EntityPersister persister = entry.getPersister();
final Status status = entry.getStatus();
final Type[] types = persister.getPropertyTypes(); final boolean mightBeDirty = entry.requiresDirtyCheck( entity ); final Object[] values = getValues( entity, entry, mightBeDirty, session ); event.setPropertyValues( values ); //TODO: avoid this for non-new instances where mightBeDirty==false
boolean substitute = wrapCollections( session, persister, types, values ); if ( isUpdateNecessary( event, mightBeDirty ) ) {
substitute = scheduleUpdate( event ) || substitute;
} if ( status != Status.DELETED ) {
// now update the object .. has to be outside the main if block above (because of collections)
if ( substitute ) {
persister.setPropertyValues( entity, values );
} // Search for collections by reachability, updating their role.
// We don't want to touch collections reachable from a deleted object
if ( persister.hasCollections() ) {
new FlushVisitor( session, entity ).processEntityPropertyValues( values, types );
}
} }
isUpdateNecessary( event, mightBeDirty )用于判断是否有要更新的字段,还有一个重要的操作就是,确定了要更新字段dirtyProperties
private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) {
final Status status = event.getEntityEntry().getStatus();
if ( mightBeDirty || status == Status.DELETED ) {
// compare to cached state (ignoring collections unless versioned)
dirtyCheck( event );
if ( isUpdateNecessary( event ) ) {
return true;
}
else {
if ( SelfDirtinessTracker.class.isInstance( event.getEntity() ) ) {
( (SelfDirtinessTracker) event.getEntity() ).$$_hibernate_clearDirtyAttributes();
}
event.getSession()
.getFactory()
.getCustomEntityDirtinessStrategy()
.resetDirty( event.getEntity(), event.getEntityEntry().getPersister(), event.getSession() );
return false;
}
}
else {
return hasDirtyCollections( event, event.getEntityEntry().getPersister(), status );
}
}
dirtyCheck:
/**
* Perform a dirty check, and attach the results to the event
*/
protected void dirtyCheck(final FlushEntityEvent event) throws HibernateException { final Object entity = event.getEntity();
final Object[] values = event.getPropertyValues();
final SessionImplementor session = event.getSession();
final EntityEntry entry = event.getEntityEntry();
final EntityPersister persister = entry.getPersister();
final Serializable id = entry.getId();
final Object[] loadedState = entry.getLoadedState(); int[] dirtyProperties = session.getInterceptor().findDirty(
entity,
id,
values,
loadedState,
persister.getPropertyNames(),
persister.getPropertyTypes()
); if ( dirtyProperties == null ) {
if ( entity instanceof SelfDirtinessTracker ) {
if ( ( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes() ) {
int[] dirty = persister.resolveAttributeIndexes( ( (SelfDirtinessTracker) entity ).$$_hibernate_getDirtyAttributes() ); // HHH-12051 - filter non-updatable attributes
// TODO: add Updateability to EnhancementContext and skip dirty tracking of those attributes
int count = 0;
for ( int i : dirty ) {
if ( persister.getPropertyUpdateability()[i] ) {
dirty[count++] = i;
}
}
dirtyProperties = count == 0 ? ArrayHelper.EMPTY_INT_ARRAY : count == dirty.length ? dirty : Arrays.copyOf( dirty, count );
}
else {
dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY;
}
}
else {
// see if the custom dirtiness strategy can tell us...
class DirtyCheckContextImpl implements CustomEntityDirtinessStrategy.DirtyCheckContext {
private int[] found; @Override
public void doDirtyChecking(CustomEntityDirtinessStrategy.AttributeChecker attributeChecker) {
found = new DirtyCheckAttributeInfoImpl( event ).visitAttributes( attributeChecker );
if ( found != null && found.length == 0 ) {
found = null;
}
}
}
DirtyCheckContextImpl context = new DirtyCheckContextImpl();
session.getFactory().getCustomEntityDirtinessStrategy().findDirty(
entity,
persister,
session,
context
);
dirtyProperties = context.found;
}
} event.setDatabaseSnapshot( null ); final boolean interceptorHandledDirtyCheck;
//The dirty check is considered possible unless proven otherwise (see below)
boolean dirtyCheckPossible = true; if ( dirtyProperties == null ) {
// Interceptor returned null, so do the dirtycheck ourself, if possible
try {
session.getEventListenerManager().dirtyCalculationStart(); interceptorHandledDirtyCheck = false;
// object loaded by update()
dirtyCheckPossible = loadedState != null;
if ( dirtyCheckPossible ) {
// dirty check against the usual snapshot of the entity
dirtyProperties = persister.findDirty( values, loadedState, entity, session );
}
else if ( entry.getStatus() == Status.DELETED && !event.getEntityEntry().isModifiableEntity() ) {
// A non-modifiable (e.g., read-only or immutable) entity needs to be have
// references to transient entities set to null before being deleted. No other
// fields should be updated.
if ( values != entry.getDeletedState() ) {
throw new IllegalStateException(
"Entity has status Status.DELETED but values != entry.getDeletedState"
);
}
// Even if loadedState == null, we can dirty-check by comparing currentState and
// entry.getDeletedState() because the only fields to be updated are those that
// refer to transient entities that are being set to null.
// - currentState contains the entity's current property values.
// - entry.getDeletedState() contains the entity's current property values with
// references to transient entities set to null.
// - dirtyProperties will only contain properties that refer to transient entities
final Object[] currentState = persister.getPropertyValues( event.getEntity() );
dirtyProperties = persister.findDirty( entry.getDeletedState(), currentState, entity, session );
dirtyCheckPossible = true;
}
else {
// dirty check against the database snapshot, if possible/necessary
final Object[] databaseSnapshot = getDatabaseSnapshot( session, persister, id );
if ( databaseSnapshot != null ) {
dirtyProperties = persister.findModified( databaseSnapshot, values, entity, session );
dirtyCheckPossible = true;
event.setDatabaseSnapshot( databaseSnapshot );
}
}
}
finally {
session.getEventListenerManager().dirtyCalculationEnd( dirtyProperties != null );
}
}
else {
// either the Interceptor, the bytecode enhancement or a custom dirtiness strategy handled the dirty checking
interceptorHandledDirtyCheck = true;
} logDirtyProperties( id, dirtyProperties, persister ); event.setDirtyProperties( dirtyProperties );
event.setDirtyCheckHandledByInterceptor( interceptorHandledDirtyCheck );
event.setDirtyCheckPossible( dirtyCheckPossible ); }
我们发现,代码执行到这里,并没有执行我们AuditListener, 它是什么时候执行的呢?
其实就是isUpdateNecessary方法的后面:substitute = scheduleUpdate( event ) || substitute;
private boolean scheduleUpdate(final FlushEntityEvent event) {
final EntityEntry entry = event.getEntityEntry();
final EventSource session = event.getSession();
final Object entity = event.getEntity();
final Status status = entry.getStatus();
final EntityPersister persister = entry.getPersister();
final Object[] values = event.getPropertyValues();
if ( LOG.isTraceEnabled() ) {
if ( status == Status.DELETED ) {
if ( !persister.isMutable() ) {
LOG.tracev(
"Updating immutable, deleted entity: {0}",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
else if ( !entry.isModifiableEntity() ) {
LOG.tracev(
"Updating non-modifiable, deleted entity: {0}",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
else {
LOG.tracev(
"Updating deleted entity: ",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
}
else {
LOG.tracev(
"Updating entity: {0}",
MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
);
}
}
final boolean intercepted = !entry.isBeingReplicated() && handleInterception( event );
// increment the version number (if necessary)
final Object nextVersion = getNextVersion( event );
// if it was dirtied by a collection only
int[] dirtyProperties = event.getDirtyProperties();
if ( event.isDirtyCheckPossible() && dirtyProperties == null ) {
if ( !intercepted && !event.hasDirtyCollection() ) {
throw new AssertionFailure( "dirty, but no dirty properties" );
}
dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY;
}
// check nullability but do not doAfterTransactionCompletion command execute
// we'll use scheduled updates for that.
new Nullability( session ).checkNullability( values, persister, true );
// schedule the update
// note that we intentionally do _not_ pass in currentPersistentState!
session.getActionQueue().addAction(
new EntityUpdateAction(
entry.getId(),
values,
dirtyProperties,
event.hasDirtyCollection(),
( status == Status.DELETED && !entry.isModifiableEntity() ?
persister.getPropertyValues( entity ) :
entry.getLoadedState() ),
entry.getVersion(),
nextVersion,
entity,
entry.getRowId(),
persister,
session
)
);
return intercepted;
}
就是这行代码,final boolean intercepted = !entry.isBeingReplicated() && handleInterception( event ); 总结:也就是说框架先执行了数据的脏数据检查,然后再执行了AuditListener的审计字段赋值,在脏数据检查时,就已经确定了要更新字段,改不了了,所以更新时,就不能更新我们的审计字段了。
目前的解决方法就是,去掉@DynamicUpdate,更新所有的字段。
spring boot jpa @PreUpdate结合@DynamicUpdate使用的局限性的更多相关文章
- spring boot JPA中实体类常用注解
spring boot jpa中的注解很多,参数也比较多.没必要全部记住,但是经常查看官方文档也比较麻烦,记录一下一些常用的注解.通过一些具体的例子来帮助记忆. @Entity @Table(name ...
- Spring boot Jpa添加对象字段使用数据库默认值
Spring boot Jpa添加对象字段使用数据库默认值 jpa做持久层框架,项目中数据库字段有默认值和非空约束,这样在保存对象是必须保存一个完整的对象,但在开发中我们往往只是先保存部分特殊的字段其 ...
- spring boot jpa 使用update 报错解决办法
在spring boot jpa 中自定义sql,执行update操作报错解决办法: 在@Query(...)上添加 @Modifying@Transactional注解
- Spring Boot(五):Spring Boot Jpa 的使用
在上篇文章Spring Boot(二):Web 综合开发中简单介绍了一下 Spring Boot Jpa 的基础性使用,这篇文章将更加全面的介绍 Spring Boot Jpa 常见用法以及注意事项. ...
- Spring Boot + JPA(hibernate 5) 开发时,数据库表名大小写问题
(转载)Spring Boot + JPA(hibernate 5) 开发时,数据库表名大小写问题 这几天在用spring boot开发项目, 在开发的过程中遇到一个问题hibernate在执 ...
- Spring Boot Jpa 的使用
Spring Boot Jpa 介绍 首先了解 Jpa 是什么? Jpa (Java Persistence API) 是 Sun 官方提出的 Java 持久化规范.它为 Java 开发人员提供了一种 ...
- (转)Spring Boot(五):Spring Boot Jpa 的使用
http://www.ityouknow.com/springboot/2016/08/20/spring-boot-jpa.html 在上篇文章Spring Boot(二):Web 综合开发中简单介 ...
- Spring boot JPA 用自定义主键策略 生成自定义主键ID
最近学习Spring boot JPA 学习过程解决的一些问题写成随笔,大家一起成长.这次遇到自定义主键的问题 package javax.persistence; public enum Gener ...
- Spring Boot(十五):spring boot+jpa+thymeleaf增删改查示例
Spring Boot(十五):spring boot+jpa+thymeleaf增删改查示例 一.快速上手 1,配置文件 (1)pom包配置 pom包里面添加jpa和thymeleaf的相关包引用 ...
随机推荐
- flask 第四篇 模板语言jinja2
是时候开始写个前端了,Flask中默认的模板语言是Jinja2 现在我们来一步一步的学习一下 Jinja2 捎带手把 render_template 中留下的疑问解决一下 首先我们要在后端定义几个字符 ...
- TCP被动打开 之 第三次握手-接收ACK
假定客户端主动打开,发送syn包到服务器,服务器创建连接请求控制块加入到队列,进入TCP_NEW_SYN_RECV 状态,发送syn+ack给客户端,并启动定时器,等待客户端回复最后一个握手ack: ...
- Java-内存模型 final 和 volatile 的内存语义
前提:内存屏障 内存屏障(Memory Barrier)与内存栅栏(Memory Fence)是同一个概念. 用于阻止指令重排序.保证了特定操作的执行顺序和某些变量的内存可见性. JMM 内存屏障分为 ...
- Anaconda 改为国内镜像的方法
Anaconda的conda 特别好用 但如果用国外的镜像,慢的出奇 可以改为了国内镜像会好很多 conda config --add channels https://mirrors.tuna.ts ...
- sql输出表中重复数据
数据: 1 1 2 32 2 2 33 1 2 34 2 2 35 2 1 36 1 1 37 3 2 1 表格查询: SELECT * FROM `t1`; 可以看到,如果界定为 a.b.c 都相同 ...
- LoadRunner参数化详解
LoadRunner参数化详解 距离上次使用loadrunner 已经有一年多的时间了.初做测试时在项目中用过,后面项目中用不到,自己把重点放在了工具之外的东西上,认为性能测试不仅仅是会用工具,最近又 ...
- kvm网络虚拟化(vlan,bond,vlan+bond)(3)
一.Linux Bridge网桥管理 网络虚拟化是虚拟化技术中最复杂的部分,也是非常重要的资源. VM2 的虚拟网卡 vnet1 也连接到了 br0 上. 现在 VM1 和 VM2 之间可以通信,同时 ...
- C#通过Oracle.ManagedDataAccess无法访问Oralce (转)
原文转自:https://www.cnblogs.com/duanjt/p/6955173.html 问题描述:通过C#引用Oracle.ManagedDataAccess.dll访问Oracle,写 ...
- Linux编辑网络连接
Linux编辑网络连接 实验目标: 通过本实验掌握新建网络连接.修改hosts文件.修改主机名的方法. 实验步骤: 1.新建一个名为review的网络连接,并配置ip地址,启用新连接 2.修改ho ...
- OpenStack组件——Nova计算资源管理
1.nova介绍 Nova 是 OpenStack 最核心的服务,负责维护和管理云环境的计算资源.OpenStack 作为 IaaS 的云操作系统,虚拟机生命周期管理也就是通过 Nova 来实现的. ...