[hibernate-issues] [Hibernate-JIRA] Commented: (HHH-2326) NullPointerException from merge on composite id

Avram Cherry (JIRA) noreply at atlassian.com
Fri Nov 14 01:51:18 EST 2008


    [ http://opensource.atlassian.com/projects/hibernate/browse/HHH-2326?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=31710#action_31710 ] 

Avram Cherry commented on HHH-2326:
-----------------------------------

I'm posting this here instead of the issue that it 'duplicates' (it doesn't, entirely) because it has more watchers.

I found a way around this bug since I too am stuck with a legacy database.

I've only tested this to work with the 'persist' event.  It will probably need refinement to work with 'merge'.  In this example, I will assume that you're doing 'persist'.

First, remove the persist cascade indicator from any class that uses it on the relationship to the class with the compound key.

Next, make the following interface:

---
public interface CascadePersistLast {
  public void collectEntitiesToPersistLast(Set s);
}
---

Next, make the following class:
---

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.hibernate.HibernateException;
import org.hibernate.event.EventSource;
import org.hibernate.event.FlushEvent;
import org.hibernate.event.FlushEventListener;
import org.hibernate.event.PersistEvent;
import org.hibernate.event.PersistEventListener;

public class ExplicitCascadeOrderEventListener implements PersistEventListener, FlushEventListener  {
  
  boolean isAlreadyFlushing = false;
  
  @SuppressWarnings("unchecked")
  static protected Map<EventSource, Set> entitiesToPersistLast = new WeakHashMap<EventSource, Set>();

  private static final long serialVersionUID = 1L;

  @SuppressWarnings("unchecked")
  @Override
  public void onPersist(PersistEvent event) throws HibernateException {
    doOnPersist(event, new HashMap());
  }

  @SuppressWarnings("unchecked")
  @Override
  public void onPersist(PersistEvent event, Map createdAlready)
      throws HibernateException {
    doOnPersist(event, createdAlready);
  }
  
