4

Question: How to check which fields has been changed inside method annotated with @PreUpdate?


OPTIONAL: if the answer to the question above is "It's impossible, than maybe there are another ways to solve my problem"

I want automatically update modified Tourist's field each time we change something in it.

Except the situation when we modify only location. Means if we change location only - it should be persisted, but modified mustn't be changed.

Already present code:

@Entity
public class Tourist {

  private long id;
  private String firstName;
  private String lastName;
  private Date created;
  private Date modified;
  private String location;

  @PreUpdate
  public void preUpdate() {
     modified = new Date(); //PROBLEM : change modified even if only location field has been changed!
  }   
  ....
}

Updated: After some investigations I found that I can solve it with help of interceptors (extend EmptyInterceptor):

public class TouristInterceptor extends EmptyInterceptor{

    Session session;
    private Set updates = new HashSet();

    public void setSession(Session session) {
        this.session=session;
    }

    public boolean onFlushDirty(Object entity,Serializable id,
        Object[] currentState,Object[] previousState,
        String[] propertyNames,Type[] types)
        throws CallbackException {

        if (entity instanceof Tourist){
            if (somethingChangedExceptLocation()) 
                updates.add(entity);
        }
        return false;
    }

But disadvantage of this approach is to intercept everything when you need to intercept the single entity.

Updated Questions:

  • How to intercept only Tourist entity flush calls?
  • Is that possible to do the same with help of events? Means PreUpdateEvent which contains new and old state
VB_
  • 45,112
  • 42
  • 145
  • 293
  • 2
    Why don't you use a transient field where you store previous location, and at the preUpdate you test it before you update the `modified` field? – Hichamov Dec 18 '14 at 14:03
  • I recommend you to create a side class where you'd keep track of events (creation, modification, etc), and update it at Business level not DAO level – Hichamov Dec 18 '14 at 14:06
  • @Hichamov I really like your idea about extra transient field! – VB_ Dec 18 '14 at 14:07
  • @Hichamov but I need to check if another fields aren't changed. Mode details please – VB_ Dec 18 '14 at 14:08
  • Sorry man, I never worked with one before. I may say something wrong – Hichamov Dec 18 '14 at 15:00

3 Answers3

5

There is a simple non-JPA solution which is as follows but which which does have some repetitive code but is a solution when you do not have too many fields:

@Entity
public class Tourist {

    private long id;
    private String firstName;
    private String lastName;
    private Date created;
    private Date modified;
    private String location;

    public void setFirstName(String firstName){
        if(! this.firstName.equals(firstName){
            modified = new Date();
        }

        this.firstName = firstName;
    }

    public void setLastName(String lastName){
        if(! this.lastName.equals(lastName){
            modified = new Date();
        }

        this.lastName= lastName;
    }
}

Otherwise I would go with saving the previous state on load as suggested in another answer but in a slightly cleaner way.

@Entity
public class Tourist {

  private long id;
  private String firstName;
  private String lastName;
  private Date created;
  private Date modified;
  private String location;
  
  @Transient
  private Tourist previousState;

  @PostLoad 
  public void setPreviousState() {
    previousState = new Tourist();
    //copy fields
  }
  
  @PreUpdate
  public void preUpdate() {
     if (isModified()) {
        modified = new Date();
     }
  }   
  
  private boolean isModified() {
    boolean modified = false;
    
    if (!firstName.equals(previousState.firstName) {
        modified = true;
    }
    
    //check other fields
    
    return modified;
  }
}
NealeU
  • 1,264
  • 11
  • 23
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • 1
    I've got about twenty fields((. – VB_ Dec 18 '14 at 14:42
  • You could look at using BeanUtils to copy the data on load to the transient instance and on save to compare the two beans: http://stackoverflow.com/questions/6099040/find-out-the-differences-between-two-java-beans-for-version-tracking – Alan Hay Dec 18 '14 at 14:45
  • may you look at updated section of my question please? I have some ideas – VB_ Dec 18 '14 at 14:50
  • 1
    I stopped worrying about twenty fields and started typing them. @VB_ – nurettin Jan 10 '19 at 10:38
0

Although it is much late, however I enountered a problem where I required to apply update security on certain fields in entity. I solved it using Reflections. Have a look at this thread. spring security for certain entity variables

ArslanAnjum
  • 1,674
  • 2
  • 17
  • 31
-1

I really encourage you not to do this logic in your Entity !

You should decide whether to change the modified or not at your business logic.

I mean, when you are changing only the location call merge only. And whenever you are changing something else call the <Tourist_instance>.setModified(new Date()); before you call the merge.


To verify if anything else got changed, I suggest having a transient boolean field in your Entity that you set to true whenever you change something else other than location (the idea in the comment won't be sufficient to test all the fields, only if you create a transient field to remember previous value of each one of them)

You will then test this boolean in your preUpdate method

But I highly don't recommend this workaround.


Or, another highly unrecommended way :

@Entity
public class Tourist {

  private long id;
  private String firstName;
  private String lastName;
  private Date created;
  private Date modified;
  private String location;

  @Transient 
  private String firstNamePrevious;
  @Transient 
  private String lastNamePrevious;
  @Transient 
  private Date createdPrevious;
  @Transient 
  private Date modifiedPrevious;
  @Transient 
  private String locationPrevious;

  @PreUpdate
  public void preUpdate() {
     if(!onlyLocationGotChanged()){
             modified = new Date(); 
     }
  }   
  ....

  @PostLoad
  public void postLoad(){
      //Set all previous fields to actual values
      firstNamePrevious = firstName;
      lastNamePrevious = lastName;
      //...etc.
  }
}
Hichamov
  • 636
  • 6
  • 8
  • you're right, but the app is so big, that I can't find all usages for sure. Or force everybody to save `Tourist` entity in right way. So I'm looking for some universal approach. What do you think about EmtpyInterceptor#onFlushDirty http://stackoverflow.com/questions/1927098/hibernate-save-the-previous-state-of-an-object? – VB_ Dec 18 '14 at 14:23
  • 6
    The proposal to do it in business logic fails to encapsulate the operation. Every single client of this class then needs to be aware that it should also update the modified date which is very bad! – Alan Hay Dec 18 '14 at 14:29
  • I don't really understand what is it about, since I never used it. But the `Object[] newValues` & `Object[] oldValues` seem pretty handy – Hichamov Dec 18 '14 at 14:30
  • @AlanHay I totally agree (and I am upvoting you), that is why I kept the other suggestions. I just wanted to point it out in case it could be doable for V_B – Hichamov Dec 18 '14 at 14:32
  • Also note that the transient keyword here has no effect in a JPA environment. What you are looking for is the @Transient annotation. – Alan Hay Dec 18 '14 at 14:41
  • Could you please explain? @AlanHay – Hichamov Dec 18 '14 at 14:41
  • http://stackoverflow.com/questions/2154622/why-does-jpa-have-a-transient-annotation – Alan Hay Dec 18 '14 at 14:42
  • I am pretty sure that the transient (as I wrote it) has the wanted effect (just had a maintenance task this week on it), but I edit anyway – Hichamov Dec 18 '14 at 14:47
  • @Hichamov may you look at the updated section of my question please? – VB_ Dec 18 '14 at 14:51