JPA, Mixed surrogate key with foreign key and sequence number - jpa

I've got two tables:
DOCUMENT
--------
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)

Related

How to keep data in sync with JPA across entities in session cache?

Im trying to figure out how to keep data in sync in the session cache. I have the following example:
#Table (name = "language")
#Entity
public class Language
{
#Id
#Column (name = "key", unique = true, nullable = false)
private String key;
#Column (name = "name", unique = true, nullable = false)
private String name;
#OneToMany (mappedBy = "language", cascade = { CascadeType.DETACH, CascadeType.REFRESH, CascadeType.REMOVE })
private Set<Translation> translations;
}
#Table (name = "translation")
#Entity
public class Translation
{
#Id
#GeneratedValue (strategy = GenerationType.SEQUENCE)
private Long id;
#ManyToOne(optional = false)
private Language language;
#ManyToOne (optional = false)
#JoinColumn(referencedColumnName = "key")
private TranslatableEntity translatable;
#Column(name = "value")
private String value;
}
#Table (name = "translatable")
#Entity
public class Translatable
{
#Id
#GeneratedValue (strategy = GenerationType.SEQUENCE)
private Long id;
#NotNull
#Size (max = 255)
#Column (name = "key", unique = true, nullable = false)
private String key;
#OneToMany (mappedBy = "translatable", cascade = { CascadeType.DETACH, CascadeType.REFRESH, CascadeType.REMOVE, CascadeType.PERSIST })
#MapKey (name = "language")
private Map<Language, Translation> translations;
}
So basically I do:
// Print current translations for en language
Language language = languageRepository.getOne("en");
printTranslations(language);
// Add new translatable object with translation for english language
Translatable translatable = new Translatable();
translatable.addTranslation("en", "...")
translatableRepository.saveAndFlush(translatable)
// Again print translations for en language
// This still prints outdated information
language = languageRepository.getOne("en");
printTranslations(language);
So my question is how to keep data consistent.
When inserting/removing a new translatable with some translations the translation list in Language instances are not updated in the session cache.
I could not find any satisfactory answer to this. This one came closest: Keeping entity relationship in sync when deleting child in JPA.
Thanks
JPA doesn't maintain the two sides of a bidirectional relationship for you.
And the purpose of the first level cache is that within a transaction entities get loaded only once.
This gives you two options to solve the problem:
maintain both sides of the relationship yourself, for example by implementing the Translatable.add method so that it updates Language.translations and Translation.language
force a reload of language by either evicting it or by closing the session (i.e. the transaction) before calling languageRepository.getOne("en")

How to write a Spring JPA query for a one to one relationship?