  @SuppressWarnings("unchecked")
  private void doOnPersist(PersistEvent event, Map createdAlready) throws HibernateException {
    if (event.getObject() instanceof CascadePersistLast) {
      CascadePersistLast p = (CascadePersistLast) event.getObject();
      if (! entitiesToPersistLast.containsKey(event.getSession())) {
        entitiesToPersistLast.put(event.getSession(), new HashSet());
      }
      p.collectEntitiesToPersistLast(entitiesToPersistLast.get(event.getSession()));
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public void onFlush(FlushEvent event) throws HibernateException {
    if (entitiesToPersistLast.containsKey(event.getSession())) {
      EventSource session = event.getSession();
      Set entities = entitiesToPersistLast.get(session);
      entitiesToPersistLast.remove(session);
      if (isAlreadyFlushing) {
        throw new HibernateException("Assertion failed!");
      }
      isAlreadyFlushing = true;
      for (Object e : entities) {
        if (! session.contains(e)) {
          session.persist(e);
        }
      }
      session.flush();
      isAlreadyFlushing = false;
    }
  }
}
---

Now implement CascadePersistLast on any entity class that you want to cascade persist on, and add any instances of the entity with the compound key to this set when collectEntitiesToPersistLast is called.  It should be safe to pass already-persistant instances, since it won't try to persist it twice.

Finally, alter your hibernate.hbm.xml to include the following, or programatically achieve the equivalent:

  <event type="create">
      <listener class="org.hibernate.event.def.DefaultPersistEventListener"/>
      <listener class="yourpackage.ExplicitCascadeOrderEventListener"/>
    </event>
    <event type="flush">
      <listener class="org.hibernate.event.def.DefaultFlushEventListener"/>
      <listener class="yourpackage.ExplicitCascadeOrderEventListener"/>
    </event>

Note that the order of the listeners within the vent is VERY important.  The custom listener, at least for flush, MUST be the very last one.

When I was working with the HibernateTest.zip contents when doing a merge, I noticed that Hibernate does not seem to allow you to merge an entity that contains associations that are neither cascading or already persistent, so in order for my approach to work with that, you might need to take the further step of actually temporarily removing the transient instances before the first flush, and restore them before the 2nd.  Since I don't use merge in conjunction with this particular problem, I didn't investigate further.

Also, that static map isn't thread-safe.  I guess you should synchronize it or maybe replace it with a threadlocal.

You may use this code however you'd wish, but it may, of course, cause errors, lose data or feed chocolate to your dog.

> NullPointerException from merge on composite id
> -----------------------------------------------
>
>                 Key: HHH-2326
>                 URL: http://opensource.atlassian.com/projects/hibernate/browse/HHH-2326
>             Project: Hibernate Core
>          Issue Type: Bug
>          Components: core
>    Affects Versions: 3.2.1
>         Environment: Hibernate 3.2.1ga, tested against Oracle 10g and Derby
>            Reporter: Greg Adams
>            Assignee: Diego Plentz
>            Priority: Critical
>         Attachments: HibernateMergeBug.zip
>
>
> Merge is throwing an NPE from the bowels of Hibernate when I have a class mapped with a composite ID.
> Stacktrace:
> [junit] java.lang.NullPointerException
>     [junit] 	at org.hibernate.type.AbstractType.getHashCode(AbstractType.java:112)
>     [junit] 	at org.hibernate.type.AbstractType.getHashCode(AbstractType.java:120)
>     [junit] 	at org.hibernate.type.EntityType.getHashCode(EntityType.java:279)
>     [junit] 	at org.hibernate.type.ComponentType.getHashCode(ComponentType.java:189)
>     [junit] 	at org.hibernate.engine.EntityKey.generateHashCode(EntityKey.java:104)
>     [junit] 	at org.hibernate.engine.EntityKey.<init>(EntityKey.java:48)
>     [junit] 	at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:100)
>     [junit] 	at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:687)
>     [junit] 	at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:669)
>     [junit] 	at org.hibernate.engine.CascadingAction$6.cascade(CascadingAction.java:245)
>     [junit] 	at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:268)
>     [junit] 	at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:216)
>     [junit] 	at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
>     [junit] 	at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:296)
>     [junit] 	at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:242)
>     [junit] 	at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:219)
>     [junit] 	at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
>     [junit] 	at org.hibernate.engine.Cascade.cascade(Cascade.java:130)
>     [junit] 	at org.hibernate.event.def.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:437)
>     [junit] 	at org.hibernate.event.def.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:194)
>     [junit] 	at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:123)
>     [junit] 	at org.hibernate.event.def.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:53)
>     [junit] 	at org.hibernate.impl.SessionImpl.fireMerge(SessionImpl.java:677)
>     [junit] 	at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:661)
>     [junit] 	at org.hibernate.impl.SessionImpl.merge(SessionImpl.java:665)
>     [junit] 	at com.foo.test.HibernateTest.doMerge(Unknown Source)
>     [junit] 	at com.foo.test.HibernateTest.testInsert(Unknown Source)
>     [junit] 	at org.junit.internal.runners.TestMethodRunner.executeMethodBody(TestMethodRunner.java:99)
>     [junit] 	at org.junit.internal.runners.TestMethodRunner.runUnprotected(TestMethodRunner.java:81)
>     [junit] 	at org.junit.internal.runners.BeforeAndAfterRunner.runProtected(BeforeAndAfterRunner.java:34)
>     [junit] 	at org.junit.internal.runners.TestMethodRunner.runMethod(TestMethodRunner.java:75)
>     [junit] 	at org.junit.internal.runners.TestMethodRunner.run(TestMethodRunner.java:45)
>     [junit] 	at org.junit.internal.runners.TestClassMethodsRunner.invokeTestMethod(TestClassMethodsRunner.java:71)
>     [junit] 	at org.junit.internal.runners.TestClassMethodsRunner.run(TestClassMethodsRunner.java:35)
>     [junit] 	at org.junit.internal.runners.TestClassRunner$1.runUnprotected(TestClassRunner.java:42)
>     [junit] 	at org.junit.internal.runners.BeforeAndAfterRunner.runProtected(BeforeAndAfterRunner.java:34)
>     [junit] 	at org.junit.internal.runners.TestClassRunner.run(TestClassRunner.java:52)
>     [junit] 	at junit.framework.JUnit4TestAdapter.run(JUnit4TestAdapter.java:32)
> I've attached a zipped-up project that reproduces the error. Just extract and run ant.

-- 
This message is automatically generated by JIRA.
-
If you think it was sent incorrectly contact one of the administrators: http://opensource.atlassian.com/projects/hibernate/secure/Administrators.jspa
-
For more information on JIRA, see: http://www.atlassian.com/software/jira

        



More information about the hibernate-issues mailing list