Cascade delete problem on **custom** unidirectional #OneToMany relationship (JPA eclipselink) - jpa

I'm having problem deleting an entity having an Unidirectional #OneToMany custom relationship.
Here the relationship on the "base" entity (relevant columns only):
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "Id", updatable = false)
protected Integer id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "ObjectId", referencedColumnName = "Id")
protected Collection<Attachment> attachmentsCollection;
Here the relevant columns on the "child" entity:
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "Id", updatable = false)
protected Integer id;
#NotNull
#Basic(optional = false)
#Size(min = 1, max = 64)
#Column(name = "ObjectTable", updatable = false)
protected String objectTable;
#Basic(optional = false)
#Column(name = "ObjectId", updatable = false)
protected Integer objectId;
#NotNull
#Basic(optional = false)
#Lob
#Column(name = "Data")
protected byte[] data;
#NotNull
#Basic(optional = false)
#Column(name = "SizeInBytes")
protected Long sizeInBytes;
#NotNull
#Basic(optional = false)
#Size(min = 1, max = 128)
#Column(name = "Name")
protected String name;
Here explanation on why this is a custom relationship:
The base object is the super class of all entities. It has, additional to its own id, the opportunity to associate any number of attachments via the attachments collection. Since Ids are not unique between tables (entities), it's needed to add an additional column in the child table (the attachment table). This additional column (ObjectTable) identifies the entity-kind owning the attachment. Adding this column to the entity'id (ObjectId) column, the relation is complete:
Suppose record 99 of entity Invoice has 2 attachments (attachment 'Z' and attachment 'Y'):
Table Invoice
-------------
Id ColumnA ColumnB ColumnC...
99 'xyz' '2343' 'zyx'
.
.
Table Attachment
----------------
Id ObjectTable ObjectId Data SizeInBytes Name
43542 'Invoice' 99 11100110 437834 'Z.pdf'
43543 'Invoice' 99 101110 867454 'Y.pdf'
I managed to load the relation with a mapping customizer:
public static final String TABLENAME = "TECAttachment";
public static final String OBJECTIDFIELDNAME = TABLENAME + ".ObjectId";
public static final String OBJECTTABLEFIELDNAME = TABLENAME + ".ObjectTable";
// Customize how records are selecting inside entity's attachments collection: include the entities table name
#Override
public void customize(ClassDescriptor descriptor) {
OneToManyMapping mapping = (OneToManyMapping)descriptor.getMappingForAttributeName(AttachmentEntitySessionCustomizer.ATTACHMENTSCOLLECTIONNAME);
ExpressionBuilder eb = new ExpressionBuilder();
Expression eObjectIdNotNull = eb.getField(AttachmentEntitySessionCustomizer.OBJECTIDFIELDNAME).notNull();
Expression eObjectId = eb.getField(AttachmentEntitySessionCustomizer.OBJECTIDFIELDNAME).equal(eb.getParameter(descriptor.getPrimaryKeyFields().get(0)));
Expression eObjectTable = eb.getField(AttachmentEntitySessionCustomizer.OBJECTTABLEFIELDNAME).equalsIgnoreCase(descriptor.getTableName());
mapping.setSelectionCriteria(eObjectIdNotNull.and(eObjectId.and(eObjectTable)));
}
... but I'm having problems during the delete operation of any entity. For a reason that I still don't understand, JPA is executing an update statement over the Attachment table without taking in consideration the ObjectTable column. Here is what's going on when I delete a record from the table PRHTABidParticipationItem:
Finest: Execute query DeleteObjectQuery(com.tec.uportal.prhta.model.bid.participation.PRHTABidParticipationItem[ id=24 ])
Finest: Execute query DataModifyQuery()
Fine: UPDATE TECAttachment SET ObjectId = ? WHERE (ObjectId = ?)
bind => [null, 24]
Fine: DELETE FROM TECPRHTABidParticipationItem WHERE (Id = ?)
bind => [24]
Finer: end unit of work flush
Finer: resume unit of work
Finer: begin unit of work commit
Finer: commit transaction
My problem is that the UPDATE statement over the table TECAttachment updates all records with the given Id and not only those related to the Entity TECPRHTABidParticipationItem.
I think I must override the sql statements DeleteObjectQuery or DataModifyQuery but don't know how.
Any help will be really appreciated.
I'm working with eclipselink-2.7.4
Thanks in advanace!

