JPA, смешанный суррогатный ключ с внешним ключом и порядковым номером

У меня есть две таблицы:

DOCUMENT
--------
DOC_ID (PK)
.
.
.

SECTION
-------
DOC_ID (FK, PK)
SECTION_NUM (PK)
.
.
.

Записи в базе данных могут выглядеть так:

Документ:

DOC_ID | . . .
--------------
1      | . . .
2      | . . .

Раздел:

DOC_ID | SECTION_NUM | . . .
---------------------------
1      | 1           | . . .
1      | 2           | . . .
1      | 3           | . . .
2      | 1           | . . .

Document имеет сгенерированный идентификатор для DOC_ID, а Section имеет составной первичный ключ для DOC_ID и SECTION_NUM.

SECTION_NUM — это локально (приложением) генерируемый порядковый номер, начиная с нового для каждого документа.

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

@Entity
@Table(name = "DOCUMENT")
public class Document implements java.io.Serializable {
    @Id
    @Column(name = "DOC_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "DocIdSeq")
    @SequenceGenerator(name = "DocIdSeq", sequenceName = "DOC_ID_SEQ", allocationSize = 1)
    private Long docId;
}


@Entity
@Table(name = "SECTION")
@IdClass(SectionId.class)
public class Section implements java.io.Serializable {
    @Id
    @Column(name = "DOC_ID", nullable = false)
    private Long docId;

    @Id
    @Column(name = "SECTION_NUM", nullable = false)
    private Integer sectionNum;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "DOC_ID")
    private Document document;
}

public class SectionId implements java.io.Serializable {
    private Long docId;
    private Integer sectionNum;
}

При вставке нового документа и связанного с ним раздела я делаю следующее:

Document doc = new Document();

Section section = new Section();
section.setDocument(doc);
section.setSectionNum(1);

entityManager.persist(doc);

При сохранении я получаю исключение о том, что NULL не разрешен для столбца SECTION_NUM. Я использую OpenEJB (который полагается на OpenJPA за кулисами для модульного тестирования) и обнаружил, что при выполнении кода OpenJPA он успешно сохраняет объект Document, но когда дело доходит до объекта Section, он рефлективно создает новый экземпляр и устанавливает все со значением null, поэтому потеря значения sectionNum перед связыванием с объектом Document сохранялась ранее.

К сожалению, я не могу изменить схему БД, так как это устаревшая система. Кто-нибудь делал что-то подобное и заработало?


person superkruger    schedule 30.11.2010    source источник


Ответы (2)


Давно собирался обновить, но был слишком занят...

Итак, оказывается, это невозможно с JPA. Однако есть обходной путь.

Ранее я упоминал, что класс Document выглядит так.

@Entity
@Table(name = "DOCUMENT")
public class Document implements java.io.Serializable {
    @Id
    @Column(name = "DOC_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
    "DocIdSeq")
    @SequenceGenerator(name = "DocIdSeq", sequenceName = "DOC_ID_SEQ", allocationSize = 1)
    private Long docId;
}

Это была просто сокращенная версия, чтобы прояснить вопрос. В реальном классе также есть коллекция Sections:

@Entity
@Table(name = "DOCUMENT")
public class Document implements java.io.Serializable {
    @Id
    @Column(name = "DOC_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"DocIdSeq")
    @SequenceGenerator(name = "DocIdSeq", sequenceName = "DOC_ID_SEQ", allocationSize = 1)
    private Long docId;

    @OneToMany
    private Set<Section> sections = new HashSet<Section>(0);
}

Если бы у Section был простой первичный ключ, JPA легко обработала бы отношения, так как приняла бы идентификатор от приложения или сгенерировала бы его из последовательности, но не будет делать и то, и другое с одним идентификатором.

Итак, решение состоит в том, чтобы самостоятельно управлять отношениями и добавить функцию жизненного цикла:

@Entity
@Table(name = "DOCUMENT")
public class Document implements java.io.Serializable {
    @Id
    @Column(name = "DOC_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
    "DocIdSeq")
    @SequenceGenerator(name = "DocIdSeq", sequenceName = "DOC_ID_SEQ", allocationSize = 1)
    private Long docId;

    @Transient
    private Set<Section> sections = new HashSet<Section>(0);

    @PostPersist
    public void updateChildIds() {
        for (Section section : this.sections) {
            section.getId().setDocId(this.docId);
        }
    }
}

Как видите, связь Section теперь является Transient, что означает, что JPA не будет ею управлять. После сохранения документа платформа вызовет функцию updateChildIds, в которой вы вручную обновите идентификатор раздела с помощью нового сохраненного идентификатора документа.

Это можно продемонстрировать на следующем фасаде:

@Stateless
public void DocumentFacade implements DocumentFacadeLocal {

    @PersistenceContext
    private EntityManager entityManager;

    public void save(Document entity) throws Exception {
        this.entityManager.persist(entity);
        this.entityManager.flush();
        this.persistTransientEntities(entity);
        this.entityManager.flush();
    }

    private void persistTransientEntities(CaseInstructionSheet entity) {
       for (Section section : entity.getSections()) {
            this.entityManager.persist(section);
        }
    }
}
person superkruger    schedule 29.12.2010

На самом деле, JPA прекрасно справляется с этим. Аннотация, которую вы ищете, это MapsId.

В вашем случае в вашем Section на docId вам просто нужно добавить следующее:

@MapsId("docId")

Значение аннотации MapsId — это имя атрибута вашего составного первичного ключа (в данном случае это то же самое).

person Guillaume Polet    schedule 21.06.2013
comment
Как вы думаете, может ли @MapsId быть полезным в моем случае? stackoverflow.com/questions/46159867 Основное отличие от оригинального постера заключается в использовании OneToOne вместо ManyToOne. - person gouessej; 14.09.2017