Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate/SpringData : Incorrect dirty check on field with AttributeConverter

I have below entity with custom AttributeConverter which saves field in the DB as binary data.

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

So after TaskEntity is selected with SpringData nativeQuery within method with @Transactional annotation UPDATE queries are fired for all retrieved entities.

After investigation I suppose it is happened because of dirty checking of hibernate, as context field is converted from byte[] and for some reasons it is considered dirty by hibernate.

The interesting thing is that making @Transactional(readOnly=true) does not help as Postgres is throwing exception "Could not UPDATE in readOnly transaction" but if I remove @Transactional annotation completely everything works fine and UPDATE queries are not fired after select.

What is the best solution to fix this issue? Maybe it is possible to disable dirty checking for readOnly transactions? Is it possible to rewrite hibernate dirty check for one field? I found that it is possible to overwrite dirty checking completely but I would prefer not to do this.

like image 680
Aliaksei Stadnik Avatar asked Nov 14 '17 12:11

Aliaksei Stadnik


1 Answers

I faced the same issue when I was using a convertor to convert my JSON field from DB to my custom class. The Dirty checking policy of Hibernate calls the .equals method on the entity from the Persistent Context (which is saved as soon as you fetch an object from DB) and your current Entity.

So overriding the .equals method of StateMachineContext should do it for you. It actually worked for me.

For reference : https://medium.com/@paul.klingelhuber/hibernate-dirty-checking-with-converted-attributes-1b6d1cd27f68

like image 67
sumitb.mdi Avatar answered Nov 03 '22 23:11

sumitb.mdi