After a lot or searching, digging and reading I managed to solve my problem.
Basically:
My attachments collection is an unidirectional custom #OneToMany relationship.
I was in the correct path trying to achieve my goal with a collection customizer (a class associated to my collection via annotation and implementing the DescriptorCustomizer interface).
My customizer not only needs to customize the way the "child" records are selected, also it's needed to customize what to do when the parent record is deleted.
The way to do this is to override the default removeAllTargetsQuery property providing a new custom query through the method mapping.setCustomRemoveAllTargetsQuery(DataModifyQuery).
The most difficult part was to understand how the underlying eclipselink implementation sends parameters (which, order, type, etc.) to the custom DataModifyQuery. I had to download the source code of EclipseLink's JPA implementation and figure out how things are done...
Finally, everything is working good and ok thanks to the following simple DescriptorCustomizer:
package com.tec.uportal.model.customizer;
import org.eclipse.persistence.config.DescriptorCustomizer;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.mappings.OneToManyMapping;
import org.eclipse.persistence.queries.DataModifyQuery;
/**
*
* #author MarcB
*/
public class AttachmenstCollectionAttributeCustomizer implements DescriptorCustomizer {
public static final String ATTACHMENTS__COLLECTION_NAME = "attachmentsCollection";
public static final String ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME = "ObjectTable";
// Customize attachments collection mapping
#Override
public void customize(ClassDescriptor descriptor) {
// Customize how records are selected inside parent entity's attachments collection: include in the WHERE clause the column ObjectTable
OneToManyMapping mapping = (OneToManyMapping)descriptor.getMappingForAttributeName(ATTACHMENTS__COLLECTION_NAME);
ExpressionBuilder eb = new ExpressionBuilder();
Expression eObjectId = eb.getField(mapping.getTargetForeignKeyFields().get(0).getQualifiedName()).equal(eb.getParameter(descriptor.getPrimaryKeyFields().get(0)));
Expression eObjectTable = eb.getField(mapping.getTargetForeignKeyFields().get(0).getTable().getQualifiedName() + "." + ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME).equalsIgnoreCase(descriptor.getTable(descriptor.getTableName()).getQualifiedName());
mapping.setSelectionCriteria(eObjectId.and(eObjectTable));
// Customize what must be done (delete childs) when parent entity is deleted
DataModifyQuery dmf = new DataModifyQuery("DELETE " + mapping.getTargetForeignKeyFields().get(0).getTable().getQualifiedName() + " WHERE " + mapping.getTargetForeignKeyFields().get(0).getName() + " = #" + mapping.getTargetForeignKeyFields().get(0).getName() + " AND " + ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME + " = '" + descriptor.getTableName() + "'");
mapping.setCustomRemoveAllTargetsQuery(dmf);
}
}

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.

Hibernate Filter being ignored

