1.为什么我在查出数据实体对象或集合后,修改了实体对象或集合会同步到数据库,难道是代码有bug? 如果你正在使用或使用过JPA作为项目的ORM框架,尤其是用习惯了mybatis突然使用JPA时,肯定会遇到这样的问题,场景类似如下:

@RestController

@Slf4j

public class OrderController {

@Autowired

private OrderService orderService;

@GetMapping("/dirtyCheck")

@Transactional

public void dirtyCheck() {

Order order = orderService.getOrder(1L);

log.info(order.toString());

order.setCustomerName("阿祖");

}

}

操作日志如下,可以看见在打印后执行了一条更新sql:

Hibernate: select order0_.id as id1_1_0_, order0_.customer_name as customer2_1_0_, order0_.version as version3_1_0_ from orders order0_ where order0_.id=?

2023-05-26 15:39:14.615 INFO 6772 --- [nio-8080-exec-1] com.study.controller.OrderController : Order{id=1, customerName='冠希哥', version=1}

Hibernate: update orders set customer_name=?, version=? where id=? and version=?``

数据库表字段变了 于是有这样的疑问,我明明只修改了实体类,为什么数据库的结果也发生了变化?其实这是JPA的优化策略,叫脏检查dirtycheck,让我们往下看

2. 什么是脏检查,为什么要进行脏检查? JPA是Java Persistence API 持久化API的缩写,当项目中使用JPA作为ORM框架,会将Java对象和关系型数据库中的表进行映射。JPA中的脏检查是一种优化策略,用于检测实体是否被修改。如果实体被修改,即被认为是“脏”的,那么在事务提交时,JPA会发送相应的更新SQL语句,以同步持久化上下文中的更改。

3.对象在hibernate中的几种状态 要理解这块,先让让我们先搞清对象在hibernate中的几种状态: TRANSIENT(瞬时状态):Transient 对象是指刚刚创建的对象,它尚未与数据库关联(即未持久化)。在这个状态下,对象在数据库中没有对应的记录。例如,当你使用 Java 的 new 关键字创建一个实体对象时,它处于 Transient 状态。

PERSISTENT(持久化状态):Persistent 对象是与数据库关联的对象(即已持久化),在数据库中有相应的记录。这种对象由 Hibernate 的 Session 托管和维护。在持久化状态下的对象,任何更改都会被 Hibernate 跟踪,并在必要时自动更新到数据库中。

DETACHED(脱管状态):Detached 对象曾经处于 Persistent 状态,但在某个时刻,Session 关闭了,这样 Hibernate 就不再管理这些对象。Detached 对象在数据库中有对应的记录,但更改它们的属性不会自动同步到数据库中。

DELETED(删除状态):对象被删除了。

脏检查的操作正是在对象为持久化状态时,进行的一项优化操作,保持实体对象与数据库表的统一

4.源码解析(以hibernate5.4版本为例) 脏检查的源码在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 );

// 获取实体类的属性值,存储在values数组中

final Object[] values = getValues( entity, entry, mightBeDirty, session );

event.setPropertyValues( values );

//TODO: avoid this for non-new instances where mightBeDirty==false

// 为实体的集合属性(例如 Set、List 等)创建包装器(wrapper),以便在更新集合时触发脏检查和级联操作。

boolean substitute = wrapCollections( session, persister, entity, entry.getId(), 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

// 如果实体具有集合属性,则处理实体的集合属性。FlushVisitor 类会遍历实体的集合属性,并更新它们的关联关系和持久化状态。

if ( persister.hasCollections() ) {

new FlushVisitor( session, entity ).processEntityPropertyValues( values, types );

}

}

}

其中看下requiresDirtyCheck方法

public boolean requiresDirtyCheck(Object entity) {

// 实体可以被修改,且实体不是非脏的

return isModifiableEntity()

&& ( !isUnequivocallyNonDirty( entity ) );

}

public boolean isModifiableEntity() {

final Status status = getStatus();

final Status previousStatus = getPreviousStatus();

// 1. 实体可变

// 2. 实体状态不是只读

// 3. 实体状态是删除,且之前的状态不是只读

return getPersister().isMutable()

&& status != Status.READ_ONLY

&& ! ( status == Status.DELETED && previousStatus == Status.READ_ONLY );

}

