Hibernate: Dirty Field Tracking

2.4k Views Asked by At

I am a bit confused right now. I'll try to explain my concern as easy as possible:

I've an Spring Boot app which, of course, has entities. These entities get updated via an thymeleaf form. In this form only some relevant fields are changeable. For example the name of the entity. Other fields like, created, createdby or lastUsedBy are not controlled / changed by this form.

Now heres the problem: If we now change the entity, all other fields are set to null, because they are not in the request. One approach would be to add for these missing fields <input type="hidden"/>inputs. But this not very elegant and error prone. Than I've come to the conclusion, that hibernate should only update those fields which have been changed. So this has to be done via DirtyTracking. I've currently another app, which uses OpenJPA with the openJPA Enhancer and in this app the update only updates the changed fields. My assumption was that the Hibernate enhancer would solve my issue. But even with dirty tracking enabled all fields are updated and information gets lost. I've managed to get it working when I add the @DynamicUpdateannotation to the given entity, but this can't be the right way right?

I double checked that the entities are enhanced and also debugged the whole save process of spring/hibernate. Am I missing something here? Why is hibernate also updating all non-dirty fields?

EDIT

I've checked further and got to this point: The code is from the AbstractEntityTuplizer

public void setPropertyValues(Object entity, Object[] values) throws HibernateException {
    boolean setAll = !entityMetamodel.hasLazyProperties();

    for ( int j = 0; j < entityMetamodel.getPropertySpan(); j++ ) {
        if ( setAll || values[j] != LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
            setters[j].set( entity, values[j], getFactory() );
        }
    }
}

The values object array only has a few values prefilled, only the changed / dirty ones. For example values[5] and values[6]. But ALL setters are called and the values are set to null if they have not been in the values paraemter of the function.. Looks like an bug to me.

3

There are 3 best solutions below

0
On BEST ANSWER

I've found another solution which suits my needs better: The problem was not Hibernate after all but Spring MVC.

Some more code before:

@PutMapping("/{oid}/edit")
public String update(@ModelAttribute("oid") @Valid T entity,
                     BindingResult result,
                     Model model,
                     HttpServletRequest request)

The crucial thing was the @ModelAttribute("oid") here. Before I had not the "oid" in it. So Spring was using the model which was posted from the form, which was obviously not complete filled. Only with the form inputs. With adding the "oid" to the annotation, Spring MVC and Spring Data fetches the entity BEFORE applying the form fields. So than we have an complete Entity, which we can save.

This solution comes close to the second one from @Vlad but omits the cumbersome, manual mapping of changed fields.

Maybe I had to provide more information for others to get the whole picture for my issue! Otherwise, thank you!

1
On

The easiest way is to just merge the modifications onto the entity which is:

  1. Either saved in the HttpSession or in Redis (e.g. Spring Session) between the two HTTP requests. In this case, make sure you merge the detached entity.
  2. Fetched on the second HTTP request based on its identifier

So, once you have the entity, you just set only the properties that you send over the HTTP request, and you let the dirty checking mechanism pick the changes.

This way, you don't need @DynamicUpdate, and this should work with any JPA provider.

3
On

Using @DynamicUpdate is the way to go, when the entity is annotated with @DynamicUpdate, during the update phase, hibernate will not issue an update statement for columns you did not change. This way, created, createdby and lastUsedBy will not be changed when you save your entity.

See: @DynamicUpdate @DynamicInsert