My application uses Hibernate 5.02 and Wildfly 10 with a PostgreSQL 9.5 database. I'm trying to enable a filter on a #OneToMany collection held within an entity that is constructed via a NamedQuery. Unfortunately, it seems as if the filter is just ignored. Here are the different components, redacted for ease of reading.
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "analystprocess_id IN (:processIds) AND id NOT IN (SELECT msg_id FROM analysis.analyzedmsg WHERE analyst_id IN (:analystIds) AND analystprocess_id IN (:processIds)) ORDER BY process_msg_id")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
private void buildAnalystProcess() {
LOG.info("Building AnalystProcessEntity");
analystUser.getJdbcSession().enableFilter(AnalystProcessEntity.MSG_FILTER)
.setParameter(AnalystProcessEntity.MSG_FILTER_PROC_ID_PARAM, analystProcessId)
.setParameter(AnalystProcessEntity.MSG_FILTER_ANALYST_ID_PARAM, analystUser.getId());
Query query = analystUser.getJdbcSession().getNamedQuery(AnalystProcessEntity.GET_PROCESS)
.setParameter("processId", analystProcessId);
// Query query = analystUser.getJdbcSession().createNativeQuery("SELECT * FROM analysis.analystprocess WHERE id = :processId")
// .setParameter("processId", analystProcessId)
// .addEntity(AnalystProcessEntity.class);
analystProcess = (AnalystProcessEntity) query.getSingleResult();
CREATE TABLE analysis.analystprocess (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_date TIMESTAMP NOT NULL DEFAULT now(),
...
);
CREATE TABLE analysis.msg (
id SERIAL PRIMARY KEY,
analystprocess_id INTEGER NOT NULL REFERENCES analysis.analystprocess(id) ON DELETE CASCADE ON UPDATE CASCADE,
process_msg_id INTEGER NOT NULL,
constraint tbl_statusid_analystprocessid unique(status_id, analystprocess_id)
);
As seen above, I have also tried the filter on constructing the AnalystProcessEntity class via createNativeQuery instead of getNamedQuery and no luck.
I also added a defaultCondition with hardcoded values into the #FilterDef just to see if it would execute the default condition and it still didn't.
I've tried the #Filter above the entity definition as well as above the class definition. I even came across a blog post which made it sound like the condition references entity fields (variable names) and not table fields (column names). Trying to stick to Java naming conventions in the Entity and Postgres naming conventions in the table, so I tried switching the references in the condition and to no avail.
I have sql logging turned on in Hibernate and the condition doesn't show up anywhere, as if it's just simply being ignored.
Any help would be greatly appreciated!
So, the problem was that I had the #FilterDef applied to the wrong class. It was my presumption that because I was constructing the AnalystProcessEntity which holds the MsgEntity collection (which I am trying to filter), that the #FilterDef would be applied to the AnalystProcessEntity class. Instead, it needs to be applied to the entity that it's actually filtering (hindsight being 20/20, that's pretty obvious).
Also, the actual condition needed to be modified to use complete references within the sub-select query.
I hope this helps someone at some point...
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "id NOT IN (SELECT amsg.msg_id FROM analysis.analyzedmsg amsg WHERE amsg.analyst_id IN (:analystIds) AND amsg.analystprocess_id IN (:processIds))")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
Additionally, I ran into another problem with null's appearing in the collection, despite the fact that I am using an #OrderColumn, which I thought fixed that issue. It seems that with the use of the #Filter, null's are inserted in place of what ended up being filtered OUT (excluded).

Default Sort on a Spring Data JPA Repository Method with Custom Query and Pageable Parameter

