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: Potential bug: Saving objects with detached associations
PostPosted: Fri Mar 27, 2009 4:39 am 
Pro
Pro

Joined: Tue Aug 26, 2003 8:07 pm
Posts: 229
Location: Brisbane, Australia
We are having a problem with Hibernate related to the saving of objects with references to detched objects.

I have a simplified and self-contained example, the source code is attached below.
I also have a (maybe very bad) idea for a patch to fix the issue.


Basically, the problem is where we have a Person object being saved to the DB and the person object is associated with a detached address object that was loaded in a previous session.

The symptom of the problem is that Hibernate throws a LazyInitException while trying to load the detached object (which it shouldn't try to do, since it's detached) during the persist operation.

Ordinarily, the above outlined scenario works fine, Hibernate detects that the Address is detached and since it is only trying to issue an insert, it just uses the ID of the address to generate the insert statement for the Person. The problem comes when a seperate piece of code loads the object that was detached into the session, but doesn't assign it to the object we are later trying to persist. (The two pieces of code are pretty much orthogonal, they just happen to refer to the same object for different reasons)

The example code is very simple, so maybe it would be a better idea to just look at the attached code for an understanding of the problem we are having.

The problem appears to be in the org.hibernate.engine.ForeignKeys.isNullifiable(String, Object) method, when it is trying to deal with lazy properties.

The problem seems to be with this code here:
Code:
private boolean isNullifiable(final String entityName, Object object)
throws HibernateException {
   
   if (object==LazyPropertyInitializer.UNFETCHED_PROPERTY) return false; //this is kinda the best we can do...
   
   if ( object instanceof HibernateProxy ) {
      // if its an uninitialized proxy it can't be transient
      LazyInitializer li = ( (HibernateProxy) object ).getHibernateLazyInitializer();
      if ( li.getImplementation(session) == null ) {
         return false;
         // ie. we never have to null out a reference to
         // an uninitialized proxy
      }
      else {
         //unwrap it
         object = li.getImplementation();
      }
   }

When you uncomment the line that reloads the Address in my example below, this code will throw a lazy exception when trying to unwrap the LazyInitializer implementation, because it thinks it has a Proxy that's attached to the current session, but it doesn't: it has the detached one from a previously closed session.

I think if you changed
Code:
if ( li.getImplementation(session) == null ) {

to
Code:
if ( li.getImplementation(session) == null || li.getImplementation(session) != object) {

that the isNullifiable() method would then be dealing properly with detached objects.

I'm not sure if this would break other things in Hibernate since I'm not sure about the contract isNullifiable() method needs to fill in the surrounding Hibernate code, but it does look like a pretty easy fix.

I've looked in the Hibernate JIRA for this issue but can't seem to find anything related. I'd like to raise the issue and attach a patch; please let me know if this is a known issue or maybe an already fixed issue.

Hibernate version: 3.2.5 GA

Mapping documents:
Code:
@Entity
@Table(name = "eg_sbt_address")
public class Address {

    private Long id;
    private String content;

    @Id
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Basic
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

@Entity
@Table(name = "eg_sbt_person")
public class Person {

    private Long id;
    private Address address;
    private String name;

    @Id
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @ManyToOne(fetch = FetchType.LAZY, cascade = {})
    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Basic
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}



Code between sessionFactory.openSession() and session.close():
Code:
    @Test
    public void hibernateTwoSessionTest(){

        DataSource ds = getDataSourceFromSomewhere();
       
        Ejb3Configuration cfg = new Ejb3Configuration();
        cfg.setDataSource(ds);
        cfg.setProperty("hibernate.hbm2ddl.auto", "update");

        cfg.addAnnotatedClass(Person.class);
        cfg.addAnnotatedClass(Address.class);


        HibernateEntityManagerFactory emf = (HibernateEntityManagerFactory)
            cfg.buildEntityManagerFactory();
       
        SessionFactory sf = emf.getSessionFactory();
        Session s = sf.openSession();
        Transaction tx = s.beginTransaction();

        Address createAddr = new Address();
        createAddr.setId(System.currentTimeMillis());
        createAddr.setContent("21 Jump St");
        s.persist(createAddr);

        tx.commit();
        s.close();
        s = sf.openSession();
        tx = s.beginTransaction();

        Address loadedAddr = (Address) s.load(Address.class, createAddr.getId());

        tx.commit();
        s.close();
        s = sf.openSession();
        tx = s.beginTransaction();


        // Uncomment this line and the test will fail inside the persist()
        // operation with a LazyInitException
//        Address reloadedAddr = (Address) s.get(Address.class, createAddr.getId());

        Person person = new Person();
        person.setId(System.currentTimeMillis());
        person.setName("Johnny Depp");
        person.setAddress(loadedAddr);

        s.persist(person);

        tx.commit();
        s.close();
    }



Full stack trace of any exception that occurs:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:86)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:140)
at org.hibernate.engine.ForeignKeys$Nullifier.isNullifiable(ForeignKeys.java:137)
at org.hibernate.engine.ForeignKeys$Nullifier.nullifyTransientReferences(ForeignKeys.java:92)
at org.hibernate.engine.ForeignKeys$Nullifier.nullifyTransientReferences(ForeignKeys.java:70)
at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:311)
at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:144)
at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:49)
at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:154)
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:110)
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:61)
at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:645)
at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:619)
at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:623)
at gekko.services.query.QueryServiceTest.hibernateTwoSessionTest(QueryServiceTest.java:1404)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59)
at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98)
at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79)
at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87)
at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77)
at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42)
at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88)
at gekko.services.test.util.NameAwareTestClassRunner.invokeTestMethod(NameAwareTestClassRunner.java:39)
at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
at com.intellij.rt.junit4.Junit4TestMethodAdapter.run(Junit4TestMethodAdapter.java:62)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:90)