private boolean isUnequivocallyNonDirty(Object entity) {

// 字节码增强,可忽略

if ( entity instanceof SelfDirtinessTracker ) {

...

}

// 字节码增强,可忽略

if ( entity instanceof PersistentAttributeInterceptable ) {

...

}

// 自定义脏检查策略

final CustomEntityDirtinessStrategy customEntityDirtinessStrategy =

getPersistenceContext().getSession().getFactory().getCustomEntityDirtinessStrategy();

if ( customEntityDirtinessStrategy.canDirtyCheck( entity, getPersister(), (Session) getPersistenceContext().getSession() ) ) {

return ! customEntityDirtinessStrategy.isDirty( entity, getPersister(), (Session) getPersistenceContext().getSession() );

}

// 持久化类有可变属性(如集合、数组等)

if ( getPersister().hasMutableProperties() ) {

return false;

}

return false;

}

ok,这样完成判断对象是否需要脏检查,下面需要判断对象是否真的脏了,额,这句话怎么这么有点恶心心的。 其中最主要的是isUpdateNecessary方法中的dirtyCheck方法,方法内,在没有字节码增强和自定义脏价差策略的情况下,将会使用hibernate的脏检查策略进行脏检查,核心代码如下:

try {

session.getEventListenerManager().dirtyCalculationStart();

interceptorHandledDirtyCheck = false;

// object loaded by update()

// 如果实体的加载状态(loadedState)不为null,则说明实体对象是由update()方法加载的。在这种情况下,代码通过比较实体当前属性值(values)和加载时的属性值快照(loadedState)来进行脏检查,并获取脏属性数组(dirtyProperties)

dirtyCheckPossible = loadedState != null;

if (dirtyCheckPossible) {

// dirty check against the usual snapshot of the entity

// 比较实体当前属性值(values)和加载时的属性值快照(loadedState)来进行脏检查,并获取脏属性数组(dirtyProperties)。

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

// 代码会尝试获取实体在数据库中的快照(databaseSnapshot),然后通过比较实体当前属性值(values)和数据库快照来进行脏检查。如果数据库快照不为null,则将其存储在事件对象中(event.setDatabaseSnapshot(databaseSnapshot)),以便后续处理

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);

}

1.当前实体属性值跟加载时的属性快照进行对比 2.当前实体属性值跟数据库快照进行对比 而不管是findDirty还是findModified,都是同样的想法,遍历属性值,找出脏的属性,并将脏属性的index存储到数组中,以便后续更新操作时进行指定属性的更新。代码如下:

public static int[] findDirty(

final NonIdentifierAttribute[] properties,

final Object[] currentState,

final Object[] previousState,

final boolean[][] includeColumns,

final SharedSessionContractImplementor session) {

int[] results = null;

int count = 0;

int span = properties.length;

for ( int i = 0; i < span; i++ ) {

final boolean dirty = currentState[i] != LazyPropertyInitializer.UNFETCHED_PROPERTY &&

( previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ||

( properties[i].isDirtyCheckable()

&& properties[i].getType().isDirty( previousState[i], currentState[i], includeColumns[i], session ) ) );

if ( dirty ) {

if ( results == null ) {

results = new int[span];

}

results[count++] = i;

}

}

if ( count == 0 ) {

return null;

}

else {

return ArrayHelper.trim(results, count);

}

}

至此,脏检查操作完成,找到了脏的对象及脏的属性值,再调用scheduleUpdate方法,将实体对象的更新操作计划到 Hibernate 的更新队列中,在事务提交或刷新会话时执行更新操作。 5.那如果不想脏检查怎么设置呢? (1)将实体设置成只读。上述源码中,会先判断实体是否需要进行脏检查,而内部就是判断实体是否为Status.READ_ONLY,有几种方式可以设置实体只读

设置Session默认只读: session.setDefaultReadOnly(true); 使用此方法将整个Session的默认只读状态设置为true。这意味着在此Session中加载的所有实体都将被标记为只读,Hibernate将不会跟踪它们的更改。 设置特定实体为只读: session.setReadOnly(entity, true); 使用此方法将指定的实体设置为只读。这意味着Hibernate将不会跟踪此实体的更改。请注意,如果在同一个会话中查询同一实体,这个标记会被保留,而不仅仅是对当前查询生效。 设置查询实体为只读: Query query = session.createQuery(“FROM MyEntity”); query.setReadOnly(true); List entities = query.list(); 使用此方法将查询结果设置为只读。这意味着从此查询中加载的实体将被标记为只读,Hibernate将不会跟踪它们的更改。 设置Criteria查询实体为只读: Criteria criteria = session.createCriteria(MyEntity.class); criteria.setReadOnly(true); List entities = criteria.list(); 使用此方法将Criteria查询结果设置为只读。这意味着从此查询中加载的实体将被标记为只读,Hibernate将不会跟踪它们的更改。