I have the following repository method that works exactly the way I need it to iff the user provides a sort column in the page parameter:
public interface IdentityRepository extends JpaRepository<Identity, String> {
#Query("select distinct ident from Identity ident left outer join ident.authorities authority "
+ "where ("
+ "(:src is null or ident.source = :src) and "
+ "(:org is null or ident.organization = :org) and "
+ "(:auth is null or authority.authority = :auth) and "
+ "(:authSrc is null or authority.authoritySource = :authSrc))")
#RestResource(path="filter")
public Page<Identity> findWithFilter(
#Param("src") String source,
#Param("org") String org,
#Param("auth") Authority auth,
#Param("authSrc") AuthoritySource authSrc,
Pageable page);
...
}
If the caller provides a page count, but not a sort column, they will get back the correct number of results when retrieving all the pages. However, many of the entities will be duplicated, so even though the result count is correct, many expected entities are missing and others are duplicated (or triplicated).
What I'm wondering is if there is a way to provide a default sort column and direction if the user does not specify one. I've learned that #EnableSpringDataWebSupport can help here, but we're not using Spring MVC, so I don't have any controllers to attach the #SortDefaults to. We are using Spring Data Rest though. Also, I've tried changing the method name to findWithFilterOrderByIdAsc, but that did not seem to help. Ran across this issue in the Spring JIRA, which I believe is exactly what I need, but until it's resolved, does anyone know of a work around?
Here's my entity...
#Entity
#Table(name = "identity", indexes = { #Index(columnList = "user_id", unique = true) })
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#Audited
public class Identity implements Serializable, Identifiable<String> {
/**
* The unique identifier for this identity within the IDD application.
*/
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "IDDUidGenerator")
#GenericGenerator(name = "IDDUidGenerator")
private String id;
/**
* The name of the identity provider wherein this identity is originally defined.
*/
#Column(name = "source")
private String source = INTERNAL_SOURCE;
/**
* The unique identifier for this identity within the customer's identity provider.
*/
#NotNull
#Column(name = "user_id", nullable = false, unique = true)
private String userId;
/**
* The roles this identity is authorized to perform.
*/
#OneToMany(fetch = FetchType.EAGER, mappedBy = "identity", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<IdentityAuthority> authorities = new HashSet<>();
...
}
And its sub-entity...
#Entity
#Table(name = "identity_authority")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#Audited
public class IdentityAuthority implements Serializable, Identifiable<Long> {
private static final long serialVersionUID = -5315412946768343445L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#XmlTransient
#JsonIgnore
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "identity_id", nullable = false)
#XmlTransient
#JsonIgnore
private Identity identity;
#Enumerated(EnumType.STRING)
#Column(name = "authority", length = 20, nullable = false)
private Authority authority;
#Enumerated(EnumType.STRING)
#Column(name = "authority_source", length = 30, nullable = false)
private AuthoritySource authoritySource;
...
}
Here's the test case I ran to demonstrate the problem...
#Test
public void testPagedRequestsReturnAllResults() {
// Create identities
String source = "One Hundred Identities Generator";
int numIdentities = 100;
int pageSize = 5;
List<Identity> input = new ArrayList<>();
for (int i=0; i<numIdentities; i++) {
Identity identity = new Identity();
identity.setUserId(UUID.randomUUID().toString());
identity.setSource(source);
input.add(identity);
}
// Save identities
List<Identity> output = repository.saveBulk(input);
Set<String> savedIds = collectIds(output, null);
assertThat(savedIds.size()).isEqualTo(numIdentities);
// Test Sorted Find Filter with Paging (THIS PASSES)
Pageable pageRequest = new PageRequest(0, pageSize, new Sort(Direction.ASC, "id"));
Set<String> foundPagedIds = new HashSet<>();
do {
Page<Identity> page = repository.findOrderByIdAsc(source, null, null, null, pageRequest);
List<Identity> foundIdentities = page.getContent();
foundPagedIds = collectIds(foundIdentities, foundPagedIds);
pageRequest = page.nextPageable();
} while (pageRequest != null);
assertThat(foundPagedIds.size()).isEqualTo(numIdentities);
assertThat(foundPagedIds).isEqualTo(savedIds);
// Test Unsorted Find Filter with Paging (THIS FAILS)
pageRequest = new PageRequest(0, pageSize);
foundPagedIds = new HashSet<>();
do {
Page<Identity> page = repository.findOrderByIdAsc(source, null, null, null, pageRequest);
List<Identity> foundIdentities = page.getContent();
foundPagedIds = collectIds(foundIdentities, foundPagedIds);
pageRequest = page.nextPageable();
} while (pageRequest != null);
assertThat(foundPagedIds.size()).isEqualTo(numIdentities);
assertThat(foundPagedIds).isEqualTo(savedIds);
}

Foreign Key constraint violation when persisting a many-to-one class

