如何坚持使用@JoinFormula的逆@OneToOne映射?

时间:2013-11-06 16:56:54

标签: java hibernate jpa

我试图在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,
    ...
);

二进制表应该保存任意其他表的二进制数据,“外键” - 虽然不是数据库术语 - 由binary.entity_classbinary.entity_id组成。这是我现在必须接受的一种结构,它似乎在这引起混淆。列binary.entity_id引用聚合表的主键,而binary.entity_class定义聚合表本身:

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

当使用只读时,ContainerEntity中的映射已经处于工作状态:

@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;
}

但是我在持久化ContainerEntity时遇到了问题:

  • 如果我只是指定CascadeType.PERSIST,则Hibernate无法设置binary.entity_id
  • 如果我没有级联持久,我不知道何时自己设置binary.entity_id,如何持久保存映射对象,我最终得到:

    org.hibernate.TransientObjectException:object引用未保存的瞬态实例 - 在刷新之前保存瞬态实例:ContainerEntity.binary - >二进制

换句话说,我想但是目前无法坚持这样的两个实体:

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

任何想法或有用的建议?


关于赏金的注意事项:这个问题没有答案,我可以接受为'正确',尽管还有一个提示,我将在下周检查。我的赏金时间虽然结束了,所以我会把它奖励到目前为止最接近的答案。

4 个答案:

答案 0 :(得分:2)

好的,请尝试以下我认为完美的方法。我已经测试过,可以按预期加载和保存Container和相关实体。

首先,容器必须从一些常见的实体扩展:

@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();
}

混凝土容器A.必须将关系映射为OneToMany,但是附加的@Where子句(以及您的数据库唯一键)有效地使其成为@OneToOne。这个类的客户端可以将其视为单端关联:

@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";
    }
}

从BinaryData到Container的映射形式需要使用Hibernate的@Any映射。

@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;
    }
}

以下测试按预期传递:

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" };
    }
}

使用以下数据集时:

<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>

答案 1 :(得分:1)

你的问题的根源是Hibernate是一个对象关系映射器,这个数据库并不是真正的关系。具体而言,使用列中的值来引用表名称不属于关系模型。理想情况下,您可以通过更改架构来解决此问题,但听起来这是不可能的。

您是否坚持自动生成ID?如果没有,您可以在持久化之前为应用程序代码中的容器实体生成ID,并在附加它们时将它们复制到Binary个对象中。

如果您需要自动生成,您可能尝试的是级联持久性,并在ContainerEntityA上使用@PostPersist生命周期回调方法将生成的ID复制到Binary 。像这样:

@Entity
public class ContainerEntityA {
    @PostPersist
    public void copyIdToBinary() {
        binary.setEntityId(id);
    }
}

您可以确定此方法将看到ID的生成值; spec个状态(在第3.5节“实体监听器和回调方法”中):

  

PostPersist方法中提供了生成的主键值。

但是,您无法确定的是,Binary的{​​{1}}字段的更新将在保留之前发生。该规范警告说:

  

通常,便携式应用程序的生命周期方法不应该访问其他实体实例

  

在将生命周期事件级联到相关实体之前或之后是否调用回调方法与实现有关。应用程序不应该依赖于此顺序。

所以,这不是一个很好的解决方案。它不便携,也可能不起作用。但它可能适用于您的特定版本的Hibernate,如果确实如此,它可能是您可以在不更改架构的情况下使用的最干净的解决方案。

答案 2 :(得分:0)

您没有切换连接列名称和referenceColumnName吗? 像这样:

@JoinColumn(name = "entity_id", referencedColumnName = "id", insertable=false, updatable=false))

答案 3 :(得分:0)

Idea1(不起作用): 您是否尝试从insertable=false删除insertable(即不应该@JoinColumn为真?)并在@OneToOne注释中添加mappingBy =“entityId”?

Idea2 :(您可能会更改保存实体的顺序)。

1.删除级联

2

containerEntity = new ContainerEntity();
entityManager.persist(containerEntity);
binary = new Binary();
containerEntity.set( binary );
entityManager.persist(binary);
entityManager.merge(containerEntity);

....  in ContainerEntity
public void setBinary(Binary aBinary) {
  aBinary.setEntityClass("ContainerEntity");
  aBinary.setEntityId(this.id);
  this.binary = aBinary;
}

<强> Idea3: 指定CascadeType.PERSIST&amp;例如,将binary.entityId设置为0。 @PostPersistEntityContainer将其二进制文件的entityId设置为正确的值。这会使您的数据库暂时不一致,但如果您在事务中执行此操作,则不会出现问题。