I am new to Spring JPA (or rather looking into it after a long time) and I need some help with writing JPA repository.
I have an entity class Itinerary and User. There is a one to one relationship between the two.
#Entity
#Table(name = "ITINERARY")
public class Itinerary implements Serializable {
#Id
#Column(name = "ID")
#GeneratedValue(generator = "Itinerary_IDGenerator", strategy = GenerationType.SEQUENCE)
#SequenceGenerator(name = "Itinerary_IDGenerator", sequenceName = "Itinerary_IDGenerator", allocationSize = 1)
private long id;
#Basic(optional = false)
#Column(nullable = false)
#Temporal(TemporalType.DATE)
private Date itineraryDate;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "USER_ID")
User user;
}
#Entity
#Table(name = "USER")
public class User implements Serializable {
#Id
#Column(name = "ID")
#GeneratedValue(generator = "User_IDGenerator", strategy = GenerationType.SEQUENCE)
#SequenceGenerator(name = "User_IDGenerator", sequenceName = "User_IDGenerator", allocationSize = 1)
private long id;
#Basic(optional = false)
#Column(unique = true, nullable = false)
private String signInName;
}
I want to write a Spring JPA repository for the Itinerary entity to have a method that will return an itinerary for a user (with the specifid singInName) and the itinerary date.
I have write the following interface, but I don't know how to specify the spring query. I have provided a JPA query that works.
public interface ItineraryRepository extends Repository<Itinerary, Long> {
//String queryStr = "select i from Itinerary i where i.user.signInName = '" + signedInUser + "' and i.itineraryDate = :today";
#Query(select i from Itinerary i where i.user.signInName = :signInName and i.itineraryDate = :today)
Itinerary findBySignInNameAndDate(#Param("signInName") String signInName, #Param("itineraryDate") Date itineraryDate);
}
I get an error on the query.
Invalid derived query! No property signInName found for type Itinerary!
and
Syntax error on tokens, delete these tokens
How do I convert this query to a spring query? I could not find an example in the documentation. I am not sure if this is possible at all and whether I am using the repository incorrectly.
Use this method in your repository Interface. And no need of #Query
findByItineraryDateAndUser_SignInName(Date itineraryDate, String signInName);
Spring data is intelligent enough to understand what query it needs to use to fetch results based on your method name(atleast in simple query cases).
User_SignInName specifies to look for a property signInName inside property user.

Exception while persisting JPA object in DB having one to many relation

hi have two tables in picture table a and table b as follows :
#Entity
#Table(name = "A")
public class A implements Serializable {
#Id
#SequenceGenerator(name = "JOURNAL_CATEGORY_ID_GENERATOR", allocationSize = 1, sequenceName = "clm_jounal_category_config_seq")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "JOURNAL_CATEGORY_ID_GENERATOR")
#Column(name = "CLAIM_ID")
private String claimId;
#Column(name = "name")
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "country")
private List<ClaimDTLS> claimDetails;
}
B Primary Key:
#Embeddable
public class BPK implements Serializable {
#Column(name = "code")
private String code;
#Column(name = "CLAIM_ID")
private String claimId;
}
B Entity:
#Entity
#Table(name = "B")
public class B implements Serializable {
#EmbeddedId
protected BPK bpk;
#Column(name = "name")
private String name;
#MapsId("country_code")
#JoinColumn(name = "claimId", referencedColumnName = "claimId", insertable = false, updatable = false)
#ManyToOne
private A a;
}
when i try to persist object of A type in Db the value of table b claim id is not set and is intialized with zero.
Also primary key of table A is generated with a oracle sequence.
any help will be welcomed.
thanks in advance
Sequence values are numbers and when JPA use them as a generator it call the setter method of the entity PK. Now, you defined your PK as a string while you use a sequence and so no matching setter can be found. Change the type of you PK to be Long and things shall work

JPA: How to fill related entity after the master is inserted

I have two tables called SL_DOCUMENT and SL_PROPOSE. The SL_DOCUMENT has its own ID (ID_DOCUMENT) and a foreign key to SL_PROPOSE (ID_PROPOSE). The SL_PROPOSE ID column is ID_PROPOSE. The particularity is that SL_PROPOSE ID value is actually the SL_DOCUMENT.ID_DOCUMENT value. i.e., after a new SL_DOCUMENT is inserted, the related SL_PROPOSE should be inserted with the SL_DOCUMENT.ID_DOCUMENT as ID and later the same value should be used in SL_DOCUMENT.ID_PROPOSE column.
I did my JPA mapping as follows:
#Entity
#Table(name = "SL_DOCUMENT")
public class DocumentORM {
#Id
#Column(name = "ID_DOCUMENT")
#SequenceGenerator(name = "SEQ_SL_DOCUMENT", sequenceName = "SEQ_SL_DOCUMENT")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_SL_DOCUMENT")
private Long id;
#OneToOne(mappedBy = "document", cascade = { CascadeType.PERSIST })
// #JoinColumn(name = "ID_PROPOSE", updatable = false)
private ProposeORM propose;
// ...
}
#Entity
#Table(name = "SL_PROPOSE")
public class ProposeORM {
#Id
#Column(name = "ID_PROPOSE")
private Long id;
#MapsId
#OneToOne
#JoinColumn(name="ID_PROPOSE")
private DocumentORM document;
// ...
public ProposeORM(DocumentORM document) {
super();
this.document = document;
this.document.setPropositura(this);
}
}
To create the new instances of of DocumentORM and ProposeORM:
DocumentORM document = new DocumentORM();
ProposeORM propose = new ProposeORM(document);
And finally to insert the new Document with ProposeORM:
this.documentoDAO.insert(document);
When I really insert a document, according the snippets above, I see in the console (Websphere 8.5) the INSERT commands for the SL_DOCUMENT, SL_PROPOSE running correctly. However, when I see the tables, the column SL_DOCUMENT.ID_PROPOSE is still NULL. Even If I uncomment the #JoinColumn annotation over DocumentORM.propose, the SL_DOCUMENT.ID_PROPOSE column continues to be not filled.
The ideal would be if SL_DOCUMENT had a discriminator column and ProposeORM was a DocumentORM subclass, using the JOINED InheritanceType (there are other tables with the same kind of relationship with SL_DOCUMENT). However, these are legacy tables and it is not possible to change it.
So, what is the alternative to fill SL_DOCUMENT.ID_PROPOSE? A workaround I was thinking is fill this column using a native SQL. Do you have better ideas?
Thanks,
Rafael Afonso
The solution I see is to make ProposeORM's ID not auto-generated, since you always want it to have the ID of the document it's linked to, AND still have a join column in the document table:
#Entity
#Table(name = "SL_DOCUMENT")
public class DocumentORM {
#Id
#Column(name = "ID_DOCUMENT")
#SequenceGenerator(name = "SEQ_SL_DOCUMENT", sequenceName = "SEQ_SL_DOCUMENT")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_SL_DOCUMENT")
private Long id;
#OneToOne
#JoinColumn(name = "ID_PROPOSE")
private ProposeORM propose;
// ...
}
#Entity
#Table(name = "SL_PROPOSE")
public class ProposeORM {
#Id
#Column(name = "ID_PROPOSE")
private Long id;
#OneToOne(mappedBy = propose)
private DocumentORM document;
// ...
public ProposeORM(DocumentORM document) {
super();
this.id = document.getId();
this.document = document;
this.document.setPropositura(this);
}
}
You'll have to persist the document first, flush the EntityManager to make sure the document has a generated ID, and then persist the propose and set it into the document.

