JPA Hibernate 修改实体对象同步数据库 脏检查策略源码分析

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。
<class name="com.example.YourEntity" table="your_entity_table" mutable="false">
    <!-- ...其他映射配置... -->
</class>
  • 基于注解的配置:
    对于基于注解的配置方式,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即可

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值