Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to persist with inverse @OneToOne mapping involving @JoinFormula?

I am trying to map the relation between the following tables in Hibernate:

create table binary (
    id number not null primary key,
    data blob,
    entity_class varchar(255) not null,
    entity_id number not null,
    unique (entity_id, entity_class)
);

create table container_entity (
    id number not null primary key,
    ...
);

The binary table is supposed to hold binary data for arbitrary other tables, the "foreign key" - though not in database terms - is composed of binary.entity_class and binary.entity_id. This is a construct that I have to accept for now and it seems to be causing confusion here. The column binary.entity_id references the primary key of the aggregated table, while binary.entity_class defines the aggregated table itself:

BINARY                               CONTAINER_ENTITY_A  CONTAINER_ENTITY_B 
id  entity_class      entity_id      id                  id                    ...
-------------------------------      ------------------  ------------------
1   ContainerEntityA  1          ->  1                                         ...
2   ContainerEntityB  1          ->                      1
3   ContainerEntityB  2          ->                      2

The mapping in ContainerEntity is already working find when used read-only:

@Entity @Table(name="container_entity_a")
public class ContainerEntityA {
  @Id @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  @OneToOne
  @JoinColumnsOrFormulas({ 
    @JoinColumnOrFormula(column = 
      @JoinColumn(name = "id", referencedColumnName = "entity_id", 
        insertable=false, updatable=false)),
    @JoinColumnOrFormula(formula = 
      @JoinFormula(value = "'ContainerEntityA'", referencedColumnName = "entity_class")) 
  })
  private Binary binary;

  public void setBinary(Binary aBinary) {
    aBinary.setEntityClass("ContainerEntityA");
    this.binary = aBinary;
  }
}

@Entity @Table(name="binary")
public class Binary {
  @Column(name = "entity_id", nullable = false)
  private Long entityId;

  @Column(name = "entity_class", nullable = false)
  private String entityClass;
}

But I am having problems persisting ContainerEntity:

  • If I just specify CascadeType.PERSIST Hibernate fails to set binary.entity_id.
  • If I do not cascade-persist, I don't know when to set binary.entity_id myself, how to persist the mapped object, and I end up with:

    org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: ContainerEntity.binary -> Binary

In other words, I would like but currently fail to persist both entities like this:

containerEntity = new ContainerEntity();
containerEntity.setBinary( new Binary() );
entityManager.persist(containerEntity);

Any ideas or helpful suggestions?


Note on the bounty: There is no answer to this question yet that I can accept as 'correct', although there is one more hint that I'll check on next week. The time for my bounty is over though, so I'll award it to the answer coming closest so far.

like image 535
cg. Avatar asked Jan 17 '26 00:01

cg.


1 Answers

Okay, please try the following which I think works perfectly. I have tested and can load and save the Container and associated entities as expected.

Firstly Containers will have to extend from some common Entity:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Container {

    //cannot use identity here however a table or sequence should work so long as
    //the initial value is > current max ids from all container tables.
    @Id
    @TableGenerator(initialValue = 10000, allocationSize = 100, table = "id_gen", name = "id_gen")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "id_gen")
    private Long id;

    public Long getId() {
        return id;
    }

    public BinaryData getBinaryData() {
        return getData().size() > 0 ? getData().get(0) : null;
    }

    public void setBinaryData(BinaryData binaryData) {
        binaryData.setContainerClass(getName());
        binaryData.setContainer(this);

        this.getData().clear();
        this.getData().add(binaryData);
    }

    protected abstract List<BinaryData> getData();

    protected abstract String getName();
}

Concrete ContainerA. The relationship has to be mapped as a OneToMany however an additional @Where clause (and your database unique key) effectively makes this a @OneToOne. Clients of this class can see it as a single ended association:

@Entity
@Table(name = "container_a")
public class ContainerA extends Container {

    @OneToMany(mappedBy = "container", cascade = CascadeType.ALL)
    @Where(clause = "container_class = 'container_a'")
    private List<BinaryData> binaryData;

    public ContainerA() {
        binaryData = new ArrayList<>();
    }

    @Override
    protected List<BinaryData> getData() {
        return binaryData;
    }

    @Override
    protected String getName() {
        return "container_a";
    }
}

ContainerB

@Entity
@Table(name = "container_b")
public class ContainerB extends Container {

    @OneToMany(mappedBy = "container", cascade = CascadeType.ALL)
    @Where(clause = "container_class = 'container_b'")
    private List<BinaryData> binaryData;

    public ContainerB() {
        binaryData = new ArrayList<>();
    }

    @Override
    protected List<BinaryData> getData() {
        return binaryData;
    }

    @Override
    protected String getName() {
        return "container_b";
    }
}

The mapping back form BinaryData to Container requires use of Hibernate's @Any mapping.

@Entity
@Table(name = "binary_data")
public class BinaryData {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @OneToOne
    @Any(metaColumn = @Column(name = "container_class"))
    @AnyMetaDef(idType = "long", metaType = "string", metaValues = {
            @MetaValue(targetEntity = ContainerA.class, value = "container_a"),
            @MetaValue(targetEntity = ContainerB.class, value = "container_b") })
    @JoinColumn(name = "entity_id")
    private Container container;

    @Column(name = "container_class")
    private String containerClass;

    public Long getId() {
        return id;
    }

    public Container getContainer() {
        return container;
    }

    public void setContainer(Container container) {
        this.container = container;
    }

    public String getContainerClass() {
        return containerClass;
    }

    public void setContainerClass(String containerClass) {
        this.containerClass = containerClass;
    }
}

The following tests passed as expected:

public class ContainerDaoTest extends BaseDaoTest {

    @Test
    public void testSaveEntityA() {

        ContainerA c = new ContainerA();

        BinaryData b = new BinaryData();
        c.setBinaryData(b);

        ContainerDao dao = new ContainerDao();
        dao.persist(c);

        c = dao.load(c.getId());
        Assert.assertEquals(c.getId(), b.getContainer().getId());
    }

    @Test
    public void testLoadEntity() {
        ContainerA c = new ContainerDao().load(2l);
        Assert.assertEquals(new Long(3), c.getBinaryData().getId());
        Assert.assertEquals(new Long(2), c.getBinaryData().getContainer().getId());
        Assert.assertEquals("container_a", c.getBinaryData().getContainerClass());
    }

    @Override
    protected String[] getDataSetPaths() {
        return new String[] { "/stack/container.xml", "/stack/binarydata.xml" };
    }
}

When using the following datasets:

<dataset>
    <container_a id="1" />
    <container_a id="2" />
    <container_b id="1" />
    <container_b id="2" />
</dataset>

<dataset>
    <binary_data id="1" container_class="container_a" entity_id="1" />
    <binary_data id="2" container_class="container_b" entity_id="2" />
    <binary_data id="3" container_class="container_a" entity_id="2" />
    <binary_data id="4" container_class="container_b" entity_id="1" />
</dataset>
like image 90
Alan Hay Avatar answered Jan 19 '26 12:01

Alan Hay



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!