I'm getting an error when trying to persist a many to one entity:
Internal Exception: org.postgresql.util.PSQLException: ERROR: insert or update on table "concept" violates foreign key constraint "concept_concept_class_fk"
Detail: Key (concept_class_id)=(Concept) is not present in table "concept_class".
Error Code: 0
Call: INSERT INTO concept (concept_key, description, label, code, concept_class_id) VALUES (?, ?, ?, ?, ?)
bind => [27, description_1, label_1, code_1, Concept]
Query: InsertObjectQuery(com.mirth.results.entities.Concept[conceptKey=27])
at com.sun.ejb.containers.BaseContainer.checkExceptionClientTx(BaseContainer.java:3728)
at com.sun.ejb.containers.BaseContainer.postInvokeTx(BaseContainer.java:3576)
at com.sun.ejb.containers.BaseContainer.postInvoke(BaseContainer.java:1354)
... 101 more
Here is the method that tries to persist it. I've put a comment where the line is:
#Override
public void loadConcept(String metaDataFilePath, String dataFilePath) throws Exception {
try {
ConceptClassMetaData conceptClassMetaData = (ConceptClassMetaData) ModelSerializer.getInstance().fromXML(FileUtils.readFileToString(new File(metaDataFilePath), "UTF8"));
em.executeNativeQuery(conceptClassMetaData.getCreateStatement());
ConceptClassRow conceptClassRow = conceptClassMetaData.getConceptClassRow();
ConceptClass conceptClass = em.findByPrimaryKey(ConceptClass.class, conceptClassRow.getId());
if (conceptClass == null) {
conceptClass = new ConceptClass(conceptClassRow.getId());
}
conceptClass.setLabel(conceptClassRow.getLabel());
conceptClass.setOid(conceptClassRow.getOid());
conceptClass.setDescription(conceptClassRow.getDescription());
conceptClass = em.merge(conceptClass);
DataParser dataParser = new DataParser(conceptClassMetaData, dataFilePath);
for (ConceptModel conceptModel : dataParser.getConceptRows()) {
ConceptFilter<Concept> filter = new ConceptFilter<Concept>(Concept.class);
filter.setCode(conceptModel.getCode());
filter.setConceptClass(conceptClass.getLabel());
List<Concept> concepts = em.findAllByFilter(filter);
Concept concept = new Concept();
if (concepts != null && !concepts.isEmpty()) {
concept = concepts.get(0);
}
concept.setCode(conceptModel.getCode());
concept.setDescription(conceptModel.getDescription());
concept.setLabel(conceptModel.getLabel());
concept.setConceptClass(conceptClass);
concept = em.merge(concept); //THIS LINE CAUSES THE ERROR!
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
...
Here are how the two entities are defined:
#Entity
#Table(name = "concept")
#Inheritance(strategy=InheritanceType.JOINED)
#DiscriminatorColumn(name="concept_class_id", discriminatorType=DiscriminatorType.STRING)
public class Concept extends KanaEntity {
#Id
#Basic(optional = false)
#Column(name = "concept_key")
protected Integer conceptKey;
#Basic(optional = false)
#Column(name = "code")
private String code;
#Basic(optional = false)
#Column(name = "label")
private String label;
#Column(name = "description")
private String description;
#JoinColumn(name = "concept_class_id", referencedColumnName = "id")
#ManyToOne
private ConceptClass conceptClass;
...
#Entity
#Table(name = "concept_class")
public class ConceptClass extends KanaEntity {
#Id
#Basic(optional = false)
#Column(name = "id")
private String id;
#Basic(optional = false)
#Column(name = "label")
private String label;
#Column(name = "oid")
private String oid;
#Column(name = "description")
private String description;
....
And also, what's important is the sql that's being generated:
INSERT INTO concept_class (id, oid, description, label) VALUES (?, ?, ?, ?) bind => [LOINC_TEST, 2.16.212.31.231.54, This is a meta data file for LOINC_TEST, loinc_test]
INSERT INTO concept (concept_key, description, label, code, concept_class_id) VALUES (?, ?, ?, ?, ?) bind => [27, description_1, label_1, code_1, Concept]
The reason this is failing is obvious: It's inserting the word Concept for the concept_class_id. It should be inserting the word LOINC_TEST. I can't figure out why it's using this word. I've used the debugger to look at the Concept and the ConceptClass instance and neither of them contain this word. I'm using eclipselink. Does anyone know why this is happening?
You have two conflicting definitions for the column concept_class_id.
The column concept_class_id appears in the #DiscriminatorColumn and #JoinColumn annotations around class Concept. You can't do that. Those two annotations are fighting for control of the column concept_class_id in table concept. #DiscriminatorColumn happens to be winning. That's why the class name, "Concept", appears in the SQL binding where you expect a ConceptClass id.
By the way, #DiscriminatorColumn is only useful when multiple classes share a single table. The #D/C records which class a row represents. If only objects of class Concept are stored in the concept table, then you can remove #DiscriminatorColumn. #D/C is pointless unless you have multiple classes in a table.
To summarize, fixes include:
Alter "name" in Concept's #DiscriminatorColumn annotation
Alter "name" in Concept.conceptClass's #JoinColumn annotation
Remove #DiscriminatorColumn annotation on class Concept
HTH