@PreUpdate never called on respository.save()

In a spring-boot 3 app with spring-boot-starter-data-jpa I have an entity:

@Entity
public class SomeJson {

  public static final ObjectMapper MAPPER = new ObjectMapper();

  public static final TypeReference<HashMap<String, Object>> MAP_REF = new TypeReference<HashMap<String, Object>>() {};

  // some fields

  public String jsonData;

  @Transient
  public HashMap<String, Object> data = new HashMap<String, Object>();
  
  @Transient
  private int initialHashCode = 0;

  /**
   * Converts JSON-Data to {@link HashMap}.
   */
  @PostLoad
  public void onLoad() {
    try {
      if (StringUtils.isNotEmpty(jsonData))
        data.putAll( MAPPER.readValue(jsonData, MAP_REF) );
    } catch (JacksonException ignore) {}
    initialHashCode = data.hashCode();
  }

  /**
   * Converts {@link HashMap} to JSON-String if dirty.
   */
  @PreUpdate
  public void onSave() {
    System.out.println("**** " + initialHashCode + " == " + data.hashCode() );
    if( initialHashCode == data.hashCode() ) return;
    try {
      jsonData = MAPPER.writeValueAsString(data);
    } catch (JsonProcessingException e) {}
  }
  
}

and the corresponding repo:

@Repository
public interface SomeJsonRepo extends JpaRepository<SomeJson, Long> {}

The @PostLoad works just fine, but the code

SomeJson sj = someJsonRepo.findById(42);
sj.data.put("lastSuccessfulExport", System.currentTimeMillis());
// sj.onSave(); (1)
someJsonRepo.save(er);
log.info( "Authentication failed" );

does NOT trigger the @PreUpdate method.

If I uncomment the line (1), I see that the onSave() is executed twice:

**** -888829936 == 1673013342
2024-02-19T11:44:27.913+01:00  INFO 18408 --- [nio-8050-exec-1] some.Service : Authentication failed
**** -888829936 == 1673013342

Does JPA/Hibernate have troubles determining whether the instance is dirty and “forgetting” to fire the @PreUpdate?

  • 2

    data is @Transient so doesn’t mark anything dirty so will not update anything.

    – 

  • @M.Deinum how to extend the dirtyness-check to include a @Transient field?

    – 

  • You don’t else it will also be persisted. Why aren’t you just mapping the data field to the jsonData column and use a proper custom type to convert from/to the map. THen you don’t need all those @PostLoad and @PreUpdate mess. Or even better maybe use this which already provides a JsonType.

    – 




  • did it before in another project, and it’s not worth it. JSON custom type is not represented in java as a map, so marshaling must be done by hand

    – 

  • If you can do this manually you can do it in a type, without the callback mess.

    – 

You can use entity listener

@PreUpdate callback method is there you can customize as you wish

Leave a Comment