Why am I getting "Duplicate entry" errors for related objects upon merge in eclipselink?

I have an entity class that contains a map of key-value pairs which live in a different table and there may be no such pairs for a given entity. The relevant code for the entity classes is below.
Now, when I insert such an entity with persist(), then add key-value pairs, and then save it with merge(), I get duplicate entry errors for the related table that stores the key-value pairs. I tried to hold back insertion until the keys were added, to have one call to persist() only. This led to duplicate entry errors containing an empty (zero) id in the foreign key column (ixSource).
I followed the process in the debugger, and found that eclipselink seems to be confused about the cascading. While it is updating the entity, it executes calls that update the related table. Nonetheless, it also adds those operations to a queue that is processed afterwards, which is when the duplicate entry errors occur. I have tried CascadeType.ALL and MERGE, with no difference.
I'm using static weaving, if it matters.
Here's the entities`code, shortened for brevity:
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "sType")
#Table(name = "BaseEntity")
public abstract class BaseEntity extends AbstractModel
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ix")
private long _ix;
}
#Entity
#Table(name = "Source")
public class Source extends BaseEntity
{
#OneToMany(cascade = CascadeType.MERGE)
#JoinTable(name = "SourceProperty", joinColumns = { #JoinColumn(name = "ixSource") })
#MapKey(name = "sKey")
private Map<String, SourceProperty> _mpKeys;
// ... there's more columns that probably don't matter ...
}
#Entity
#Table(name = "SourceProperty")
#IdClass(SourcePropertyKey.class)
public class SourceProperty
{
#Id
#Column(name = "sKey", nullable = false)
public String sKey;
#Id
#Column(name = "ixSource", nullable = false)
public long ixSource;
#Column(name = "sValue", nullable = true)
public String sValue;
}
public class SourcePropertyKey implements Serializable
{
private final static long serialVersionUID = 1L;
public String sKey;
public long ixSource;
#Override
public boolean equals(Object obj)
{
if (obj instanceof SourcePropertyKey) {
return this.sKey.equals(((SourcePropertyKey) obj).sKey)
&& this.ixSource == ((SourcePropertyKey) obj).ixSource;
} else {
return false;
}
}
}
I can't see how those errors would occur. Could you include the SQL and ful exception.
What version of EclipseLink are you using, did you try the latest release?
Why are you calling merge? Are you detaching the objects through serialization, if it is the same object, you do not need to call merge.
It could be an issue with the #MapKey, does it work if you remove this?