views:

168

answers:

2

If i have a @OneToMany relationship with @Cascade(CascadeType.SAVE_UPDATE) as follows

public class One {

    private Integer id;

    private List<Many> manyList = new ArrayList<Many>();

    @Id
    @GeneratedValue
    public Integer getId() {
        return this.id;
    }

    @OneToMany
    @JoinColumn(name="ONE_ID", updateable=false, nullable=false)
    @Cascade(CascadeType.SAVE_UPDATE)
    public List<Many> getManyList() {
        return this.manyList;
    }        

}

And Many class

public class Many {

    private Integer id;

    /**
      * required no-arg constructor
      */ 
    public Many() {}

    public Many(Integer uniqueId) {
        this.id = uniqueId
    }

    /**
      * Without @GeneratedValue annotation
      * Hibernate will use assigned Strategy
      */ 
    @Id
    public Integer getId() {
        return this.id;
    }

}

If i have The following scenario

One one = new One();

/**
  * generateUniqueId method will Take care of assigning unique id for each Many instance
  */
one.getManyList().add(new Many(generateUniqueId()));
one.getManyList().add(new Many(generateUniqueId()));
one.getManyList().add(new Many(generateUniqueId()));
one.getManyList().add(new Many(generateUniqueId()));

And i call

sessionFactory.getCurrentSession().save(one);

Before going on

According to Transitive persistence Hibernate reference documentation, you can see

If a parent is passed to save(), update() or saveOrUpdate(), all children are passed to saveOrUpdate()

ok. Now Let's see what Java Persistence With Hibernate book Talks about saveOrUpdate method

Hibernate queries the MANY table for the given id, and if it is found, Hibernate updates the row. If it is not found, insertion of a new row is required and done.

Which can be translated according to

INSERT INTO ONE (ID) VALUES (?)

/**
  * I have four Many instances added To One instance
  * So four select-before-saving
  *
  * I DO NOT NEED select-before-saving 
  * Because i know i have a Fresh Transient instance
  */
SELECT * FROM MANY WHERE MANY.ID = ?
SELECT * FROM MANY WHERE MANY.ID = ?
SELECT * FROM MANY WHERE MANY.ID = ?
SELECT * FROM MANY WHERE MANY.ID = ?

INSERT INTO MANY (ID, ONE_ID) VALUES (?, ?)
INSERT INTO MANY (ID, ONE_ID) VALUES (?, ?)
INSERT INTO MANY (ID, ONE_ID) VALUES (?, ?)
INSERT INTO MANY (ID, ONE_ID) VALUES (?, ?)

Any workaround To avoid select-before-saving ??? Yes, You can either

  • Add a @Version column (Not applied)
  • Implement isTransient method provided by Hibernate interceptor (The option i have)

So as a way to avoid select-before-saving default behavior when using this kind of cascading, i have improved my code by assigning a Hibernate Interceptor to a Hibernate Session whose Transaction is managed by Spring.

Here goes my repository

Before (Without any Hibernate Interceptor): It works fine!

@Repository
public class SomeEntityRepository extends AbstractRepository<SomeEntity, Integer> {

    @Autowired
    private SessionFactory sessionFactory;

    @Override
    public void add(SomeEntity instance) {
        sessionFactory.getCurrentSession().save(instance);
    }

}

After (With Hibernate Inteceptor): something goes wrong (No SQL query is performed - Neither INSERT Nor SELECT-BEFORE-SAVING)

@Repository
public class SomeEntityRepository extends AbstractRepository<SomeEntity, Integer> {

    @Autowired
    private SessionFactory sessionFactory;

    @Override
    public void add(SomeEntity instance) {
        sessionFactory.openSession(new EmptyInterceptor() {
            /**
              * To avoid select-before-saving
              */
            @Override
            public Boolean isTransient(Object o) {
                return true;
            }
        }).save(instance);
    }

}

My question is: Why Spring does not persist my Entity and its relationships when using Hibernate Interceptor and what should i do as workaround to work fine ???

A: 

In the 'after' method you are creating a new session and not flushing it, therefore no update is sent to the database. This has nothing to do with Spring, but is pure Hibernate behavior.

What you probably want is adding an (entity) interceptor to the sessionFactory, probably configured using Spring. You can then just keep your repository's add() method as before. See http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/orm/hibernate3/LocalSessionFactoryBean.html#setEntityInterceptor%28org.hibernate.Interceptor%29

Fried Hoeben
@Fried Hoeben As said: *You can add an Interceptor to the SessionFactory*. It happens whether i add an Interceptor to the SessionFactory, i will get a **global** behavior. I just want to add an Interceptor to the Session instance which is used by add(SomeEntity instance) method. Any workaround ???
Arthur Ronald F D Garcia
+1  A: 

Spring maintains an association between the current session and the current transaction (see SessionFactoryUtils.java.) Since there is already a session associated for the current DAO method call, you have to use this Session, or take the plunge of getting involved with the murky details of associating the new session with the previous transaction context. It's probably possible, but with considerable risk, and is definitely not recommended. In hibernate, if you have a session already open, then it should be used.

Having said that, you may be able to get spring to create a new session for you and associate it with the current transaction context. Use SessionFactoryUtils.getNewSession(SessionFactory, Interceptor). If you use this rather than hibernate's sessionFactory, then this should keep the association with the transaction.

Initially, you can code this up directly in the DAO. When it's tried and tested and hopefully found to be working, you can then take steps to move the spring code out of your DAO, such as using AOP to add around advice to the add() methods that create and clean up new session.

Another alternative is to use a global Interceptor. Even though it's global, you can give it locally controllable behaviour. The TransientInterceptor contains a threadLocal<Boolean>. This is the flag for the current thread to indicate if the interceptor should return true for isTransient. You set it to true at the start of the add() method and clear it at the end. E.g.

   class TransientInterceptor extends EntityInterceptor {
      ThreadLocal<Boolean> transientFlag = new ThreadLocal<Boolean)();
      public boolean isTransient() {
         return transientFlag.get()==Boolean.TRUE;
      }
      static public setTransient(boolean b) {
          transientFlag.set(b);
      }
   }

And then in your DAO:

@Override
public void add(SomeEntity instance) {
   try {
       TransientInterceptor.set(true);
       sessionFactory.getCurrentSession().save(instance);
   }
   finally {
      TransientInterceptor.set(false);   
   }
}

You can then setup TransientInterceptor as a global interceptor on the SessionFactory (e.g. LocalSessionFactoryBean.) To make this less invasive, you could create an AOP around advice to apply this behaviour to all your DAO add methods, where appropriate.

mdma