JPA/Hibernate однонаправленное однонаправленное отображение с общим первичным ключом

Мне очень тяжело пытаться получить однонаправленное отношение "один к одному" для работы с JPA (Provider: Hibernate). По-моему, это не должно быть слишком много хлопот, но, по-видимому, JPA/Hibernate не согласны с этим; -)

Проблема в том, что мне нужно сопоставить устаревшую схему, которую я не могу изменить, и что эта схема использует общий первичный ключ между двумя объектами, который одновременно является внешним ключом для одного объекта.

Я создал простую TestCase:

DB выглядит следующим образом:

CREATE TABLE PARENT (PARENT_ID Number primary key, Message varchar2(50));

CREATE TABLE CHILD (CHILD_ID Number primary key, Message varchar2(50),
CONSTRAINT FK_PARENT_ID FOREIGN KEY (CHILD_ID )REFERENCES PARENT (PARENT_ID));

CREATE SEQUENCE SEQ_PK_PARENT START WITH 1 INCREMENT BY 1 ORDER;

Родитель (= собственная сторона "один к одному" ) выглядит следующим образом:

@Entity
@Table(name = "PARENT")
public class Parent implements java.io.Serializable {       
    private Long parentId;
    private String message;
    private Child child;

    @Id
    @Column(name = "PARENT_ID", unique = true, nullable = false, precision = 22, scale = 0)
    @SequenceGenerator(name="pk_sequence", sequenceName="SEQ_PK_PARENT")
    @GeneratedValue(generator="pk_sequence", strategy=GenerationType.SEQUENCE)
    public Long getParentId() {
        return this.parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    @Column(name = "MESSAGE", length = 50)
    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @OneToOne (cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn(name="PARENT_ID", referencedColumnName="CHILD_ID")
    public Child getTestOneToOneChild() {
        return this.child;
    }

    public void setTestOneToOneChild(Child child) {
        this.child = child;
    }
}

Ребенок:

@Entity
@Table(name = "TEST_ONE_TO_ONE_CHILD", schema = "EXTUSER")
public class Child implements java.io.Serializable {    
    private static final long serialVersionUID = 1L;
    private Long childId;       

    private String message;

    public Child() {
    }

    public Child(String message) {
        this.message = message;
    }

    @Id
    @Column(name = "CHILD_ID")    
    public Long getChildId() {
        return this.childId;
    }

    public void setChildId(Long childId) {
        this.childId = childId;
    }

    @Column(name = "MESSAGE", length = 50)
    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Я полностью вижу проблему, что JPA не знает, как назначить id для ребенка. Однако я также пробовал использовать "чужой" генератор ключей Hibernates, также безуспешно, потому что нужно иметь обратную ссылку на родителя от ребенка, что нежелательно. Эта проблема не кажется мне слишком необычной, так что мне здесь не хватает? Есть ли решение? Я также могу использовать расширения для спящего режима, если чистая JPA не предоставляет решение.

Мои ожидания относительно правильного поведения: Если я попытаюсь сохранить родителя с прикрепленным дочерним элементом:

  • получить идентификатор из последовательности, установить его на родительский
  • persist parent
  • установить родительский идентификатор для дочернего
  • упорный ребенок

Если я попытаюсь сохранить "автономный" дочерний элемент (например, entityManager.persist(aChild)), я бы ожидал исключение RuntimeException.

Любая помощь очень ценится!

Ответ 1

В приведенной ниже схеме db вы можете использовать аннотацию @MapsId в зависимом классе (ваш класс Child), чтобы получить обратное отображение родительский, например:

@Entity
class Parent {
  @Id
  @Column(name = "parent_id")
  @GeneratedValue 
  Long parent_id;
}

@Entity
class Child {
  @Id
  @Column(name = "child_id")
  Long child_id;

  @MapsId 
  @OneToOne
  @JoinColumn(name = "child_id")
  Parent parent;
}

Добавляя сопоставление от родителя к дочернему, вы используете аннотацию @PrimaryKeyJoinColumn, как вы указали, делая полное двунаправленное сопоставление "один-к-одному" выглядит следующим образом:

@Entity
class Parent {
  @Id
  @Column(name = "parent_id")
  @GeneratedValue 
  Long parent_id;

  @OneToOne
  @PrimaryKeyJoinColumn(name="parent_id", referencedColumnName="child_id")
  public Child;
}

@Entity
class Child {
  @Id
  @Column(name = "child_id")
  Long child_id;

  @MapsId 
  @OneToOne
  @JoinColumn(name = "child_id")
  Parent parent;
}

Я использовал поле, а не доступ к методу (и удалил все посторонние отношения), но это будут те же аннотации, которые применяются к вашим геттерам.

Также см. последний бит раздела 2.2.3.1 здесь для другого примера @MapsId.

Ответ 2

потому что нужно иметь обратную ссылку на родителя от дочернего, что нежелательно

Хорошо, если ребенок может существовать только в том случае, если есть родитель, то между ними существует взаимосвязь. Вы можете просто не хотеть выразить в OO, но он существует в реляционной модели.

Тем не менее, я бы сказал, что естественным решением для этого является родитель в Ребенке.

Но если вы действительно не хотите этого делать, я бы предложил взглянуть на сопоставление идентификатора как класса PK и поделиться им с обоими классами, используя @EmbeddedId. Я уверен, что это решит вашу проблему, за одним исключением:

Если я попытаюсь сохранить "автономный" дочерний элемент (например, entityManager.persist(aChild)), я бы ожидал исключение RuntimeException.

Если вы решили использовать подход @EmbeddedId в классе PK, я думаю, вам нужно обработать вышеуказанный случай как "бизнес-правило".

Ответ 3

Решение этой проблемы использует аннотацию @PostPersist для родительской Entity. Вам необходимо создать метод в родительском классе сущности и аннотировать его с помощью @PostPersist, поэтому этот метод будет вызываться после того, как родительский объект будет сохранен, и в этом методе просто установите идентификатор класса дочерних сущностей. См. Пример ниже.

@Entity
@Table(name = "PARENT")
public class Parent implements java.io.Serializable {       
    private Long parentId;
    private String message;
    private Child child;

    @Id
    @Column(name = "PARENT_ID", unique = true, nullable = false, precision = 22, scale = 0)
    @SequenceGenerator(name="pk_sequence", sequenceName="SEQ_PK_PARENT")
    @GeneratedValue(generator="pk_sequence", strategy=GenerationType.SEQUENCE)
    public Long getParentId() {
        return this.parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    @OneToOne (cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    public Child getTestOneToOneChild() {
        return this.child;
    }

    public void setTestOneToOneChild(Child child) {
        this.child = child;
    }


   @PostPersist
    public void initializeCandidateDetailID()
    {
        System.out.println("reached here");// for debugging purpose
        this.child.setChildId(parentId); // set child id here
        System.out.println("reached here"+Id); // for debugging purpose
    }
}