I know how to implement spring data repositories,
Create an interface like this :
public interface CountryRepository extends CrudRepository<Country, Long> {}
Now Country is an AbstractCatalog and I have (a lot) more catalogs in my project.
I'm wondering if I can make only one repository that would work for all the catalogs:
public interface AbstractCatalogRepository extends CrudRepository<AbstractCatalog, Long> {}
Now I don't see a problem while saving, but if I want to search an AbstractCatalog I'm already sure that I'll hit the wall because the repository will not know which sub-class it must choose.
AbstractCatalog.class
#MappedSuperclass
public abstract class AbstractCatalog extends PersistentEntity {
/**
* The Constant serialVersionUID.
*/
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
/**
* The code.
*/
#Column(unique = true, nullable = false, updatable = false)
private String code;
/**
* The description.
*/
#Column(nullable = false)
private String description;
/**
* The in use.
*/
#Column(name = "IN_USE", nullable = false, columnDefinition = "bit default 1")
private Boolean inUse = Boolean.TRUE;
// getters and setters
}
Country.class
#Entity
#Table(name = "tc_country")
#AttributeOverrides({
#AttributeOverride(name = "id", column =
#Column(name = "COUNTRY_SID")),
#AttributeOverride(name = "code", column =
#Column(name = "COUNTRY_CODE")),
#AttributeOverride(name = "description", column =
#Column(name = "COUNTRY_DESCRIPTION"))})
public class Country extends AbstractCatalog {
public static final int MAX_CODE_LENGTH = 11;
#Column(name = "GEONAMEID", nullable = true, unique = false)
private Long geonameid;
// getter and setter
}
Has anyone any idea, how I could use only ONE repository for all the implementations of AbstractCatalog class without having to create the same interface over and over again with minimal differences in name and other properties?
If you aren't using table inheritance on the database side (e.g. super class table with descriminator column), AFAIK, and based off reading the JPA tutorial, this can't be done (i.e. simply using #MappedSuperclass annotation for your abstract class)
Mapped superclasses cannot be queried and cannot be used in EntityManager or Query operations. You must use entity subclasses of the mapped superclass in EntityManager or Query operations. Mapped superclasses can't be targets of entity relationships
Note, the JPA repository abstraction uses an EntityManager under the hood. I did a simple test, and what you will get (in the case of Hibernate implementation) an "IllegalArgumentException : not an entity AbstractClass"
On the other hand, if you do use table inheritance, then you can use the abstract type. I know you said "with just the minimal change" (and I guess my short answer is I don't think it's possible - probably for the reasons you guessed), so I guess the rest of this answer is for other inquiring minds ;-)
An example of a table inheritance strategy would be something like this (disclaimer: this is not the correct visualization for erd inheritance, but MySQL Workbench doesn't support it, but what I have below forward engineered the model to MYSQL the way it needs to be)
Where CountryCatalog has a FK/PK reference to the AbstractCatalog table pk (id). The AbstractCatalog table has a descriminatorColumn that will be used to determine to which subtype the supertype occurrence is related.
In terms of how you would code that, it would look something like
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name="descriminatorColumn")
#Table(name="AbstractCatalog")
public abstract class AbstractCatalog {
#Id
private long id;
...
}
#Entity
#Table(name = "CountryCatalog")
public class CountryCatalog extends AbstractCatalog {
// id is inherited
...
}
public interface AbstractCatalogRepository
extends JpaRepository<AbstractCatalog, Long> {
}
#Repository
public class CountryCatalogServiceImpl implements CountryCatalogService {
#Autowired
private AbstractCatalogRepository catalogRepository;
#Override
public List<CountryCatalog> findAll() {
return (List<CountryCatalog>)(List<?>)catalogRepository.findAll();
}
#Override
public CountryCatalog findOne(long id) {
return (CountryCatalog)catalogRepository.findOne(id);
}
}
Basically, in conclusion, what you are trying to do won't work if you don't have table inheritance. The class type for the repository needs to be an entity. If your tables aren't set up this way for inheritance, it just comes down to whether or not you want to change the tables. It may be a bit much just to avoid multiple repositories though.
Some references I used are here and here
Note: Everything in this answer is tested against Hibernate provider
Oke, new project and I'm following this set up a little bit.
The problem was :
We want to add attachments, but an attachment can be uploading a file, a link or a mail.
Pojo classes :
Attachment.java :
#Entity
#Table(name = "T_ATTACHMENT")
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.STRING)
public abstract class Attachment {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ATTACHMENT_SID")
private Long id;
#ManyToOne
#JoinColumn(name = "TASK_SID", referencedColumnName = "TASK_SID", nullable = false, unique = false, insertable = true, updatable = true)
private Task task;
#ManyToOne
#JoinColumn(name = "USER_SID", referencedColumnName = "USER_SID", nullable = false, unique = false, insertable = true, updatable = true)
private User user;
public Task getTask() {
return task;
}
public void setTask(Task task) {
this.task = task;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
FileAttachment.java :
#Entity
#Table(name = "T_FILE_ATTACHMENT")
#DiscriminatorValue("FILE")
public class FileAttachment extends Attachment {
#Column(name = "NAME", nullable = false, unique = false)
private String fileName;
#Lob
#Basic
#Column(name = "FILE", nullable = false, unique = false)
private byte[] file;
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public byte[] getFile() {
return file;
}
public void setFile(byte[] file) {
this.file = file;
}
}
MailAttachment.java :
#Entity
#Table(name = "T_MAIL_ATTACHMENT")
#DiscriminatorValue("MAIL")
public class MailAttachment extends Attachment {
#Column(name = "RECIPIENT", nullable = false, unique = false)
private String to;
#Column(name = "CC", nullable = true, unique = false)
private String cc;
#Column(name = "BCC", nullable = true, unique = false)
private String bcc;
#Column(name = "TITLE", nullable = true, unique = false)
private String title;
#Column(name = "MESSAGE", nullable = true, unique = false)
private String message;
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getCc() {
return cc;
}
public void setCc(String cc) {
this.cc = cc;
}
public String getBcc() {
return bcc;
}
public void setBcc(String bcc) {
this.bcc = bcc;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
LinkAttachment.java :
#Entity
#Table(name = "T_LINK_ATTACHMENT")
#DiscriminatorValue("LINK")
public class LinkAttachment extends Attachment {
#Column(name = "DESCRIPTION", nullable = true, unique = false)
private String description;
#Column(name = "LINK", nullable = false, unique = false)
private String link;
public String getDescription() {
return description == null ? getLink() : description;
}
public void setDescription(String description) {
this.description = description;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
}
Spring data repo's :
AttachmentRepository.java:
public interface AttachmentRepository extends CustomRepository<Attachment, Long> {
List<Attachment> findByTask(Task task);
}
CustomRepository.java :
public interface CustomRepository<E, PK extends Serializable> extends
PagingAndSortingRepository<E, PK>,
JpaSpecificationExecutor<E>,
QueryDslPredicateExecutor<E> {
#Override
List<E> findAll();
}
And at last the service :
#Service
public class AttachmentServiceImpl implements AttachmentService {
#Inject
private AttachmentRepository attachmentRepository;
#Override
public List<Attachment> findByTask(Task task) {
return attachmentRepository.findByTask(task);
}
#Override
#Transactional
public Attachment save(Attachment attachment) {
return attachmentRepository.save(attachment);
}
}
This results in :
I can save to the abstract repo with any implementation I created, JPA will do it correct.
If I call findByTask(Task task) I get a List<Attachment> of all the subclasses, and they have the correct subclass in the back.
This means, you can make a renderer who do instanceof and you can customize your rendering for each subclass.
Downside is, you still need to create custom specific repository's, but only when you want to query on a specific property what is in the subclass or when you only want 1 specific implementation in stead of all implementations.
What DB are you using?
If it's JPA, take a look at
Can I use a generic Repository for all children of a MappedSuperClass with Spring Data JPA?
If it's Mongo you need to properly tune Jackson polymorphism configuration
http://wiki.fasterxml.com/JacksonPolymorphicDeserialization
So this is possible.
Related
I have an Abstract class used as MappedSuperclass like :
#MappedSuperclass
#IdClass(LogId.class)
public abstract class Log {
private LocalDateTime moment;
private String pid;
// attributes omitted
private String type;
private String message;
#Id
public LocalDateTime getMoment() { return moment; }
#Id
public String getPid() { return pid; }
// getters/setters omitted
}
My Log class provides to every entity the same structure with standard attributes type and message. Each entity represents a table in my database which share every attributes (#Id and // omitted ones). Columns type and message might have different names & types.
My first attempt was :
#Entity
#Table(name = "flow_catcher")
public class FlowCatcher extends Log {
#Override
#Column(name = "message_type")
public String getType() {
return super.getType();
}
#Override
#Column(name = "count")
#Convert(converter = StringToIntegerDbConverter.class)
public String getMessage() {
return super.getMessage();
}
}
Following this post I used #AttributeOverride like :
#Entity
#Table(name = "flow_catcher")
#AttributeOverride(name = "type", column = #Column(name = "message_type"))
#AttributeOverride(name = "message", column = #Column(name = "count"))
public class FlowCatcher extends Log {
}
But it seems impossible to use any converter to get data count as a String and not an Integer.
Here is my error log:
Unable to build Hibernate SessionFactory; nested exception is
org.hibernate.tool.schema.spi.SchemaManagementException:
Schema-validation: wrong column type encountered in column [count] in table [flow_catcher];
found [int (Types#INTEGER)], but expecting [varchar(255) (Types#VARCHAR)]
Is there any way to get my column count as a String and not an Integer ?
PS: Hibernate hbm2ddl.auto is set on validate and database scheme can't change.
EDIT 1: I found a working solution but it uses another (unused) column in my database other_string_column which doesn't sound clean to me :
#Entity
#Table(name = "flow_catcher")
#AttributeOverride(name = "type", column = #Column(name = "message_type"))
#AttributeOverride(name = "message", column = #Column(name = "other_string_column"))
public class FlowCatcher extends Log{
private Integer count;
#Override
public String getMessage() {
return this.count.toString();
}
public Integer getCount() { return count; }
public void setCount(Integer count) { this.count = count; }
}
You can go with generic
#MappedSuperclass
#IdClass(LogId.class)
public class Log<T extends Comparable> implements Serializable {
#Id
private LocalDateTime moment;
#Id
private String pid;
//additional required fields here
// Do NOT SPECIFY MAPPING
private T message;
}
#Entity
#Table(name = "flow_catcher")
#AttributeOverride(name = "type", column = #Column(name = "message_type"))
#AttributeOverride(name = "message", column = #Column(name = "count", columnDefinition = "BIGINT(15)"))
public class FlowCatcher extends Log<Integer> {
private static final long serialVersionUID = -3629698185247120860L;
}
#Entity
#Table(name = "flow_catcher_another_sample")
#AttributeOverride(name = "type", column = #Column(name = "message_type"))
#AttributeOverride(name = "message", column = #Column(name = "message", columnDefinition = "VARCHAR(20)"))
public class FlowCatcherString extends Log<String> {
private static final long serialVersionUID = -3629698185247120860L;
}
I am trying to understand hibernate a bit better after spending already quite some time on a One to many relationship I want to establish.
So currently my application works, i.e. I can add children (DocTemplateArguments) to the parent (DocumentTemplate). What bothers me is that hibernate is not updating the child records when I set them through the parent, it actually inserts and deletes them every time again I execute a merge.
As you can see, I have already updated the equals and hashcode by only keeping the relevant fields for the business logic. So, as long as the name of the child is not updated in the set that is being used for the initialisation of parent, I want hibernate to execute an update statement instead of an insert/delete combo.
Please find some code snippets below:
Parent
import javax.persistence.*;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
#Entity
#Table(name = "document_template")
#Data
#EqualsAndHashCode(exclude="docTemplateArguments")
public class DocumentTemplate {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(name = "name", nullable = false, length = 30)
private String name;
#Column(name = "description", nullable = true, length = 100)
private String description;
#Enumerated(EnumType.STRING)
#Column(name = "type", nullable = false, length = 15)
private DocumentType type;
#Enumerated(EnumType.STRING)
#Column(name = "format", nullable = false, length = 15)
private DocumentFormat format;
#Column(name = "content", nullable = false, length = 50000)
private String content;
#Setter(AccessLevel.NONE)
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "documentTemplate", orphanRemoval = true)
private Set<DocTemplateArgument> docTemplateArguments = new HashSet<DocTemplateArgument>();
public void setDocTemplateArguments (Set<DocTemplateArgument> dtas){
this.docTemplateArguments.addAll(dtas);
}
}
Child
import lombok.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
#Entity
#Table(name = "doc_template_argument")
#Data
#EqualsAndHashCode(exclude={"DocumentTemplate","id","dialogText"})
public class DocTemplateArgument {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column (name="name", nullable = false, length=30)
private String name;
#Column (name="dialog_text", length = 100)
private String dialogText;
#Transient
private String argument;
#ManyToOne(fetch = FetchType.EAGER)
#OnDelete(action = OnDeleteAction.CASCADE)
#JoinColumn(name="doctemplate_id", nullable=false)
private DocumentTemplate documentTemplate;
}
From my current understanding I think I need to intervene in the merge method of the DAO class of the parent in order to get this done. This currently looks as follows:
#Repository
public class DocumentTemplateDAOImpl implements DocumentTemplateDAO {
private final SessionFactory sessionFactory;
public DocumentTemplateDAOImpl(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
#Override
#Transactional
public List<DocumentTemplate> getAll() {
List<DocumentTemplate> documentTemplates = getCurrentSession().createQuery("from DocumentTemplate").list();
return documentTemplates;
}
#Override
#Transactional
public DocumentTemplate get(int id) {
return getCurrentSession().get(DocumentTemplate.class, id);
}
#Override
#Transactional
public void create(DocumentTemplate documentTemplate) {
getCurrentSession().persist(documentTemplate);
}
#Override
#Transactional
public void update(DocumentTemplate documentTemplate) {
getCurrentSession().merge(documentTemplate);
}
#Override
#Transactional
public void delete(int id) {
DocumentTemplate documentTemplate = get(id);
getCurrentSession().delete(documentTemplate);
}
private Session getCurrentSession() {
return sessionFactory.getCurrentSession();
}
}
I have no idea on how to do this though, any help would be greatly appreciated.
For completeness, the whole structure is being called in a convertToEntity method of a DTO:
public static DocumentTemplate convertToEntity(DocumentTemplateDTO documentTemplateDTO) {
DocumentTemplate documentTemplate = new DocumentTemplate();
documentTemplate.setId(documentTemplateDTO.getId());
documentTemplate.setName(documentTemplateDTO.getName());
documentTemplate.setDescription(documentTemplateDTO.getDescription());
documentTemplate.setType(documentTemplateDTO.getType());
documentTemplate.setFormat(documentTemplateDTO.getFormat());
documentTemplate.setContent(documentTemplateDTO.getContent());
documentTemplateDTO.getDocTemplateArguments().stream().forEach(dta -> dta.setDocumentTemplate(documentTemplate));
documentTemplate.setDocTemplateArguments(documentTemplateDTO.getDocTemplateArguments());
return documentTemplate;
}
EDIT
I found out why this happened. In my request I was omitting the id of the argument. I presume that when the json is deserialised it defaults a non specified property of type integer into 0, which then as such ended up in the entity.
When I put this to the latest id for the record to be updated, it properly issues an update statement. Still, I find this a bit counterintuitive when the id is explicitly excluded from the equals and hash check with:
#EqualsAndHashCode(exclude={"DocumentTemplate","id","dialogText"})
If would be great if someone can explain this!
I have something similar to this:
#Entity
#Table(name = "claim", schema = "test")
public class Claim implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idClaim", unique = true, nullable = false)
private Integer idClaim;
#OneToOne(mappedBy = "claim", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JsonManagedReference
private ClaimReturnInfo claimReturnInfo;
#Column(name = "notes")
private String notes;
// Getters and setters
}
#Entity
#Table(name = "claim_returninfo", schema = "test")
public class ClaimReturnInfo implements Serializable {
#Id
#Column(name = "Claim_idClaim")
private Integer id;
#MapsId("Claim_idClaim")
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Claim_idClaim")
#JsonBackReference
private Claim claim;
#Column(name = "description")
private String description;
// Getters and setters
}
ClaimReturnInfo Id is not autogenerated because we want to propagate the Id from its parent (Claim). We are not able to do this automatically and we are getting this error: ids for this class must be manually assigned before calling save() when 'cascade' is executed in ClaimReturnInfo .
Is it possible to map Claim Id into ClaimReturnInfo Id or should we do this manually?
Even if we set this ID manually on claimReturnInfo and we can perform updates, we still get this error when trying to create a new Claim:
// POST -> claimRepository.save() -> Error
{
"notes": "Some test notes on a new claim",
"claimReturnInfo": {
"description": "Test description for a new claimReturnInfo"
}
}
In the ServiceImplemetation:
#Override
#Transactional
public Claim save(Claim claim) throws Exception {
if(null != claim.getClaimReturnInfo()) {
claim.getClaimReturnInfo().setId(claim.getIdClaim());
}
Claim claimSaved = claimRepository.save(claim);
return claimSaved;
}
I have tried using the following mappings and from your comments it was apparent that Json object is populated correctly.
I have noticed that the annotation #MapsId is the culprit.If you check the documentation of #MapsId annotation it says
Blockquote
The name of the attribute within the composite key
* to which the relationship attribute corresponds. If not
* supplied, the relationship maps the entity's primary
* key
Blockquote
If you change #MapsId("Claim_idClaim") to #MapsId it will start persisting your entities.
import javax.persistence.*;
#Entity
#Table(name = "CLAIM")
public class Claim {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idClaim", unique = true, nullable = false)
private Long idClaim;
#Column(name = "notes")
private String notes;
#OneToOne(mappedBy = "claim", cascade = CascadeType.ALL, optional = false)
private ClaimReturnInfo claimReturnInfo;
public Long getIdClaim() {
return idClaim;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public ClaimReturnInfo getClaimReturnInfo() {
return claimReturnInfo;
}
public void setClaimReturnInfo(ClaimReturnInfo claimReturnInfo) {
if (claimReturnInfo == null) {
if (this.claimReturnInfo != null) {
this.claimReturnInfo.setClaim(null);
}
} else {
claimReturnInfo.setClaim(this);
}
this.claimReturnInfo = claimReturnInfo;
}
}
package com.hiber.hiberelations;
import javax.persistence.*;
#Entity
#Table(name = "CLAIM_RETURN_INFO")
public class ClaimReturnInfo {
#Id
#Column(name = "Claim_idClaim")
private Long childId;
#Column(name = "DESCRIPTION")
private String description;
#MapsId
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "Claim_idClaim")
private Claim claim;
public Long getChildId() {
return childId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Claim getClaim() {
return this.claim;
}
public void setClaim(Claim claim) {
this.claim = claim;
}
}
I have two pojo classes wihch are named Document and DocumentUser. DocumentUser has an property documentId which linked to Document's id by foreign key.
So i want to create criteria query which retrieve Documents with its DocumentUser which is linked itself by forein key("document_id")
pojo classes:
Document
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
#Entity
#Table(name = "DYS_BYS_DOSYA")
#Audited
public class Document implements Serializable {
private Long id;
private String name;
private List<DocumentUser> documentUserList = new ArrayList<DocumentUser>();
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID", nullable = false, precision = 15, scale = 0)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "AD", nullable = false, length = 500)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "document", fetch = FetchType.EAGER)
#Fetch(FetchMode.SUBSELECT)
#Cascade(CascadeType.ALL)
public List<DocumentUser> getDocumentUserList() {
return documentUserList;
}
public void setDocumentUserList(List<DocumentUser> documentUserList) {
this.documentUserList = documentUserList;
}
#Override
public String toString() {
return "tr.com.enlil.dys.server.servis.model.Document[id=" + id + "]";
}
}
DocumentUser:
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
#Entity
#Table(name = "DYS_DOSYA_SAHIBI_USER")
#Audited
public class DocumentUser implements Serializable {
/**
*
*/
private static final long serialVersionUID = 6393919788296838129L;
private Long id;
private Long personelId;
private Document document;
private String personelName;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID", unique = true, nullable = false, precision = 15, scale = 0)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "OLUSTURUCU_PERSONEL_ID")
public Long getPersonelId() {
return personelId;
}
public void setPersonelId(Long personelId) {
this.personelId = personelId;
}
#Column(name = "KULLANICI_AD")
public String getPersonelName() {
return personelName;
}
public void setPersonelName(String personelName) {
this.personelName = personelName;
}
#ManyToOne
#JoinColumn(name = "DOSYA_ID")
public Document getDocument() {
return document;
}
public void setDocument(Document document) {
this.document = document;
}
}
In this way, how can i get Document data depends on personelId of DocumentUser table by using criteria query? I am not familiar with hibernate and i need your helps. I try to write some codes but didn't work.
public List<Document> fetchRecordsByCriteriaLimitedList(String userId) throws Exception{
Criteria criteria = getSessionFactory().getCurrentSession().createCriteria(Dosya.class);
DetachedCriteria dosyaSahibiCriteria = (DetachedCriteria) criteria.createCriteria("documentUserList");
dosyaSahibiCriteria.add(Restrictions.eq("personelId", userId));
dosyaSahibiCriteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return criteria.list();
}
Several problems with your code. First of all, you said
2)DocumentUser is subclass of Document
This isn't true, judging from your code (it would mean that DocumentUser extends Document), but you probably meant they are in a parent -> child relation. Second, in documentUserList mapping, there is this #OneToMany(mappedBy = "dosya", fetch = FetchType.EAGER), which means there is a field named dosya in DocumentUser, and there isn't. Instead, replace it with mappedBy = "document". Assuming everything else is ok, query to get all documents based on their DocumentUser's id would be
public List<Document> fetchRecordsByCriteriaLimitedList(String userId) throws Exception{
Criteria criteria = getSessionFactory().getCurrentSession().createCriteria(Document.class);
criteria.createAlias("documentUserList", "users").add(Restrictions.eq("users.personelId", userId));
return criteria.list();
}
I'm working on a project where I need to save a download file information with keywords.
Typical one-to-many scenario, right? A download record to many download-keyword relationship records.
So here is what the Download.java look like
#Entity
#Table(name = "downloads", catalog = "nbpx")
public class Download extends BaseEntity implements Serializable {
public Integer downloadId;
public String title;
public Set<DownloadKeyword> downloadKeywords = new HashSet<DownloadKeyword>(
0);
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "downloadId", unique = true, nullable = false)
public Integer getDownloadId() {
return downloadId;
}
public void setDownloadId(Integer downloadId) {
this.downloadId = downloadId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
#OneToMany(fetch = FetchType.EAGER, mappedBy = "download")
#Cascade({CascadeType.SAVE_UPDATE, CascadeType.DELETE})
public Set<DownloadKeyword> getDownloadKeywords() {
return downloadKeywords;
}
public void setDownloadKeywords(Set<DownloadKeyword> downloadKeywords) {
this.downloadKeywords = downloadKeywords;
}
}
Since I don't want any duplicate download-keyword relation records in the table, I set a JPA unique Constraints for the class. But I didn't use a composite key. And this is what DownloadKeyword.java look like:
#Entity
#Table(name = "downloadkeywords", catalog = "nbpx", uniqueConstraints =
#UniqueConstraint(columnNames = {"keywordId", "downloadId" }))
public class DownloadKeyword extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
public Integer downloadKeywordId;
public Download download;
public Integer keywordId;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "downloadKeywordId", unique = true, nullable = false)
public Integer getDownloadKeywordId() {
return downloadKeywordId;
}
public void setDownloadKeywordId(Integer downloadKeywordId) {
this.downloadKeywordId = downloadKeywordId;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "downloadId", nullable = false, insertable=true)
public Download getDownload() {
return download;
}
public void setDownload(Download download) {
this.download = download;
}
public Integer getKeywordId() {
return keywordId;
}
public void setKeywordId(Integer keywordId) {
this.keywordId = keywordId;
}
}
When I save a download entity, I expect to see all the download-keyword relationship entities are saved within the download entity save session. And I need to avoid duplication in download-keyword relationship table. But every time I save it, the relation records were inserted straightly into DB without checking the duplication with JPA annotation.
So here is my question: Did multiple-column uniqueConstraints of JPA annotation get ignored when entities were saving in one-to-many save or update session?
The uniqueConstraints are only for table generation support.
See the documentation.
Since you're already using Set<DownloadKeyword> in the Download class - you can implement hashCode and equals methods in the DownloadKeyword class (include primary key downloadKeywordId and the keywordId properties) to accomplish what you want.
However, I would still prefer a composite key approach.