8

When I am trying to save a ManyToMany relation I get a database exception:

Exception in thread "main" javax.persistence.PersistenceException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.6.2.v20151217-774c696): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: org.h2.jdbc.JdbcSQLException: NULL not allowed for column "INVERSES_ID"; SQL statement:
INSERT INTO m2m_owner_inverse (inverses_ID, owners_ID) VALUES (?, ?) [23502-184]
Error Code: 23502
Call: INSERT INTO m2m_owner_inverse (inverses_ID, owners_ID) VALUES (?, ?)
    bind => [null, 1]

It is strange since I see in the log that both of the instances are inserted:

[EL Fine]: sql: 2016-02-22 13:18:32.67--ClientSession(1546269015)--Connection(1894667608)--INSERT INTO M2MOWNER (NAME) VALUES (?)
    bind => [null]
[EL Fine]: sql: 2016-02-22 13:18:32.676--ClientSession(1546269015)--Connection(1894667608)--CALL IDENTITY()
[EL Fine]: sql: 2016-02-22 13:18:32.716--ClientSession(1546269015)--Connection(1894667608)--INSERT INTO M2MINVERSE (NAME) VALUES (?)
    bind => [null]
[EL Fine]: sql: 2016-02-22 13:18:32.718--ClientSession(1546269015)--Connection(1894667608)--CALL IDENTITY()

The ChangeTracking is an eclipselink feature and the application needs a javaagent to run:

java -javaagent:eclipselink.jar

Without ChangeTracking and the agent it works as expected, the insertion happens. It also works if I save the owner side (see the commented line)

The files can be found on github: https://github.com/zbiro/many2many

The sample can be started with

gradle start

The java files:

public class M2MTest {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("m2m-pu");
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        M2MOwner owner = em.merge(new M2MOwner());
        em.flush();

        M2MInverse inverse = new M2MInverse();
        owner.getInverses().add(inverse);
        inverse.getOwners().add(owner);
        inverse = em.merge(inverse); // does not work if agent is used
        //owner = em.merge(owner); // works in all cases
        em.flush();

        em.getTransaction().commit();
        em.close();
        emf.close();
    }
}

@Entity
@ChangeTracking(ChangeTrackingType.ATTRIBUTE)
public class M2MOwner {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @ManyToMany(cascade={CascadeType.ALL})
    @JoinTable(name="m2m_owner_inverse")
    private Set<M2MInverse> inverses = new HashSet<>();

    public Set<M2MInverse> getInverses() {
        return inverses;
    }
}

@Entity
@ChangeTracking(ChangeTrackingType.ATTRIBUTE)
public class M2MInverse {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @javax.persistence.ManyToMany( cascade = { CascadeType.ALL }, mappedBy="inverses")
    private Set<M2MOwner> owners = new HashSet<>();

    public Set<M2MOwner> getOwners() {
        return owners;
    }
}

persistence.xml

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="m2m-pu" transaction-type="RESOURCE_LOCAL">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>

        <class>M2MOwner</class>
        <class>M2MInverse</class>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:tests;LOCK_TIMEOUT=10000" />
            <property name="javax.persistence.jdbc.user" value="sa" />
            <property name="eclipselink.ddl-generation" value="drop-and-create-tables" />
            <property name="eclipselink.ddl-generation.output-mode" value="database" />
            <property name="eclipselink.logging.parameters" value="true" />
            <property name="eclipselink.logging.level.sql" value="FINEST" />
        </properties>
    </persistence-unit>
</persistence>

build.gradle

apply plugin: "java"

repositories {
    mavenCentral()
}

configurations {
    eclipseLink { transitive = false }
}

ext.libs = [:]
libs.eclipseLink = 'org.eclipse.persistence:eclipselink:2.6.2'

dependencies {
    compile 'com.h2database:h2:1.4.191'
    compile libs.eclipseLink
    eclipseLink libs.eclipseLink
}

task start(type: JavaExec) {
    dependsOn build
    classpath = sourceSets.main.runtimeClasspath
    main = 'M2MTest'
    jvmArgs = ["-javaagent:${configurations.eclipseLink.singleFile}"]
}

My question is why it is happening and how I could avoid it.

ZBiro
  • 81
  • 4
  • What is the ID that is generated for the M2MInverse instance? Does it work when calling persist instead of merge on inverse? How about with something other then IDENTITY? What happens during the flush if you don't explicitly call merge on inverse? – Chris Feb 22 '16 at 15:42
  • - I dont really know how to find out the inserted id (there is only a "CALL IDENTITY() sql) - it fails the same way with GenerationType.TABLE - it works with persist - it works without the second merge I have figure out what these two things mean – ZBiro Feb 22 '16 at 17:15
  • I suggest you try without using either merge - the owner instance you are adding the new inverse instance to should already be managed, so changes to it should be picked up on flush/commit. I'm guessing this is a bug where change tracking isn't correctly correlating the inverse instance in the collection with the one merge has been called on. Persist is different to merge in that it makes the instance passed in managed, while merge makes a copy. The copy will have the ID assigned, while the referenced instance will still have its ID left as null. – Chris Feb 22 '16 at 19:17

0 Answers0