1144 words

Merge Issue: Attempt to reuse an existing primary key value

#1
2014-06-10 08:02

Hello,

I have just updated my objectdb version from 2.4.6 to 2.5.5.

I observed a new bug or a feature on an ordinary merge operation.

This is the test case ( work in 2.4.6 version ) :

2 parents object share the same child on a ManyToMany relationship with Cascade.ALL constraint.

public final class MyTestCase {
 
    public static void main(String[] args)  {
 
        EntityManagerFactory emf = Persistence.createEntityManagerFactory(
                "objectdb:$objectdb/db/test.tmp;drop");
 
 
        EntityManager em = null ; 
 
        // Merge parent1
        em = emf.createEntityManager();
        if (! em.getTransaction().isActive()) {
            em.getTransaction().begin();
        }
 
        MyEntity e1 = new MyEntity("parent1");
        e1.addChild(String.valueOf("child1"));
        em.merge(e1);
 
        em.getTransaction().commit();
        em.clear();
        em.close();
 
        // Merge parent2
        em = emf.createEntityManager();
        if (! em.getTransaction().isActive()) {
            em.getTransaction().begin();
        }
 
        MyEntity e2 = new MyEntity("parent2");
        e2.addChild(String.valueOf("child1"));
        em.merge(e2);
 
        em.getTransaction().commit();
        em.clear();
        em.close();
 
        emf.close();
    }
 
    @Entity
    public static class MyEntity {
 
        @Id
        private String name;
 
        @ManyToMany(cascade = {
                CascadeType.ALL
        })
        private List <MyEntityChild> lst = new ArrayList <MyEntityChild> ();
 
        MyEntity(String name) {
            this.name = name;
        }
 
        public void addChild(String name) {
            lst.add(new MyEntityChild(name));
        }
    }
 
    @Entity
    public static class MyEntityChild {
 
        @Id
        private String name;
 
        MyEntityChild(String name) {
            this.name = name;
        }
    }
}

With ObjectDb 2.5.5, i have the following exception :

Caused by: javax.persistence.EntityExistsException: com.objectdb.o.UserException: Attempt to reuse an existing primary key value (MyTestCase$MyEntityChild:'child1')
at com.objectdb.o._EntityExistsException.b(_EntityExistsException.java:46)
at com.objectdb.o.JPE.g(JPE.java:98)
at com.objectdb.o.JPE.g(JPE.java:78)
... 4 more

Is that behaviour normal, is it a regression ?

If that behaviour is considered as "normal", so how i can have in a simple way 2 parents sharing the same children ? ( without retrieving child on Db or using other tricks ).

Thanks,

Xirt

Edit : Using CascadeType.MERGE make the test case working !

I think the CascadeType.PERSIST ( included in CascadeType.ALL ) is the problem.
In my case i never explicit a "persist" operation, but a "merge" one.

It seems that ObjectDb ( on 2.5.5 ) made this "persist" operation, also in merge case.

xirt
xirt's picture
Joined on 2012-07-12
User Post #23
#2
2014-06-10 21:47

After some investigation we can confirm that a change in the behavior of merge in build 2.4.6_17 affects your test case.

The change was applied following a support ticket that demonstrated a strange merge behavior. Following is a simplified test case that demonstrates the problem:

import javax.persistence.*;
 
import org.junit.Assert;
import org.junit.Test;
 
import java.util.*;
 
 
public class F799x {
 
    @Test
    public void test() {
 
        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory(
                "objectdb:$objectdb/test.tmp;drop");
        EntityManager em = emf.createEntityManager();
 
        em.getTransaction().begin();
        A a = new A();
        a.list = new ArrayList();
        a.list.add(new B());
        em.persist(a); // first managed B instance
        em.getTransaction().commit();
 
        a = em.createQuery(
            "SELECT a FROM A a", A.class).getSingleResult();
        Assert.assertEquals(1, a.list.size());
 
        em.getTransaction().begin();
        a.list.add(new B());
        a = em.merge(a); // second managed B instance
        em.getTransaction().commit(); // third managed B instance
 
        List<B> resultList = em.createQuery(
            "SELECT b FROM B b", B.class).getResultList();
        Assert.assertEquals(2, resultList.size());
 
        em.clear();
        emf.close();
 
 
    }
 
    @Entity
    static class A {
        @OneToMany(cascade=CascadeType.ALL)
        List<B> list;
    }
 
    @Entity
    static class B {
    }
}

Since 2 B instances are created we expect that the database will contain 2 B instances at the end of the test. Until that fix 3 B instances were stored in the database, so this test failed with build 2.4.6_16 and passed with build 2.4.6_17. The fix includes using new entity objects without merging them, which affects your test case, and apparently against the JPA rules.

After considering this, we think that the old behavior is correct, so build 2.5.5_14 returns to the way that merge worked until 2.4.6_16. Although it looks weird,  storing 3 B instances can be explained. The second B instance is duplicated to a managed B instance in the cascading merge, and then also stored as a third instance during commit as part of the change in the list of the A instance.

