The following unit test demonstrates this problem. I have also included 3 solutions to the problem in the test. I have considered the existing comments and solutions posted on this stack trace but I believe this case could be different. I am looking for an explanation as to why the second test fails "testGetEntitiesInListenerQueryIt".
Code:
import static org.junit.Assert.assertEquals;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.Transaction;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.internal.DefaultPostLoadEventListener;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostLoadEvent;
import org.hibernate.service.ServiceRegistryBuilder;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
public class EventListenerTest {
private static ServiceRegistryImplementor serviceRegistry;
private static SessionFactory sessionFactory;
private static MyEntityListener myListener;
private static DataSource createDataSource(String url, String driver, String userName, String password) {
final BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUsername(userName);
dataSource.setPassword(password);
dataSource.setUrl(url);
return dataSource;
}
@BeforeClass
public static void setup() {
final DataSource dataSource = createDataSource("jdbc:hsqldb:mem:test", "org.hsqldb.jdbcDriver", "sa", "");
final Configuration config = new Configuration();
config.getProperties().put(AvailableSettings.DIALECT, "org.hibernate.dialect.HSQLDialect");
config.getProperties().put(AvailableSettings.SHOW_SQL, "false");
config.getProperties().put(AvailableSettings.FORMAT_SQL, "true");
config.getProperties().put(AvailableSettings.HBM2DDL_AUTO, "create");
config.getProperties().put(AvailableSettings.MAX_FETCH_DEPTH, "1");
config.getProperties().put(Environment.DATASOURCE, dataSource);
config.getProperties().put(AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, "org.hibernate.context.internal.ThreadLocalSessionContext");
config.addAnnotatedClass(Entity1.class);
config.addAnnotatedClass(Entity2.class);
config.addAnnotatedClass(Entity3.class);
serviceRegistry = (ServiceRegistryImplementor) new ServiceRegistryBuilder().applySettings(config.getProperties()).buildServiceRegistry();
sessionFactory = (SessionFactoryImplementor) config.buildSessionFactory( serviceRegistry );
final EventListenerRegistry registry = ((SessionFactoryImplementor) sessionFactory).getServiceRegistry().getService(EventListenerRegistry.class);
myListener = new MyEntityListener();
registry.getEventListenerGroup(EventType.POST_LOAD).appendListener(myListener);
populateHsqlDb();
}
private static void populateHsqlDb() {
final Session session = sessionFactory.getCurrentSession();
final Transaction transaction = session.beginTransaction();
final Entity1 entity1 = new Entity1("Entity1");
entity1.setId(Long.valueOf(1));
final Entity2 entity2a = new Entity2("Entity2a");
entity2a.setId(Long.valueOf(2));
Entity2 entity2b = new Entity2("Entity2b");
entity2b.setId(Long.valueOf(3));
entity2a.setParentEntity(entity1);
entity2b.setParentEntity(entity1);
final Set<Entity2> entity2s = new HashSet<Entity2>();
entity2s.add(entity2a);
entity2s.add(entity2b);
entity1.setEntity2s(entity2s);
entity1.setEntity2s(entity2s);
session.save(entity1);
//session.save(entity2a);
//session.save(entity2b);
final Entity3 entity3 = new Entity3("Entity3 - unrelated.");
entity3.setId(Long.valueOf(4));
session.save(entity3);
transaction.commit();
}
//@Ignore
@Test
public void testGetEntitiesInListenerLoadIt() {
Transaction transaction = null;
try {
final Session session = sessionFactory.getCurrentSession();
transaction = session.beginTransaction();
myListener.setLoadStrategy(new JustLoadItStrategy());
final Long id = Long.valueOf(1);
final Entity1 entity1 = (Entity1) sessionFactory.getCurrentSession().load(EventListenerTest.Entity1.class, id);
assertEquals(2, entity1.getEntity2s().size());
} finally {
transaction.rollback();
}
}
@Ignore
@Test
public void testGetEntitiesInListenerQueryIt() {
Transaction transaction = null;
try {
final Session session = sessionFactory.getCurrentSession();
transaction = session.beginTransaction();
//this approach will trigger a flush in the event listener.
myListener.setLoadStrategy(new TryQueryItStrategy());
final Long id = Long.valueOf(1);
final Entity1 entity1 = (Entity1) sessionFactory.getCurrentSession().load(EventListenerTest.Entity1.class, id);
assertEquals(2, entity1.getEntity2s().size());
} finally {
transaction.rollback();
}
}
public interface EntityInterface {
String getEntityDescription();
Long getId();
}
@Entity
@Table(name = "ENTITY1")
public static class Entity1 implements Serializable, EntityInterface {
private static final long serialVersionUID = -8262641590629153464L;
private Long id;
private Set<Entity2> entity2s = new HashSet<Entity2>();
private String description;
public Entity1() {
}
public Entity1(String description) {
this.description = description;
}
@Id
@Column(name = "ENTITY1_ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@OneToMany(cascade={CascadeType.ALL}, mappedBy = "parentEntity")
/**
* Solution option 3.
*/
//commenting out the join fetch mode also solves the problem but only because it impacts the auto flush logic
//in retrieving the set.
@Fetch(FetchMode.JOIN)
public Set<Entity2> getEntity2s() {
return entity2s;
}
public void setEntity2s(Set<Entity2> entity2s) {
this.entity2s = entity2s;
}
@Column(name = "ENTITY1_DESCRIPTION")
@Override
public String getEntityDescription() {
return description;
}
public void setEntityDescription(String description) {
this.description = description;
}
}
@Entity
@Table(name = "ENTITY2")
public static class Entity2 implements Serializable, EntityInterface {
private static final long serialVersionUID = 7451097975241296549L;
private Entity1 parentEntity;
private Long id;
private String description;
public Entity2() {
}
public Entity2(String description) {
this.description = description;
}
@Id
@Column(name = "ENTITY2_ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@ManyToOne(fetch=FetchType.LAZY, optional=false)
@JoinColumn(name="PARENT_ENTITY1_ID", nullable=false)
public Entity1 getParentEntity() {
return parentEntity;
}
public void setParentEntity(Entity1 entity) {
this.parentEntity = entity;
}
@Column(name = "ENTITY2_DESCRIPTION")
@Override
public String getEntityDescription() {
return description;
}
public void setEntityDescription(String description) {
this.description = description;
}
}
@Entity
@Table(name = "ENTITY3")
public static class Entity3 implements Serializable, EntityInterface {
private static final long serialVersionUID = 7451097975241296549L;
private Long id;
private String description;
public Entity3() {
}
public Entity3(String description) {
this.description = description;
}
@Id
@Column(name = "ENTITY3_ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(name = "ENTITY3_DESCRIPTION")
@Override
public String getEntityDescription() {
return description;
}
public void setEntityDescription(String description) {
this.description = description;
}
}
public static interface MyUnrelatedEntityLoadStrategy {
Entity3 loadIt(PostLoadEvent event);
}
/**
* Solution option1 - this approach works but only because it doesn't trigger the auto flush.
*/
public static class JustLoadItStrategy implements MyUnrelatedEntityLoadStrategy {
@Override
public Entity3 loadIt(PostLoadEvent event) {
return (Entity3) sessionFactory.getCurrentSession().load(EventListenerTest.Entity3.class, Long.valueOf(4));
}
}
public static class TryQueryItStrategy implements MyUnrelatedEntityLoadStrategy {
@Override
public Entity3 loadIt(PostLoadEvent event) {
final Query query = sessionFactory.getCurrentSession().createQuery("select e from EventListenerTest$Entity3 e where e.id = ?");
//this gets the same reference to the session as getCurrentSession
//final Query query = event.getSession().createQuery("select e from EventListenerTest$Entity3 e where e.id = ?");
/**
* Solution option2 - the statless session works because it will avoid the event listener
*/
//Does this mean it is simply unsafe to query in an event listener if querying triggers the auto flush?
//StatelessSession session = event.getPersister().getFactory().openStatelessSession();
//final Query query = session.createQuery("select e from EventListenerTest$Entity3 e where e.id = ?");
query.setParameter(0, Long.valueOf(4));
return (Entity3) query.list().get(0);
}
}
public static class MyEntityListener extends DefaultPostLoadEventListener {
private static final long serialVersionUID = -9019214503421635237L;
private MyUnrelatedEntityLoadStrategy strategy;
public MyEntityListener() {}
public void setLoadStrategy(MyUnrelatedEntityLoadStrategy strategy) {
this.strategy = strategy;
}
@Override
public void onPostLoad(PostLoadEvent event) {
super.onPostLoad(event);
final Object entity = event.getEntity();
if(EntityInterface.class.isInstance(entity)) {
EntityInterface myEntity = (EntityInterface)entity;
System.out.println("on post load: " + myEntity.getId() + " - " + myEntity.getEntityDescription());
}
final Entity3 entity3 = strategy.loadIt(event);
System.out.println("loading entity in event listener: " + entity3.getId() + " - " + entity3.getEntityDescription());
}
}
}