Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way of finding what was modified by a post in an spring-mvc controller?

It is a rather general question, but I will give a stripped down example. Say I have a Web CRUD application that manages simple entities stored in a database, nothing but classic : JSP view, RequestMapping annotated controller, transactional service layer and DAO.
On an update, I need to know the previous values of my fields, because a business rule asks a for a test involving the old and new values.

So I am searching for a best practice on that use case.

I thing that spring code is way more extensively tested and more robust than my own, and I would like to do it the spring way as much as possible.

Here is what I have tried :

1/ load an empty object in controller and manage the update in service :
Data.java:

class Data {
    int id; // primary key
    String name;
    // ... other fields, getters, and setters omitted for brevity
}

DataController

...
@RequestMapping("/data/edit/{id}", method=RequestMethod.GET)
public String edit(@PathVariable("id") int id, Model model) {
    model.setAttribute("data", service.getData(id);
    return "/data/edit";
}
@RequestMapping("/data/edit/{id}", method=RequestMethod.POST)
public String update(@PathVariable("id") int id, @ModelAttribute Data data, BindingResult result) {
    // binding result tests omitted ..
    service.update(id, data)
    return "redirect:/data/show";
}

DataService

@Transactional
public void update(int id, Data form) {
    Data data = dataDao.find(id);
    // ok I have old values in data and new values in form -> do tests stuff ...
    // and MANUALLY copy fields from form to data
    data.setName(form.getName);
    ...
}

It works fine, but in real case, if I have many domain objects and many fields in each, it is quite easy to forget one ... when spring WebDataBinder has done it including validation in the controller without I have to write any single thing other than @ModelAttribute !

2/ I tried to preload the Data from the database by declaring a Converter
DataConverter

public class DataConverter<String, Data> {
    Data convert(String strid) {
        return dataService.getId(Integer.valueOf(strid));
    }
}

Absolutely magic ! The data if fully initialized from database and fields present in form are properly updated. But ... no way to get the previous values ...

So my question is : what could be the way to use spring DataBinder magic and to have access to previous values of my domain objects ?

like image 321
Serge Ballesta Avatar asked Feb 02 '26 13:02

Serge Ballesta


1 Answers

You have already found the possible choices so i will just add some ideas here ;)

I will start with your option of using a empty bean and copying the values over to a loaded instance:

As you have shown in your example it's an easy approach. It's quite easily adaptable to create a generalized solution.

You do not need to copy the properties manually! Take a look at the 'BeanWrapperImpl' class. This spring object allows you to copy properties and is in fact the one used by Spring itself to achieve it's magic. It's used by the 'ParameterResolvers' for example.

So copying properties is the easy part. Clone the loaded object, fill the loaded object and compare them somehow.

If you have one service or just several this is the way to go.

In my case we needed this feature on each entity. Using Hibernate we have the issue that an entity might not only change inside a specific service call, but theoretically all over the place..

So I decided to create a 'MappedSuperClass' which all entities need to extend. This entity has a 'PostLoad' event listener which clones the entity in a transient field directly after loading. (This works if you don't have to load thousands of entities in a request.) Then you need also the 'PostPersist' and 'PostUpdate' listeners to clone the new state again as you probably don't reload the entity before another modification.

To facilitate the controller mapping I have implemented a 'StringToEntityConverter' doing exactly what you did, just generalized to support any entity type.

Finding the changes in a generalized approach will involve quite a bit of reflection. It's not that hard and I don't have the code available right now, but you can also use the 'BeanWrapper' for that:

Create a wrapper for both objects. Get all 'PropertyDescriptors' and compare the results. The hardest part is to find out when to stop. Compare only the first level or do you need deep comparison?

One other solution could also be to rely on Hibernate Envers. This would work if you do not need the changes during the same transaction. As Envers tracks the changes during a flush and creates a 'Revision' you can "simply" fetch twp revisions and compare them.

In all scenarios you will have to write a comparison code. I'm not aware of a library but probably there is something around in the java world :)

Hope that helps a bit.

like image 182
Martin Frey Avatar answered Feb 05 '26 04:02

Martin Frey



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!