_________________
Cheers,
Shorn.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Mar 27, 2009 10:48 am 
Beginner
Beginner

Joined: Wed Dec 17, 2008 12:10 pm
Posts: 47
This is because session.get does not actually load the full object, it creates a proxy and sets the ID of the object. Try changing the commented line to session.load.

EDIT: I take that back, I think it's the other way around...


Top
 Profile  
 
 Post subject:
PostPosted: Fri Mar 27, 2009 10:57 am 
Beginner
Beginner

Joined: Wed Dec 17, 2008 12:10 pm
Posts: 47
What happens if you call session.merge on createAddr?

Also, you're not even using reloadedAddr.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Mar 27, 2009 3:19 pm 
Expert
Expert

Joined: Wed Mar 03, 2004 6:35 am
Posts: 1240
Location: Lund, Sweden
Hmmm... it could be a bug. I am not 100% sure, but your reasoning seems to fit. The LazyInitializer.getImplementation(SessionImplementor) is returning the underlying instance in the current session, but I don't really understand why it would need that object at that point in the code. The comments seem to indicate that what is important is if the proxy has been initialized or not.

Now I am just speculating, but wouldn't it be enough to just check LazyInitializer.isUninitialized() at that stage? And then only get the underlying object in the else part of the same statement. Something like this:

Code:
if (li.isUninitialized()) {
  return false;
  // ie. we never have to null out a reference to
  // an uninitialized proxy
}
else {
   //unwrap it
   object = li.getImplementation();
}


Or, as an alternative, get the underlying object that actually belongs to the same session:

Code:
if (li.getImplementation(session) == null) {
  return false;
  // ie. we never have to null out a reference to
  // an uninitialized proxy
}
else {
   //unwrap it
   object = li.getImplementation(session);
}


Can you try the above? If it seems to work, then open a JIRA issue and attach a patch and if possible also a test case.


Top
 Profile  
 
 Post subject:
PostPosted: Mon Mar 30, 2009 2:57 am 
Pro
Pro

Joined: Tue Aug 26, 2003 8:07 pm
Posts: 229
Location: Brisbane, Australia
Alrighty.

First thing we did was try upgrading to HB 3.3.1: no dice, still doesn't work.

Then we tried your first suggestion; that seemed to fix the problem with the example code, but the HB test suite reported errors (something to do with batch stuff, can post more information if required).

Then we tried my idea; same thing - fixed the problem in the example code, but the HB test suite was broken again (even more broken).

Lastly, we tried your second suggestion; this fixed the problem in the example code, and the HB test suite seems to run fine... BUT it breaks our actual application.
I haven't had time today to work more on this, but I thought I would post our progress so far.


Code:
16:50:59.830 ERROR [main] testCappingForRenewal org.hibernate.AssertionFailure - an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session)
org.hibernate.AssertionFailure: Merged entity does not have status set to MANAGED; EntityEntry[gekko.domain.common.Address#1372](READ_ONLY) status=READ_ONLY
at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:97)
at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:704)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:688)
at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:692)
at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:235)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:198)
at $Proxy67.merge(Unknown Source)
at gekko.services.entity.EntityServiceImpl.merge(EntityServiceImpl.java:31)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:182)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:149)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:160)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:77)
at gekko.persistence.spring.GekkoTraceInterceptor.traceParameters(GekkoTraceInterceptor.java:54)
at sun.reflect.GeneratedMethodAccessor19.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:627)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:616)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:64)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:160)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:77)
at gekko.persistence.spring.GekkoTraceInterceptor.pushLogContext(GekkoTraceInterceptor.java:82)
at sun.reflect.GeneratedMethodAccessor18.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:627)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:616)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:64)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:160)
at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:54)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:160)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:77)
at gekko.services.util.infrastructure.ServiceContextInterceptor.wrapServiceCall(ServiceContextInterceptor.java:83)
at sun.reflect.GeneratedMethodAccessor17.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:627)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:616)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:64)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:160)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
at $Proxy69.merge(Unknown Source)
at gekko.services.calculator.CalculatorServiceTest.testCappingForRenewal(CalculatorServiceTest.java:557)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59)
at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98)
at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79)
at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87)
at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77)
at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42)
at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88)
at gekko.services.test.util.NameAwareTestClassRunner.invokeTestMethod(NameAwareTestClassRunner.java:39)
at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:45)
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)

_________________
Cheers,
Shorn.


Top
 Profile  
 
 Post subject:
PostPosted: Mon Mar 30, 2009 3:00 am 
Pro
Pro

Joined: Tue Aug 26, 2003 8:07 pm
Posts: 229
Location: Brisbane, Australia
Ok.

Turns out we had a bit of an issue with our testing, and we were running multiple changes at once.

Once we re-tested with just your second suggestion (i.e changing the unwrapping branch to use the active session ), everything works.

That is; it works in our sample code, works for the Hibernate test suite and works in our application proper.

I will raise a JIRA and attach the patch ASAP.

Thanks for your help.

_________________
Cheers,
Shorn.


Top
 Profile  
 
 Post subject:
PostPosted: Wed Apr 01, 2009 9:39 pm 
Pro
Pro

Joined: Tue Aug 26, 2003 8:07 pm
Posts: 229
Location: Brisbane, Australia
Jira raised with sample code and patch attached: http://opensource.atlassian.com/project ... e/HHH-3846

_________________
Cheers,
Shorn.


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.