6

I need something that seems not so specific but anyway I was unable to come up with nice and sophisticated solution.

Say I have very simple hibernate/jpa entity:

@Entity(name="entity")
public class Type {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @Column(unique = true, nullable = false)
    private String name;

    @Column(unique = false, nullable = false)
    private boolean defaultType;
}

What i need is to somehow annotate defaultType field so only (and exactly) one persisted entity have this value as true. When new entity get persisted with this defaultType as true, the old one (with defaultType=true) entity has to be altered and its defaultType value changed to false. Also if any entity get changed (its defaultType got changed to true), same rule should apply.

As far I know this can be achieved inside business logic (e.g. in DAO layer), with DB trigger or with hibernates interceptor or event (If there is another way, please let me know). I tried with DAO solution but it's kind of bad solution because it can be bypassed and it is really clumsy for such simple operation. DB triggers can not be added with hibernate/jpa annotations (if I am not mistaken) and i am not sure how to make this functionality with hibernate interceptors/events.

So, what is best solution for this problem?

Andrija
  • 367
  • 3
  • 14
  • Its been answered, but I want to add: there is a really good reason to want to have the constraint in the database; then the data is guarded no matter how the data ends up in the database. If you solve this from the perspective of JPA, then the guard is only in place when using JPA. Just a thought, I don't know your environment, current requirements or future requirements. – Gimby Dec 08 '15 at 22:40

1 Answers1

1

You need use Callback method in JPA, for example PreUpdate or PostUpdate, for instance:

@Entity
@EntityListeners(com.acme.AlertMonitor.class) // set callback method in another class
public class Account {
   Long accountId;
   Integer balance;
   boolean preferred;

   @Id
   public Long getAccountId() { ... }
   ...
   public Integer getBalance() { ... }
    ...
   @Transient 
   public boolean isPreferred() { ... }
   ...

   public void deposit(Integer amount) { ... }
   public Integer withdraw(Integer amount) throws NSFException {... }

   @PreUpdate // callback method in some class 
   protected void validateCreate() {
     if (getBalance() < MIN_REQUIRED_BALANCE)
        throw new AccountException("Insufficient balance to open an
account");
   }

  @PostUpdate  // callback method in some class 
  protected void adjustPreferredStatus() {
      preferred =
      (getBalance() >= AccountManager.getPreferredStatusLevel());
   }
}

// callback method in another class 
public class AlertMonitor {
   @PreUpdate  // callback method in another class
   public void updateAccountAlert(Account acct) {
      Alerts.sendMarketingInfo(acct.getAccountId(), acct.getBalance());
   }
}

Update: About your question, If I undestand what you want, this code may help you:

@Entity(name="entity")
@EntityListeners(com.yourpackage.TypeListner.class)
public class Type {

    ...
@Column(unique = false, nullable = false)
private boolean defaultType;
}

public class TypeListner {

pivate static Type objectWithTrue = null;

public void init() { // call this method when application is started 
    List<Type> results = entityManager
                  .createQuery("from Type", Type.class)
                  .getResultList();
    for(Type type: results) {
         if(type.getDefaultType()) {
             objectWithTrue = type;      
         }  
    }
}

private void changeDefaultType(Type changed) {
    if(changed.getDefaultType()) {
       if(changed != objectWithTrue && objectWithTrue != null) {        
         objectWithTrue.setDefaultType(false);
       }
       objectWithTrue = changed;
   }
}

@PostPresist  
public void newType(Type changed) {
   changeDefaultType(changed);
}

@PostUpdate
public void updateType(Type changed) {
   changeDefaultType(changed);
}

@PreRemove
public void removeType(Type changed) {
if(changed.getDefaultType() && objectWithTrue == changed) {
      objectWithTrue = null;
    }
}

OR

You can use listner @PreUpdate and @PrePresist and every times overwrite all Type objects without store any variable (it isn't so good for perfomance then first example, but more reliable):

@PreUpdate 
void updateType(Type changed) {
    if(changed.getDefaultType()
        List<Type> results = entityManager
                  .createQuery("from Type", Type.class)
                  .getResultList();
       for(Type type: results) {
           if(changed != type && type.getDefaultType()) {
             type.setDefaultType(false);    
           }  
       }
    }
 }
Slava Vedenin
  • 58,326
  • 13
  • 40
  • 59
  • Could you adapt your example to the question? From my point of view it is difficult to implement this using `@PreUpdate`. And `@PostUpdate` won't help here, as at that point the wrong update has already happened. And you should mention `@PrePersist` as well. – Tobias Liefke Dec 09 '15 at 09:39
  • Not exactly. There are some things that are problematic with your solution: The initialization is missing (after a restart of your webapp the `objectWithTrue` will be `null`). `changeDefaultType()` will have no effect, as `objectWithTrue` is only set if it is `!= null`, which it never will be. And even if you fix this, `objectWithTrue.setDefaultType(false);` will have no effect as your static instance is detached as soon as the session ends (which just happened if you call it in `PostUpdate`). – Tobias Liefke Dec 09 '15 at 12:00
  • What problem to add init() method that get all Type objects and find where deftype = true and call this method when application started? – Slava Vedenin Dec 09 '15 at 12:03
  • Add this init() example to last sample – Slava Vedenin Dec 09 '15 at 12:07
  • What about the other two things I mentioned? These still apply. All I'm saying is that it is difficult to implement this with `EntityListeners`. – Tobias Liefke Dec 09 '15 at 15:03
  • "will have no effect as your static instance is detached as soon as the session ends" - use new session and merge method – Slava Vedenin Dec 09 '15 at 15:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/97420/discussion-between-viacheslav-vedenin-and-tobias-liefke). – Slava Vedenin Dec 09 '15 at 15:16
  • All solutions are basically same as logic eventually implemented in DAO or EJB layer - if new entity have special field as true, get all existing entities, find if any has special field as true, set it to false and update it, then persist new entity. Similar for update process. Also, solution with PreUpdate, PrePresist and alike is useful only if we are using EntityManager, but it doesn't work for e.g. hibernate session. Does it mean there is no simple and compact solution for this? – Andrija Dec 12 '15 at 17:47
  • You can use hibernate event. if you wish to work with hibernate session (about hibernate event: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/events.html , example of using http://docs.jboss.org/hibernate/core/3.3/reference/en/html/events.html). – Slava Vedenin Dec 12 '15 at 17:59
  • Also you can find more info in http://stackoverflow.com/a/4133629/4318868 this question – Slava Vedenin Dec 12 '15 at 18:01