JPA, Mixed surrogate key with foreign key and sequence number
I've got two tables:
DOCU开发者_Python百科MENT
--------
DOC_ID (PK)
.
.
.
SECTION
-------
DOC_ID (FK, PK)
SECTION_NUM (PK)
.
.
.
Entries in the database might look like this:
Document:
DOC_ID | . . .
--------------
1 | . . .
2 | . . .
Section:
DOC_ID | SECTION_NUM | . . .
---------------------------
1 | 1 | . . .
1 | 2 | . . .
1 | 3 | . . .
2 | 1 | . . .
Document
has a generated Id on DOC_ID, while Section
has a composite primary key over DOC_ID and SECTION_NUM.
SECTION_NUM is a locally(application) generated sequence number starting fresh for every document.
My entity classes look as follows:
@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;
}
When inserting a new Document and related Section, I do the following:
Document doc = new Document();
Section section = new Section();
section.setDocument(doc);
section.setSectionNum(1);
entityManager.persist(doc);
When persisting I get an exception stating that NULL is not allowed for column SECTION_NUM. I'm using OpenEJB (which relies on OpenJPA behind the scenes for unit testing), and found when stepping through OpenJPA code that it successfully persists the Document object, but when it comes to the Section object it creates a new instance reflectively and sets all fields to null, so losing the sectionNum value, before linking it to the Document object persisted earlier.
Unfortunately I can't change the DB schema, as it's a legacy system. Has anybody done something similar and got it working?
I've been meaning to update this for some time, but been too busy...
Ok, so it turns out this isn't really possible with JPA. However, there is a workaround.
Previously I mentioned that the Document class looks like this.
@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;
}
That was just a shortened version to clarify the issue. The real class has a collection of Sections too:
@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);
}
If Section had a simple primary key, JPA would easily handle the relationship, as it would accept an id from the application, or generate it from a sequence, but it won't do both with one id.
So, the solution is to manage the relationship yourself, and add a lifecycle function:
@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);
}
}
}
As you can see, the Section relationship is now Transient, meaning JPA won't manage it. After persisting a Document, the framework will call the updateChildIds function, where you manually update the Section id's with the newly persisted Document id's.
This could be demonstrated in the following facade:
@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);
}
}
}
Actually, JPA is perfectly able to handle this. The annotation you are looking for is MapsId
.
In your case, in your Section
, on the docId
you simply need to add the following:
@MapsId("docId")
The value of the MapsId
annotation is the attribute name of your compound primary key (which in this case is the same)
精彩评论