ebean-orm / ebean

Ebean ORM
https://ebean.io
Apache License 2.0
1.46k stars 260 forks source link

save() on a reference bean causes insert to occur ... results in duplicate key exception #1617

Closed rbygrave closed 5 years ago

rbygrave commented 5 years ago

Steps to reproduce

    LoggedSqlCollector.start();

    EPersonOnline bean = Ebean.getReference(EPersonOnline.class, 1L);
    Ebean.save(bean); // should NOT insert really but it does ...

    List<String> sql = LoggedSqlCollector.current();
    assertThat(sql).hasSize(0);

Alternate steps to reproduce

Test case provided with 3 levels of OneToOne relationships. The lowest level (ClassC now called OtoLevelC) is a reference bean ... and when that is saved we see the error.


public class TestOneToOneSaveWithoutChanges {

  @Test
  public void testSave3Levels() {

    OtoLevelA a = new OtoLevelA("A");
    a.setB(new OtoLevelB("B"));
    a.getB().setC(new OtoLevelC("C"));

    Ebean.save(a);

    OtoLevelA dbA = Ebean.find(OtoLevelA.class, 1);
    OtoLevelB dbB = dbA.getB();
    OtoLevelC dbC = dbB.getC();

    LoggedSqlCollector.start();

    Ebean.save(dbA);
    Ebean.save(dbB);
    Ebean.save(dbC);

    List<String> sql = LoggedSqlCollector.stop();
    assertThat(sql).hasSize(0);
  }
}

io.ebean.DuplicateKeyException: Error[Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.OTO_LEVEL_C(ID)"; SQL statement: insert into oto_level_c (id, name) values (?,?) [23505-197]]

    at io.ebean.config.dbplatform.SqlCodeTranslator.translate(SqlCodeTranslator.java:46)
    at io.ebean.config.dbplatform.DatabasePlatform.translate(DatabasePlatform.java:219)
    at io.ebeaninternal.server.persist.dml.DmlBeanPersister.execute(DmlBeanPersister.java:83)
    at io.ebeaninternal.server.persist.dml.DmlBeanPersister.insert(DmlBeanPersister.java:49)
    at io.ebeaninternal.server.core.PersistRequestBean.executeInsert(PersistRequestBean.java:1280)
    at io.ebeaninternal.server.core.PersistRequestBean.executeNow(PersistRequestBean.java:790)
    at io.ebeaninternal.server.core.PersistRequestBean.executeNoBatch(PersistRequestBean.java:845)
    at io.ebeaninternal.server.core.PersistRequestBean.executeOrQueue(PersistRequestBean.java:836)
    at io.ebeaninternal.server.persist.DefaultPersister.insert(DefaultPersister.java:506)
    at io.ebeaninternal.server.persist.DefaultPersister.insert(DefaultPersister.java:454)
    at io.ebeaninternal.server.persist.DefaultPersister.save(DefaultPersister.java:438)
    at io.ebeaninternal.server.core.DefaultServer.save(DefaultServer.java:1745)
    at io.ebeaninternal.server.core.DefaultServer.save(DefaultServer.java:1737)
    at io.ebean.Ebean.save(Ebean.java:565)
    at org.tests.o2o.TestOneToOneSaveWithoutChanges.testSave3Levels(TestOneToOneSaveWithoutChanges.java:32)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.OTO_LEVEL_C(ID)"; SQL statement:
insert into oto_level_c (id, name) values (?,?) [23505-197]
    at org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
    at org.h2.message.DbException.get(DbException.java:179)
    at org.h2.message.DbException.get(DbException.java:155)
    at org.h2.mvstore.db.MVPrimaryIndex.add(MVPrimaryIndex.java:123)
    at org.h2.mvstore.db.MVTable.addRow(MVTable.java:732)
    at org.h2.command.dml.Insert.insertRows(Insert.java:182)
    at org.h2.command.dml.Insert.update(Insert.java:134)
    at org.h2.command.CommandContainer.update(CommandContainer.java:102)
    at org.h2.command.Command.executeUpdate(Command.java:261)
    at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:199)
    at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:153)
    at io.ebean.datasource.pool.ExtendedPreparedStatement.executeUpdate(ExtendedPreparedStatement.java:148)
    at io.ebeaninternal.server.type.DataBind.executeUpdate(DataBind.java:92)
    at io.ebeaninternal.server.persist.dml.InsertHandler.execute(InsertHandler.java:124)
    at io.ebeaninternal.server.persist.dml.DmlBeanPersister.execute(DmlBeanPersister.java:73)
    ... 34 more
frensjan commented 5 years ago

@rbygrave I have a use case where some classes / tables are merely serve as join tables with inheritance type information. Can you indicate how these should be handled? As they only define an identifier and a discriminator value, they can't be saved with the ID set.

I can create a new issue if that's preferred.


Background:

My data model is graph like with inheritance trees for Vertex and Edge. For example, the Vertex class is along the lines of:

@Entity
@Inheritance
public abstract class Vertex {

    @Id
    private UUID id;

    public Vertex() {
        this(UUID.randomUUID());
    }

    public Vertex(UUID id) {
        this.id = id;
    }

    public UUID getId() {
        return id;
    }
}

The create statement for the vertex table is:

create table vertex (
  type                          varchar(31) not null,
  id                            uuid not null,
  constraint pk_vertex primary key (id)
);

Note that all classes inheriting from Vertex do add properties, but they are all 'multi-valued' and are modelled at the Ebean level as @OneToMany associations.

When saving a vertex with only an ID set, it doesn't get saved because it is marked as a reference bean; e.g.:

Person person = new Person(UUID.randomUUID()); // Person extends Vertex
database.save(person);
assertThat(database.find(Vertex.class, person.getId())).isEqualTo(person); // fails
assertThat(database.find(Person.class, person.getId())).isEqualTo(person); // and this then obviously also fails

The 'culprit' for inserts seems to be in DefaultPersister.insert(EntityBean, Transaction)`:

  public void insert(EntityBean bean, Transaction t) {
    PersistRequestBean<?> req = createRequest(bean, t, PersistRequest.Type.INSERT);
    if (req.isReference()) {
      // skip insert on reference bean
      return;
    }
    ...
rbygrave commented 5 years ago

Note that all classes inheriting from Vertex do add properties, but they are all 'multi-valued' and are modelled at the Ebean level as @OneToMany associations.

Ah right, I see.

Ok, we need to log this as a new issue (related for sure) ... and I'm thinking that Ebean would need to change to detect this case where there are only ToMany properties ... and for those types of beans we can't treat "new beans with only id property" as reference beans but instead use the bean state.

Can you log a new issue with the same details you have posted here?

Thanks, Rob. Share the love, give Ebean a github star.