Hibernate脏检查机制源码分析(修改实体属性会自动更新的原理)

个人特别喜欢用jpa体系,新手初次使用JPA/Hibernate时修改某一个实体的属性值不需要手动save或者update,jpa会自动更新实体,你是否会好奇其原理实现?

tips: 文章中并不是一步步读源码,而是从初学者找问题的角度去反找源码

0x01 找源码

1
2
3
//简单demo,这样jpa会自动执行update sql语句
final Group group = this.findById(id);
group.setName("tester");

我们都知道jpa是一个方案,hibernate是其中的一种实现,我们对这个方法打断点f8看源码如何实现的
源码真的很多初次不建议一点点观察,我的方法是一直f8发现哪一句执行之后会调用update语句就代表找到了关键点;
最终我们会找到这一个方法会执行update语句

1
2
3
4
5
6
7
8
9
10
11
12
13
// DefaultFlushEventListener.class
public void onFlush(FlushEvent event) throws HibernateException {
//省略源码...

flushEverythingToExecutions( event );

//下面这一句执行完控制台就会打印update语句
performExecutions( source );

postFlush( source );

//省略源码...
}

再依次进去方法里面查找实际执行处,actionQueue.executeActions(); -> executeActions( l ); -> e.execute() -> EntityUpdateAction.execute() -> persister.update(...);
直到发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//AbstractEntityPersister
//我们通过方法名和参数称可以大概猜到,
// 返回:需要更新字段的下标数组 方法名:获取表需要更新的字段 参数:脏字段,是否有脏集合
final boolean[] tableUpdateNeeded = getTableUpdateNeeded( dirtyFields, hasDirtyCollection );

//省略代码

for ( int j = 0; j < span; j++ ) {
// Now update only the tables with dirty properties (and the table with the version number)
if ( tableUpdateNeeded[j] ) {
//终于找到实际更新的地方了,这儿会根据id 字段名,updateStrings[j]并根据传入的sql执行对应的修改或新增操作
updateOrInsert(
id,
fields,
oldFields,
j == 0 ? rowId : null,
propsToUpdate,
j,
oldVersion,
object,
updateStrings[j],
session
);
}
}

我们发现hibernate的更新操作实际去基于一个叫dirtyFields的东西来标记的,我们现在只需要找到ditryFields是在哪儿wirte进来的就能找到脏字段检测原理

我们找到dirtyFields的类EntityUpdateAction,并右键dirtyFields -> Find Usages 并打开idea弹出的usage窗口中value write写入值的地方,发现是传参进来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public EntityUpdateAction(
final Serializable id,
final Object[] state,
final int[] dirtyProperties,
final boolean hasDirtyCollection,
final Object[] previousState,
final Object previousVersion,
final Object nextVersion,
final Object instance,
final Object rowId,
final EntityPersister persister,
final SharedSessionContractImplementor session) {
super( session, id, instance, persister );
this.dirtyFields = dirtyProperties;

并继续寻找EntityUpdateAction方法的调用处发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 通过event获取到我们得核心dirty参数, 我们接着从event下手
int[] dirtyProperties = event.getDirtyProperties();
//省略代码
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
)
);

接着打开event的类,FlushEntityEvent并右键字段dirtyProperties -> find Usages 找到value wirte写入的地方,发现两处

1
2
3
4
5
6
//1  完全就是一个set方法没有任何用
public void setDirtyProperties(int[] dirtyProperties) {
this.dirtyProperties = dirtyProperties;
}
//2 这儿感觉有点意思了 打开这块的代码
event.setDirtyProperties( dirtyProperties );

发现传入核心参数的地方dirtyProperties,它是通过persister.findDirty( values, loadedState, entity, session );这个方法构造出来的,这就是核心点
我们再依次进入findDirty()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int[] findDirty(Object[] currentState, Object[] previousState, Object entity, SharedSessionContractImplementor session) throws HibernateException {
//我们的dirty脏字段就是从这儿返回回来的
int[] props = TypeHelper.findDirty(
entityMetamodel.getProperties(),
currentState,
previousState,
propertyColumnUpdateable,
session
);
if ( props == null ) {
return null;
}
else {
logDirtyProperties( props );
return props;
}
}

进入TypeHepler.findDirty就清楚了原理(最好打个断点自己再调试深入理解一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;

//循环所有的字段 其中核心就是isDirty方法往下看👇
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);
}
}

//👇👇👇👇👇👇👇👇👇
//参数 旧值 当前值
@Override
public final boolean isDirty(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) {
return checkable[0] && isDirty( old, current );
}
//方法调用
protected final boolean isDirty(Object old, Object current) {
return !isSame( old, current );
}

//核心就是判断两个值是否相等,如果不相等就说明修改过了,则放入dirtyFields中并只更新这几个字段
@Override
public final boolean isSame(Object x, Object y) {
return isEqual( x, y );
}

最终看到了内部逻辑,核心就是判断两个值是否相等,如果不相等就说明修改过了,则放入dirtyFields中并只更新这几个字段;

0x02 总结

当一个实体对象被加入到Session缓存中时,Session会为实体对象的值类型的属性复制一份快照。当Session清理缓存时,会先进行脏检查,即比较实体对象的当前属性与它的快照,来判断实体对象的属性是否发生了变化,如果发生了变化,就称这个对象是“脏对象”,Session会根据脏对象的最新属性来执行相关的SQL语句,从而同步更新数据库。