-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 
Author Message
 Post subject: Problem with 2nd level cache and unidirectional manyToOne
PostPosted: Thu Feb 28, 2008 9:32 am 
Regular
Regular

Joined: Mon Aug 20, 2007 6:47 am
Posts: 74
Location: UK
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=?





Top
 Profile  
 
 Post subject: Re: Problem with 2nd level cache and unidirectional manyToOn
PostPosted: Thu Feb 28, 2008 12:24 pm 
Expert
Expert

Joined: Wed Apr 11, 2007 11:39 am
Posts: 735
Location: Montreal, QC
There is a small alternative to number 2 and that is evicting only persons that have that address.


Farzad-


Top
 Profile  
 
 Post subject: Re: Problem with 2nd level cache and unidirectional manyToOn
PostPosted: Thu Feb 28, 2008 12:36 pm 
Regular
Regular

Joined: Mon Aug 20, 2007 6:47 am
Posts: 74
Location: UK
farzad wrote:
There is a small alternative to number 2 and that is evicting only persons that have that address.


Farzad-


How can I do that efficiently? Should I expect to have to go through every cached instance of Person to find instances that need evicting?

I hope that's not the case. For argument's sake I could have dozens of classes with references to Address. What a lot of code to mimic the basic on-delete-set-null behaviour of a database!


Top
 Profile  
 
 Post subject: Re: Problem with 2nd level cache and unidirectional manyToOn
PostPosted: Thu Feb 28, 2008 12:42 pm 
Expert
Expert

Joined: Wed Apr 11, 2007 11:39 am
Posts: 735
Location: Montreal, QC
I hear ya. I just came up with an idea that might solve your problem. How about putting some @NotFound(action=NotFoundAction.IGNORE) annotation on those relations. I am not sure how it goes when an entity is being fetched from cache but give it a try and see how it resonates. Let me know.


Farzad-


Top
 Profile  
 
 Post subject: Re: Problem with 2nd level cache and unidirectional manyToOn
PostPosted: Thu Feb 28, 2008 1:01 pm 
Regular
Regular

Joined: Mon Aug 20, 2007 6:47 am
Posts: 74
Location: UK
farzad wrote:
I hear ya. I just came up with an idea that might solve your problem. How about putting some @NotFound(action=NotFoundAction.IGNORE) annotation on those relations. I am not sure how it goes when an entity is being fetched from cache but give it a try and see how it resonates. Let me know.


Farzad-


wow, never heard of that annotation before and guess what, it makes my test case pass without the ObjectNotFoundException!

Thank you so much for your help. Now I need to apply the same thing to my real application code and see if it works there too.

One last thing is confusing me - why is this not a problem for everyone? Is there something unusual about this scenario? The hibernate annotations documentation has this to say about @NotFound:

"By default, when Hibernate cannot resolve the association because the expected associated element is not in database (wrong id on the association column), an exception is raised by Hibernate. This might be inconvenient for lecacy and badly maintained schemas. You can ask Hibernate to ignore such elements instead of raising an exception using the @NotFound annotation."

Hang on a minute, my database is not legacy OR poorly maintained! The expected associated element is not in the database because I just deleted it, and the cache not smart enough to realise that cached ids may have been deleted.

I would expect the default behaviour of the second level cache to only throw exceptions when the missing foreign key is annotated as NOT NULL, in which case the deletion should not have proceeded anyway. In the nullable case, I would expect missing ids to translate to a null ManyToOne property.

Anyway, I've got a solution so I should be happy =)

Thanks again. Thank god there are a few people like you that actually respond to questions!


Top
 Profile  
 
 Post subject: Re: Problem with 2nd level cache and unidirectional manyToOn
PostPosted: Thu Feb 28, 2008 10:22 pm 
Expert
Expert

Joined: Wed Apr 11, 2007 11:39 am
Posts: 735
Location: Montreal, QC
I totally agree with you on the fact this is a regular use case, but chances are there is something for and we don't know it. In addition, it is also possible that this is not implemented just because it is complicated a little bit. I looked at the Cache interface which is the bridge between hibernate and any cache implementation and it appears there is only one method for retrieving what is inside cache and that method is documented to be optional which means some caches might not have implemented it properly. The reason can also have roots in the fact that it is not very efficient for all caches to tell you what they have. In any events, if you feel really upset about this we can go ahead an implement a new feature for it.



Farzad-


Top
 Profile  
 
 Post subject:
PostPosted: Fri Feb 29, 2008 5:05 am 
Regular
Regular

Joined: Mon Aug 20, 2007 6:47 am
Posts: 74
Location: UK
Good stuff, Farzad.

I'm happy enough with the solution you came up with to not work on my own feature =)

I guess there are few weaknesses in the approach. Like the extra database hits to help the cache realise there are missing ids, and the fact that databases can get away with completely broken referential integrity (fortunately that's not the case for me!).

What I might do is try out a few other cache implementations and see if that effects my original test case.


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.