(2)事务设置只读 如果使用的是事务注解,则标记为如下。 这样虽然会进行脏检查,但不会把结果同步到数据库,但这时候要注意最外层的事务设置的传播行为,这个是另外的知识点,暂不在此处涉及

@Transactional(readOnly = true)

(3)查出的实体或集合转DTO 将查询的结果转成DTO,但需要注意,如果你的实体内有其他实体,也需要建对应的DTO转换才行,为什么?DTO对象与实体对象之间不共享任何引用或地址(要了解JVM的基础知识)。举例如下: 实体类 User:

@Entity

public class User {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;

private String username;

private String email;

@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)

private UserProfile userProfile;

// 省略构造函数、getter和setter方法

}

实体类 UserProfile:

@Entity

public class UserProfile {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;

private String firstName;

private String lastName;

@OneToOne

@JoinColumn(name = "user_id")

private User user;

// 省略构造函数、getter和setter方法

}

创建对应的DTO类 UserDTO:

public class UserDTO {

private Long id;

private String username;

private String email;

private UserProfileDTO userProfileDTO;

// 省略构造函数、getter和setter方法

}

创建对应的DTO类 UserProfileDTO:

public class UserProfileDTO {

private Long id;

private String firstName;

private String lastName;

// 注意:这里没有包含UserDTO,因为我们通常避免循环引用

// 省略构造函数、getter和setter方法

}

转换方法 User 到 UserDTO:

public static UserDTO convertToUserDTO(User user) {

UserDTO userDTO = new UserDTO();

userDTO.setId(user.getId());

userDTO.setUsername(user.getUsername());

userDTO.setEmail(user.getEmail());

UserProfile userProfile = user.getUserProfile();

if (userProfile != null) {

UserProfileDTO userProfileDTO = new UserProfileDTO();

userProfileDTO.setId(userProfile.getId());

userProfileDTO.setFirstName(userProfile.getFirstName());

userProfileDTO.setLastName(userProfile.getLastName());

userDTO.setUserProfileDTO(userProfileDTO);

}

return userDTO;

}

总之,将实体转换为DTO时,需要针对基本数据类型、String以及关联的实体分别进行处理。对于关联的实体,需要创建相应的DTO类并进行逐层的转换。这样可以确保脏检查不会被触发,同时保证数据的正确性和一致性。 (4)使用原生sql而不用JPA的sql方言查询 因为原生SQL直接操作数据库,不会对持久化实体进行更改,因此不会触发JPA的脏检查机制。这个自己试下吧:) (5)手动将Persistent(持久化状态)变成Detached(脱管状态)

import javax.persistence.EntityManager;

import javax.persistence.EntityManagerFactory;

import javax.persistence.Persistence;

public class EntityManagerDetachExample {

public static void main(String[] args) {

EntityManagerFactory emf = Persistence.createEntityManagerFactory("example");

EntityManager em = emf.createEntityManager();

// 开始事务

em.getTransaction().begin();

// 获取一个持久化实体

User user = em.find(User.class, 1L);

// 将实体分离

em.detach(user);

// 对实体进行修改

user.setUsername("updated_username");

// 提交事务

em.getTransaction().commit();

// 关闭EntityManager

em.close();

emf.close();

}

}

(6)分离实体:当实体处于分离状态时,它不会被Hibernate管理,因此不会进行脏检查。要分离实体,只需关闭与实体关联的Session或将实体从Session中清除。

session.close(); // 关闭Session

或者

session.evict(entity); // 将实体从Session中清除

(7)配置文件或者注解设置实体不可变(一般不设置,有场景如日志表等不可变更的表时,可进行设置)

配置映射文件(hbm.xml文件): 在映射文件中,您可以为元素设置mutable属性来指定实体类是否可变。默认值是true,表明实体是可变的。要将实体设置为不可变,请将mutable属性设置为false。

基于注解的配置: 对于基于注解的配置方式,Hibernate本身没有提供一个直接的注解来设置实体的可变性。但是,您可以实现一个自定义的Interceptor或EntityListener来模拟实体的不可变性。 例如,您可以在实体类上添加@Immutable注解,然后在自定义的Interceptor或EntityListener中检查此注解,并阻止对带有@Immutable注解的实体进行修改。 以下是一个使用@Immutable注解的示例: import javax.persistence.Entity;

import org.hibernate.annotations.Immutable;

@Entity

@Immutable

public class YourEntity {

// ...实体属性和方法...

}

本文部分内容借助chatgpt查询展示,发现chatgpt在帮助学习源码方面的确很有用!如果您对技术有兴趣,愿意友好交流,可以加v进技术群一起沟通,vx:zzs1067632338,备注csdn即可

参考文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。