Hibernate/SpringData:使用 AttributeConverter 对字段进行不正确的脏检查

问题描述 投票:0回答:2

我有以下带有自定义

AttributeConverter
的实体,它将数据库中的字段保存为二进制数据。

TaskEntity.java

@Entity
@Table(name = "task")
public class TaskEntity {

   @Id
   @GeneratedValue
   @Column(name = "id", nullable = false)
   private UUID id;

   @Column(name = "state_machine_context")
   @Convert(converter = StateMachineContextConverter.class)
   private StateMachineContext<State, Event> stateMachineContext;
}

StateMachineContextConverter.java

@Converter
public class StateMachineContextConverter
    implements AttributeConverter<StateMachineContext, byte[]> {

   private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
       Kryo kryo = new Kryo();
       kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer());
       kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer());
       kryo.addDefaultSerializer(UUID.class, new UUIDSerializer());
       return kryo;
   });

   private static final int BUFFER_SIZE = 4096;
   private static final int MAX_BUFFERED_SIZE = 10240;

   @Override
   public byte[] convertToDatabaseColumn(final StateMachineContext attribute) {
       return serialize(attribute);
   }

   @Override
   public StateMachineContext convertToEntityAttribute(final byte[] dbData) {
       return deserialize(dbData);
   }

   private byte[] serialize(final StateMachineContext context) {
       if (context == null) {
           return null;
       }
       try (Output output = new Output(BUFFER_SIZE, MAX_BUFFERED_SIZE)) {
           final Kryo kryo = kryoThreadLocal.get();
           kryo.writeObject(output, context);
           return output.toBytes();
       }
   }

   private StateMachineContext deserialize(final byte[] data) {
       if (data == null || data.length == 0) {
           return null;
       }
       final Kryo kryo = kryoThreadLocal.get();
       try (Input input = new Input(data)) {
           return kryo.readObject(input, StateMachineContext.class);
       }
   }
}

因此,在带有

@Transactional
注释的方法中使用 SpringData nativeQuery 选择 TaskEntity 后,将对所有检索到的实体触发UPDATE 查询。

经过调查,我认为这是由于休眠的脏检查而发生的,因为上下文字段是从 byte[] 转换而来的,并且由于某些原因,它被休眠认为是脏的。

有趣的是,制作

@Transactional(readOnly=true)
并没有帮助,因为 Postgres 抛出异常“无法在只读事务中更新”但是如果我完全删除
@Transactional
注释,一切正常,并且选择后不会触发 UPDATE 查询。

解决此问题的最佳解决方案是什么?也许可以禁用只读事务的脏检查?是否可以重写一个字段的休眠脏检查? 我发现可以完全覆盖脏检查 但我不想这样做。

java hibernate spring-data spring-transactions
2个回答
9
投票

当我使用转换器将 JSON 字段从数据库转换为自定义类时,我遇到了同样的问题。 Hibernate 的脏检查策略从持久上下文(从数据库获取对象后立即保存)和当前实体调用实体上的

.equals
方法。

因此重写

StateMachineContext
的 .equals 方法应该可以为你完成。它实际上对我有用。

供参考:https://medium.com/@paul.klingelhuber/hibernate-dirty-checking-with-converted-attributes-1b6d1cd27f68


0
投票

我也遇到了同样的问题,Hibernate 在使用自定义转换器将类保存为 JSON 字段的字段上检测到脏上下文,导致 Hibernate 对每个 SELECT 查询执行不必要的 UPDATE 操作。

@Entity
@DynamicUpdate
@Table(name = "MyEntity")
public class MyEntity {
    
    @Column(name = "MyClass")
    @Convert(converter = MyClassConverter.class) // Converts to JSON string
    private MyClass myClassField;
    
    // Other fields
}

在我的例子中,问题是使用自定义转换器的类没有实现可序列化接口,这将导致 Hibernate 检测到已更改的反序列化对象。

使用自定义转换器将

implements Serializable
添加到类并生成其serialVersionUID 解决了问题。 Hibernate 不再在检索后立即检测到脏上下文,也不再执行不必要的 UPDATE 查询。

public class MyClass implements Serializable {

    private static final long serialVersionUID = 3838227336741360928L;
    // MyClass fields...
}

请记住在任何嵌套类成员的所有类上实现 Serialized,否则当字段序列化发生时你会收到错误。

© www.soinside.com 2019 - 2024. All rights reserved.