Hibernate version: 3.2
2nd level cache: ehcache
I'm having some difficulty with cached entitys. In particular, when I delete an entity that is the "one" side of a manyToOne unidirectional relationship.
Please read the following example, where first of all I define two entities: Address and Person, where Person has a manyToOne relationship with Address. I then have a test case that highlights my issue.
The issue seems to be that deleting an Address that happens to be referenced to by any other entity (or entities) currently in the second level cache, will place those entities in a state in which they can't be reassembled. The reason being that they have a cached id to a deleted entity.
In terms of solutions, I can only think of unsatisfactory ones:
1) When deleting an address, run a query to find all Person(s), plus any other classes that have references to an Address, and set the address to null. This is not very good because it means I potentially have to load a lot of data, some of which may not have even been cached anyway!
2) Evict all instances of Person, and any other classes that reference an Address, from the 2nd level cache. If this is necessary then I wonder why I bother running the cache at all.
3) Make all relationships bidirectional, and have loads of extra code to set
the "many" side to null. This should not be necessary, as in many cases it's not at all useful to have a @OneToMany on the "one" side.
I really hope someone can tell me I'm doing something wrong, or missing an important step here. I've tried changing the CacheConcurrencyStrategy to TRANSACTIONAL, with no effect.
Thanks for reading a long post!
-Paul
Code:
@Entity
@Table(name = "ADDRESS")
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Address {
@Id
@Column(name = "ID")
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
@Entity
@Table(name = "PERSON")
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Person {
@Id
@Column(name = "ID")
private Long id;
@ManyToOne
@JoinColumn(name = "ADDRESS_ID", nullable = true)
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
And now for the test case:
Code:
public class ManyToOneCachedEntityTest extends TestCase{
private static final Long addressId = (long)1;
private static final Long personId = (long)2;
protected void setUp() {
TestSchema.setup();
}
public static void testCachedEntryDeletion() {
// first of all create and instance of each entity in one session
Session session = HibernateUtil.getSession();
session.beginTransaction();
Address address = new Address();
address.setId(addressId);
Person person = new Person();
person.setAddress(address);
person.setId(personId);
session.save(address);
session.save(person);
session.getTransaction().commit();
session.close();
// start a new session and delete the person's address
session = HibernateUtil.getSession();
session.beginTransaction();
Address address2 = (Address)session.get(Address.class, addressId);
session.delete(address2);
session.getTransaction().commit();
session.close();
// get a new session an see what's happened to the person
session = HibernateUtil.getSession();
session.beginTransaction();
try {
Person person2 = (Person)session.get(Person.class, personId);
System.out.println("Sucessfully loaded instance of Person");
} catch(ObjectNotFoundException e) {
System.out.println("Failed to get instance of Person");
e.printStackTrace();
try {
session.getSessionFactory().evict(Person.class);
Person entityMany2 = (Person)session.get(Person.class, personId);
System.out.println("Sucessfully loaded instance of Person after it was evicted from 2nd level cache");
} catch(ObjectNotFoundException e1) {
System.out.println("Failed to get instance of Person, even after it was evicted from 2nd level cache");
e.printStackTrace();
}
}
session.getTransaction().commit();
session.close();
}
}
Finally, here's the output
Code:
13:12:38,374 DEBUG SQL:401 - insert into ADDRESS (ID) values (?)
13:12:38,530 DEBUG SQL:401 - insert into PERSON (ADDRESS_ID, ID) values (?, ?)
13:12:38,546 DEBUG SQL:401 - delete from ADDRESS where ID=?
13:12:38,561 DEBUG SQL:401 - select address0_.ID as ID68_0_ from ADDRESS address0_ where address0_.ID=?
Failed to get instance of Person
org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [towers.framework.model.core.Address#1]
at org.hibernate.impl.SessionFactoryImpl$1.handleEntityNotFound(SessionFactoryImpl.java:377)
at org.hibernate.event.def.DefaultLoadEventListener.load(DefaultLoadEventListener.java:145)
at org.hibernate.event.def.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:195)
at org.hibernate.event.def.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:103)
at org.hibernate.impl.SessionImpl.fireLoad(SessionImpl.java:878)
at org.hibernate.impl.SessionImpl.internalLoad(SessionImpl.java:846)
at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:557)
at org.hibernate.type.ManyToOneType.assemble(ManyToOneType.java:196)
at org.hibernate.type.TypeFactory.assemble(TypeFactory.java:420)
at org.hibernate.cache.entry.CacheEntry.assemble(CacheEntry.java:96)
at org.hibernate.cache.entry.CacheEntry.assemble(CacheEntry.java:82)
at org.hibernate.event.def.DefaultLoadEventListener.assembleCacheEntry(DefaultLoadEventListener.java:553)
at org.hibernate.event.def.DefaultLoadEventListener.loadFromSecondLevelCache(DefaultLoadEventListener.java:508)
at org.hibernate.event.def.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:357)
at org.hibernate.event.def.DefaultLoadEventListener.load(DefaultLoadEventListener.java:139)
at org.hibernate.event.def.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:195)
at org.hibernate.event.def.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:103)
at org.hibernate.impl.SessionImpl.fireLoad(SessionImpl.java:878)
at org.hibernate.impl.SessionImpl.get(SessionImpl.java:815)
at org.hibernate.impl.SessionImpl.get(SessionImpl.java:808)
at towers.framwork.model.core.ManyToOneCachedEntityTest.testCachedEntryDeletion(ManyToOneCachedEntityTest.java:61)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:130)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Sucessfully loaded instance of Person after it was evicted from 2nd level cache
13:12:38,686 DEBUG SQL:401 - select address0_.ID as ID68_0_ from ADDRESS address0_ where address0_.ID=?
13:12:38,686 DEBUG SQL:401 - select person0_.ID as ID69_1_, person0_.ADDRESS_ID as ADDRESS2_69_1_, address1_.ID as ID68_0_ from PERSON person0_, ADDRESS address1_ where person0_.ADDRESS_ID=address1_.ID(+) and person0_.ID=?
13:12:38,686 DEBUG SQL:401 - delete from PERSON where ID=?