This may affect some existing applications, so anyone that has an opinion regarding this case should respond to this thread.

ObjectDB Support
ObjectDB - Fast Object Database for Java (JPA/JDO)
support
support's picture
Joined on 2010-05-03
User Post #1,951
#3
2014-06-11 08:11

Support, tell me if i am wrong, i have just launched your test case with 2.4.5 version ( so before the fix ).

Test has run succesfully ...

I am a little bit confused.

xirt
xirt's picture
Joined on 2012-07-12
User Post #24
#4
2014-06-11 10:47

You are right, simplifying the test changed it. This is closer to the original test:

import javax.persistence.*;
 
import org.junit.Assert;
import org.junit.Test;
 
import java.util.*;
 
import javax.persistence.ManyToOne;
 
public class F799y {
 
    @Test
    public void test() {
 
        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory(
                "objectdb:$objectdb/test.tmp;drop");
        EntityManager em = emf.createEntityManager();
 
        em.getTransaction().begin();
        A a = new A();
        B b = new B();
        a.addB(b);
        em.persist(a);
        em.getTransaction().commit();
 
        a = em.createQuery("select a from A a", A.class).getSingleResult();
        Assert.assertNotNull(a);
        Assert.assertEquals(1, a.list.size());
 
        em.getTransaction().begin();
        b = new B();
        a.addB(b);
        a = em.merge(a);
        em.getTransaction().commit();
 
        List<B> resultList = em.createQuery("select B from B b", B.class).getResultList();
        Assert.assertEquals(2, resultList.size());
 
        em.clear();
        emf.close();
 
 
    }
 
    @Entity
    static class A {
        //@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "parent")
        @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
        private List<B> list = new ArrayList<B>();
 
        public void addB(B b) {
            list.add(b);
            b.parent = this;
        }
    }
 
    @Entity
    static class B {
        @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.ALL })
        private A parent;
    }
}

Implementing merge correctly is tricky and has changed several times.

Anyway, the question is what should be the right behavior. Build 2.5.5_14 will persist 3 B instances:

  1. First B instance is created and persisted in the first transaction.
  2. Second B instance is created in the merge operation in the second transaction. The operation is cascaded from a to b. a is already managed so it is not affected. b is not managed yet so a new managed B instances is created (with the content of b) and later stored in the database during commit.
  3. Third B instance is persisted during commit. It is the new B instance that was added in the second transaction to a, and it becomes managed during commit.

It looks strange, but does anyone see why it shouldn't work this way (following of course the JPA specification)?

 

 

ObjectDB Support
ObjectDB - Fast Object Database for Java (JPA/JDO)
support
support's picture
Joined on 2010-05-03
User Post #1,953
#5
2014-06-11 11:01

In this test case, JUnit give me the error ( tested with 2.4.5 ) :

"Attempt to persist a reference to a non managed F799y$B instance".

I was waiting to have 3 entities like expected before fix and not that error.

Well, in my opinion, you should strictly respect the JPA specifications no matter the results in some cases.

Edit :

Please give me a working test case ( with Assert fired on 3 B items ) and so we may discuss more about this behaviour :)

xirt
xirt's picture
Joined on 2012-07-12
User Post #25
#6
2014-06-11 11:19

It is difficult to track all the old changes in these versions, but the exception of 2.4.5 is clearly wrong. Probably it was also fixed later. In the original test version 2.4.5 fails on 3 B instances as expected.

Unless someone can convince us that this old/new behavior is wrong we will have to keep it this way, and just recommend being very careful with merge, especially when cascading is enabled and when managed and new instances are mixed.

In the last test (#2, #4), merge is not really needed.

It may be better to use merge only to attach detached objects, and without mixing already managed objects.

ObjectDB Support
ObjectDB - Fast Object Database for Java (JPA/JDO)
support
support's picture
Joined on 2010-05-03
User Post #1,954
#7
2014-06-11 11:41

Ok, no matter if the old / new behaviour is good or wrong.

But i think ObjectDb should compare to other JPA implementations and check if there is a "normal" and common behaviour.

For my original test case, i am pretty sure that "Attempt to reuse an existing primary key value" on my merge operation is a total nonsense.

If 2.5.5_14 fix that problem that's ok for my case.

 

 

xirt
xirt's picture
Joined on 2012-07-12
User Post #26

Post Reply

Please read carefully the posting instructions - before posting to the ObjectDB website.

  • You may have to disable pop up blocking in order to use the toolbar (e.g. in Chrome).
  • Use ctrl + right click to open the browser context menu in the editing area (e.g. for using a browser spell checker).
  • To insert formatted lines (e.g. Java code, stack trace) - select a style in the toolbar and then insert the text in the new created block.
  • Avoid overflow of published source code examples by breaking long lines.
  • You may mark in paragraph code words (e.g. class names) with the code style (can be applied by ctrl + D).
  • Long stack traces (> 50 lines) and complex source examples (> 100 lines) should be posted as attachments.
Attachments:
Maximum file size: 32 MB
Cancel