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即可