Duplicated entities in Bidirection OneToMany relationship when fetching with Spring Data JPA - spring

I have two entities defined. Both of them are connected through a bidirectional #OneToMany.
Here are my two entities
#Entity(name = "Post")
#Table(name = "post")
public class Post {
#Id
#GeneratedValue
private Long id;
private String title;
#OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
//Constructors, getters and setters removed for brevity
public void addComment(PostComment comment) {
comments.add(comment);
comment.setPost(this);
}
public void removeComment(PostComment comment) {
comments.remove(comment);
comment.setPost(null);
}
}
#Entity(name = "PostComment")
#Table(name = "post_comment")
public class PostComment {
#Id
#GeneratedValue
private Long id;
private String review;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "post_id")
private Post post;
//Constructors, getters and setters removed for brevity
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PostComment )) return false;
return id != null && id.equals(((PostComment) o).getId());
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
I am using Spring Data JPA to fetch / save entities.
Saving works fine and for example if I save 1 post and 4 post comments I can see the entries in the database. The database I am using is PostgreSQL.
When I am fetching all the posts through my repository using the findAll method, then I receive the post with the 4 comments.
The issue is when I am fetching only one post through the getOne method, the post is found, but for some reason the entity contains 7 post comments. The first entry is duplicated 3 times and the second one is duplicated two times.
I don't understand why this is happening and how can I fix this.
Any help is appreciated.
Thanks

You need to change List to Set.
#OneToMany(mappedBy = "post",cascade = CascadeType.ALL,orphanRemoval = true)
private Set<PostComment> comments = new HashSet<>();

Related

A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance - Spring and Lombok

I am getting this A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance error with my oneToMany relationship when trying to update my child element (report). Although I see this question asked a few times here, I haven't been able to make my code to work with them and I now feel it may be an issue with me using Lombok perhaps, since most of the answers here mention about changes on the hashcode and equals methods, which are abstracted away by Lombok? I tried to remove Lombok to try without it but then I got a bit confused on what to do next. If I could get some guidance on how to fix this issue within my original Lombok implementation please.
#Entity
#Table(name = "category")
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "category_title", nullable = false)
private String title;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> report;
public Category(UUID id, String title) {
this.id = id;
this.title = title;
}
}
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "report")
#Data
public class Report {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "report_title", nullable = false)
private String reportTitle;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "category_id", nullable = false)
private Category category;
public Report(UUID id) {
this.id = id;
}
}
#Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties
Report updatedReport = reportRepository.save(existingReport);
updatedReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties
ReportUpdateDto newReportUpdateDto = new ReportUpdateDto(updatedReport.getId(),
updatedReport.getReportTitle(), updatedReport.getCategory());
return newReportUpdateDto;
} else {
return null;
}
}
Thank you very much.
Fast solution (but not recommended)
The error of collection [...] no longer referenced arrises in your code beacuse the synchronization between both sides of the bidiretional mapping category-report was just partially done.
It's important to note that binding the category to the report and vice-versa is not done by Hibernate. We must do this ouserselves, in the code, in order to sync both sides of the relationship, otherwise we may break the Domain Model relationship consistency.
In your code you have done half of the synchronization (binding the category to the report):
existingReport.setCategory(category);
What is missing is the binding of the report to the category:
category.addReport(existingReport);
where the Category.addReport() may be like that:
public void addReport(Report r){
if (this.report == null){
this.report = new ArrayList<>();
}
this.report.add(r);
}
Recommended Solution - Best practice for synchronizing both sides of the mapping
The suggested code above works, but it is error prone as the programmer may forget to call one of the lines when updating the relationship.
A better approach is to encapsulate that sychronization logic in a method in the owning side of the relationship. And that side is the Category as stated here: mappedBy = "category".
So what we do is to encapsulate in the Category.addReport(...) all the logic of cross-reference between Category and Report.
Considering the above version of addReport() method, what is missing is adding r.setCategory(this).
public class Category {
public void addReport(Report r){
if (this.reports == null){
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
}
Now, in the updateReport() it is enough to call the addReport() and the commented line bellow can be deleted:
//existingReport.setCategory(category); //That line can be removed
category.addReport(existingReport);
It is a good practice including in Category a removeReport() method as well:
public void removeReport(Report r){
if (this.reports != null){
r.setCategory = null;
this.reports.remove(r);
}
}
That is the code of Category.java after the two methods were added:
public class Category {
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
//Code ommited for brevity
public void addReport(Report r){
if (this.reports == null){
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
public void removeReport(Report r){
if (this.reports != null){
r.setCategory = null;
this.reports.remove(r);
}
}
}
And the code for updating a report category now is this:
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
existingCategory.addReport(existingReport);
reportRepository.save(existingReport);
return new ReportUpdateDto(existingReport.getId(),
existingReport.getReportTitle(), existingReport.getCategory());
} else {
return null;
}
}
A good resource to see a practical example of synchronization in bidirectional associations: https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/
Lombok and Hibernate - not the best of the combinations
Though we can not blame Lombok for the error described in your question, many problems may arrise when using Lombok alongside with Hibernate:
Properties being loaded even if marked for lazy loading...
When generating hashcode(), equals() or toString() using Lombok, the getters of fields marked as lazy are very likelly to be called. So the programmer's initial intention of postponing some properties loading will no be respected as they will be retrieved from the database when one of hascode(), equals() or toString() is invoked.
In the best case scenario, if a session is open, this will cause additional queries and slow down your application.
In the worst case scenarios, when no session is available, a LazyInitializationException will be thrown.
Lombok's hashcode()/equals() affecting the bevahior of collections
Hibernate uses hascode() and equals() logic to check if a object is order to avoid inserting the same object twice. The same applies to removing from a list.
The way Lombok generates the methods hashcode() and equals() may affect hibernate and create inconsistent properties (especially Collections).
See this article for more info on this subject: https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/
Lombok/Hibernate integration in a nutshell
Don't use Lombok for entity classes. Lombok annotations you need to avoid are #Data, #ToString, and #EqualsAndHashCode.
Off-topic - Beware of delete-orphan
In Category, the #OneToMany mapping is defined with orphanRemoval=true as bellow:
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
The orphanRemoval=true means that when deleting a category, all the reports in that category will be deleted as well.
It is important to assess if that is the desired behavior in your application.
See an example of the SQLs hibernate will execute when calling categoryRepository.delete(category):
//Retrieving all the reports associated to the category
select
report0_.category_id as category3_1_0_,
report0_.id as id1_1_0_,
report0_.id as id1_1_1_,
report0_.category_id as category3_1_1_,
report0_.report_title as report_t2_1_1_
from
report report0_
where
report0_.category_id=?
//Deleting all the report associated to the category (retrieved in previous select)
delete from
report
where
id=?
//Deleting the category
delete from
category
where
id=?
Just an update based on the accepted answer to avoid a StackOverflow and circular loop that came up after the changes.
I had to create a new Category object to remove the reports inside it within my return dto, otherwise as the category contains that same report, that again contains that category and so on, the infinite loop could be seen on my response.
#Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingCategory.addReport(existingReport);
reportRepository.save(existingReport);
return new ReportUpdateDto(existingReport.getId(),
existingReport.getReportTitle(), existingReport.getRun_date(),
existingReport.getCreated_date(), category);
} else {
return null;
}
}
So added this part:
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingCategory.addReport(existingReport);
As if I have something like
Category category = new Category(existingCategory.getId(), existingCategory.getTitle(), existingCategory.getReports);
I can see the issue once again, which is what the existingCategory object itself contains.
And here my final entities
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "report")
#Data
public class Report {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "report_title", nullable = false)
private String reportTitle;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "category_id", nullable = false)
private Category category;
#Entity
#Table(name = "category")
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "category_title", nullable = false)
private String title;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
public Category(UUID id, String title) {
this.id = id;
this.title = title;
}
public void addReport(Report r) {
if (this.reports == null) {
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
public void removeReport(Report r) {
if (this.reports != null) {
r.setCategory(null);
this.reports.remove(r);
}
}
}

SpringBoot CascadeType ALL vs MERGE and detached entities

I have the following entities:
#Entity
#Getter #Setter #NoArgsConstructor #RequiredArgsConstructor
public class Link extends Auditable {
#Id
#GeneratedValue
private Long id;
#NonNull
private String title;
#NonNull
private String url;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "link")
private List<Comment> comments = new ArrayList<>();
#Transient
#Setter(AccessLevel.NONE)
private String userAlias ;
public String getUserAlias() {
if(user == null)
return "";
return user.getAlias();
}
#ManyToOne
private User user;
public Long getUser() {
if(user == null)
return -1L;
return user.getId();
}
public void addComment(Comment c) {
comments.add(c);
c.setLink(this);
}
}
#Entity
#Getter #Setter #NoArgsConstructor #RequiredArgsConstructor
public class Comment extends Auditable{
#Id
#GeneratedValue
private Long id;
#NonNull
private String comment;
#ManyToOne(fetch = FetchType.LAZY)
private Link link;
public Long getLink() {
return link.getId();
}
}
If I create a comment and a link, associate the link to the comment and then save that works.
Eg:
Link link = new Link("Getting started", "url");
Comment c = new Comment("Hello!");
link.addComment(c);
linkRepository.save(link);
However, if I save the comment first:
Link link = new Link("Getting started", "url");
Comment c = new Comment("Hello!");
commentRepository.save(c);
link.addComment(c);
linkRepository.save(link);
I get
Caused by: org.hibernate.AnnotationException: #OneToOne or #ManyToOne on uk.me.dariosdesk.dariodemo.domain.Comment.link references an unknown entity: uk.me.dariosdesk.dariodemo.domain.Link
at org.hibernate.cfg.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:97) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processEndOfQueue(InFlightMetadataCollectorImpl.java:1815) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processFkSecondPassesInOrder(InFlightMetadataCollectorImpl.java:1759) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1646) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:287) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:903) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:934) ~[hibernate-core-5.4.0.Final.jar:5.4.0.Final]
Changing the cascade type from ALL to MERGE seems to fix the problem and accept both implementations. (Ie: Adding a pre-existing comment or creating both and then saving via the link)
1) Why is this?
2) Is there anything I should be aware of in using MERGE rather than ALL?
Repository save method checks if entity exist. For new entity persist is called, for persisted entity merge is called.
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
In 2nd use-case Link is new entity, therefore persist() is called. With CascadeType.ALL persist() is cascaded to Comment entity. Comment is already persisted and needs to be merged, persist() fails.
If you use CascadeType.MERGE persist() is not cascaded down to Comment. It does not fail.

Lazy attribute is null inside transaction after creation

I have a small example with some get/post mappings and JpaRepository calls in Spring Boot.
Firstly I have two entity Classes:
#Entity
#Table(name = "stock")
public class Stock extends BaseEntity
{
#Column(name = "value")
public String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
#Entity
#Table(name = "stock_item")
public class StockItem extends BaseEntity
{
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "stock_id", insertable = false, updatable = false)
public Stock stock;
#Column(name = "stock_id")
public Long stockId;
#Column(name = "value")
public String value;
}
I have a many-to-one association from StockItem to Stock.
I insert a Stock and have a controller as below:
#Autowired
public Controller(StockItemRepository stockItemRepository) {
this.stockItemRepository = stockItemRepository;
}
#RequestMapping("/")
#Transactional(readOnly = true)
public String get() {
List<StockItem> stockItemList = stockItemRepository.getItemsById(1L);
System.out.println("TX MANAGER: " + TransactionSynchronizationManager.isActualTransactionActive());
for (StockItem stockItem : stockItemList) {
System.out.println(stockItem.getStock().getValue());
}
return "get";
}
#RequestMapping("/fromSave")
#Transactional
public String post() {
StockItem stockItem = new StockItem();
stockItem.setStockId(1L);
stockItemRepository.saveAndFlush(stockItem);
System.out.println("saveCalled");
return get();
}
and getItemsById in the repository is defined as follows:
#Query("FROM StockItem si " +
"JOIN FETCH si.stock stk " +
"WHERE si.stockId = :id")
List<StockItem> getItemsById(#Param("id") Long id);
From my understanding, when I call the post method:
it creates a new item
sets the id of the associated attribute
saves and ends the transaction
Heres where things get strange...
I call get after the post and make the above repository call, which has a join fetch and when I call stockitem.getStock().getValue() I get a null pointer when I expect a LazyInitializationException.
If I call the get() from the mapping, outside the class, it successfully loads the associated object.
I have even removed the #Transaction annotation from the get, as well as
the join-fetch from my query and again, if I call from outside of the class it works and from the post, it crashes with a NullPointerException.
I have put the get inside of a TransactionTemplate.execute() and I still get a NullPointerException when calling from inside the class.
So the main questions are:
Why am I getting a NullPointerException instead of LazyInitializationException?
What is the transaction magic behind having no transaction but successfully fetching a lazy attribute??
The problem here is that you are misusing JPA. As you are seemingly aware judging from the comments on the other answer you have mapped the stock_id column twice. Once as a many-to-one relationship
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "stock_id", insertable = false, updatable = false)
public Stock stock;
and once as a simple column
#Column(name = "stock_id")
public Long stockId;
When you set the simple column and flush the changes as in your post() method the following happens:
the value gets set in the simple column. The reference is still null.
the value gets stored in the database. The reference is still null.
The repository call will find the id of the StockItemin the Persistence Context and return that instance, i.e. the exact same used in the post method, with the reference still null.
What is the transaction magic behind having no transaction but successfully fetching a lazy attribute??
No magic involved here. fetch specifications are only used for object traversal. JPQL queries don't honor these.
The unasked question remains: how to fix the situation?
The obvious fix is to lose the simple column and just use entity references as intended by JPA.
You don't want to do that in order to avoid DB access somewhere. But as long as you only access the id of the referenced Stock it shouldn't get initialized. So it seems that this should be possible with just Lazy Fetching.
Alternatively, I'd suggest removing the many-to-one relationship and creating a repository for Stock and manually loading it when required.
#Entity
#Table(name = "stock_item")
public class StockItem extends BaseEntity
{
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "stock_id", insertable = false, updatable = false) //here is your problem
public Stock stock;
#Column(name = "stock_id")
public Long stockId; // why explicitly define a separate column for foreign key after mapping it above
#Column(name = "value")
public String value;
}
with insertable = false and updatable = false it won't insert in your DB and neither it will allow updation, so you are getting NullPointerException. You should atleast allow insertion in order to run the query based on the foreign key stock_id
UPDATE
Change your Entity class with property-based access:
#Entity
#Table(name = "stock_item")
public class StockItem extends BaseEntity
{
private Stock stock; // variables should always be private since you have getters and setters
private String value;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "stock_id", updatable = false)
public Stock getStock() {
return stock;
}
public void setStock(Stock stock) {
this.stock = stock;
}
#Basic
#Column(name = "value")
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

Hibernate join two entities

i really don't know what actually my problem is.
I have two models in my Project.
model-package
Ansprechpartner
Lieferant
Ansprechpartner.java
#Entity
#Table(name = "ANSPRECHPARTNER")
#EntityListeners(AuditingEntityListener.class)
#JsonIgnoreProperties(value = {"anlageAm", "updatedAt"}, allowGetters = true)
public class Ansprechpartner {
...
#NotNull
#ManyToOne
#JoinColumn(name = "lief_code", foreignKey=#ForeignKey(name = "APART_LIEF_FK"))
private Lieferanten liefCode;
public Lieferanten getLiefCode() {
return liefCode;
}
public void setLiefCode(Lieferanten liefCode) {
this.liefCode = liefCode;
}
...
}
Lieferant.java
#Entity
#Table(name = "LIEFERANTEN")
#EntityListeners(AuditingEntityListener.class)
#JsonIgnoreProperties(value = {"anlageAm"}, allowGetters = true)
public class Lieferanten {
...
#Id
private String code;
#OneToMany(mappedBy = "liefCode")
private Set<Ansprechpartner> apart;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Set<Ansprechpartner> getApart() {
return apart;
}
public void setApart(Set<Ansprechpartner> apart) {
this.apart = apart;
}
...
}
My Controller:
#RestController
#RequestMapping("/apart")
public class AnsprechpartnerController {
...
#GetMapping("/all/{id}")
public Ansprechpartner getApartWithId(#PathVariable("id") long id) {
Ansprechpartner apart = apartRepository.findOne(id);
return apartRepository.findOne(id);
}
}
When i try to get the json data i get the following problem. Ansprechpartner gets data from Lieferant (because of that join). But then Lieferant again shows data from Ansprechpartner and so on.
Maybe better described with the following picture:
Image with explanation
EDIT:
I finally solved it with the #JsonIgnoreProperties annotation:
In my Ansprechpartner.java i did it this way:
#NotNull
#JsonIgnoreProperties("apart")
// #JsonManagedReference
#ManyToOne
#JoinColumn(
name = "lief_code",
foreignKey=#ForeignKey(name = "APART_LIEF_FK")
)
private Lieferanten liefCode;
And in my Lieferanten.java i did it this way:
// #JsonBackReference
#JsonIgnoreProperties("liefCode")
#OneToMany(mappedBy = "liefCode", fetch = FetchType.LAZY)
private Set<Ansprechpartner> apart;
To avoid infinite recursions you can use #JsonManagedReference & #JsonBackReference
Json Infinite Recursion is one of the most common problems when we serialize Java objects which having Bidirectional-Relationships.
#JsonManagedReference: a part with the annotation will be serialized normally.
#JsonBackReference: a part with the annotation will be omitted from serialization.
like:
#JsonBackReference
private Set<Ansprechpartner> apart;
You can check details in solution-2
Strange behaviour. Possibly you could try:
1) Make sure in the Lieferanten entity, in the equals / hashCode you do not use the Set<Ansprechpartner> apart.
2) You can explicitly detach the entities from the persistence context:
#NotNull
#ManyToOne
#JoinColumn(name = "lief_code"
, foreignKey=#ForeignKey(name = "APART_LIEF_FK")
, cascade={CascadeType.DETACH})
private Lieferanten liefCode;
and then in the controller:
#GetMapping("/all/{id}")
public Ansprechpartner getApartWithId(#PathVariable("id") long id) {
Ansprechpartner apart = apartRepository.findOne(id);
apartRepository.detach(apart);
return apart;
}
you would need to implement a bit -> link, in repository in order to have that available.
3) explicitly add lazy loading: #OneToMany(mappedBy = "liefCode", fetch = FetchType.LAZY).
The root cause is jackson trying to serialize object when object has Bidirectional-Relationships.
You can fixed it by this way
Short way
Better way :
Returning entities directly to view layer is not a good practice.
You should convert entities to DTOs (Data Transfer Object) and pass the DTOs to view

How to correctly setup bi-directional one-to-many relationship in Hibernate

I have gone through several Q/As on stackoverflow and several other tutorials online to find what am I exactly missing for the problem described below.
Background:
I am learning to use restful APIs in my android application and for that reason, I have written a simple doctor-patient management app. There's a one to many relationship between a doctor and his patients. i.e. One doctor can have many patients.
Problem:
I am using one user table that is supposed to maintain all the user information, i.e. doctor and patients' basic info is maintained in this table and this table is also used for determining what type of user is trying to log in, so that appropriate screens can be presented. Here's how the entity for that table looks like:
#Entity
public class ConcreteUser implements User{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name= "USER_ID")
private long id;
private String name;
private String email;
private int age;
private SEX sex;
private String accessLevel;
public ConcreteUser() {
}
// gettersand setters here
}
This Entity has one to one relationship with tables that maintain doctors and patient entities. And as mentioned earlier, doctors and patient entities have one to one relationship. Here's how those two entities look like:
#Entity
public class PatientEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "PATIENT_RECORD_ID")
private long recordId;
// specify this as a foreign key from ConcreteUser entity
#OneToOne(cascade = CascadeType.ALL) /*CascadeType.ALL should not be required according to almost all the tutorials I have seen - But I always get unsaved transient object error if I don't do this and try to save a patient entity */
#JoinColumn(name="USER_ID")
private ConcreteUser patient;
#ManyToOne(cascade = {CascadeType.ALL})
#JoinColumn(name = "DOCTOR_RECORD_ID")
#JsonBackReference
private DoctorEntity doctor;
public PatientEntity() {
}
public void setDoctor(DoctorEntity doctor) {
this.doctor = doctor;
//if(!doctor.getPatients().contains(this)){
// doctor.addPatient(this);
//}
/* Commented out code always leads to stack overflow error */
/* although, according to tutorial in the link below, this code is necessary */
/* http://en.wikibooks.org/wiki/Java_Persistence/OneToMany */
}
// getters and setters are not shown
}
And lastly, here's my Doctor entity:
#Entity
public class DoctorEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "DOCTOR_RECORD_ID")
private long recordId;
// specify this as a foreign key from ConcreteUser entity
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name="USER_ID")
private ConcreteUser doctor;
#OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL)
#JsonManagedReference
private Collection<PatientEntity> patients = new ArrayList<PatientEntity>();
public DoctorEntity() {
}
public boolean addPatient(PatientEntity p) {
boolean status = false;
status = patients.add(p);
//if (p.getDoctor() != this) {
// p.setDoctor(this);
//}
return status;
}
public boolean removePatient(PatientEntity p) {
boolean status = false;
status = patients.remove(p);
//if (p.getDoctor() != this) {
// p.setDoctor(null);
//}
return status;
}
// getters and setters are not shown. Same problem with the commented out code as described above
}
Now to test the fact that, when a POJO object of PatientEntity can be saved and it retains the information, I am using the following test case:
#Test
public void TestPatientDoctorManyToOne() throws Exception{
PatientEntity p1 = TestData.getPatientEntity(patient1);
DoctorEntity d = TestData.getDoctorEntity(doctor);
p1.setDoctor(d);
p1 = patientService.addPatient(p1);
assertNotNull(p1);
PatientEntity p2 = patientService.getPatientById(p1.getRecordId());
assertNotNull(p2);
assertNotNull(p2.getDoctor());
assertEquals(p1.getRecordId(), p2.getRecordId());
assertEquals(p1.getDoctor().getRecordId(), p2.getDoctor().getRecordId());
assertEquals(p1.getDoctor().getDoctor().getEmail(), p2.getDoctor().getDoctor().getEmail());
}
In the above test case, assertNotNull(p2.getDoctor()); assertion fails, as the returned patient entity does not contain doctor object at all.
Here's the log:
Outgoing:
"{"recordId":0,"patient":{"id":0,"name":"Patient-0ee1407e-2d2b-4c6c-a57b-e2fad24fafa5","email":"0ee1407e-2d2b-4c6c-a57b-e2fad24fafa5","age":50,"sex":"MALE","accessLevel":"patient"},"doctor":{"recordId":0,"doctor":{"id":0,"name":"Doctor-f025c8ce-8c31-4681-b673-a9e322dccf5a","email":"f025c8ce-8c31-4681-b673-a9e322dccf5a","age":50,"sex":"MALE","accessLevel":"doctor"},"patients":[]}}"
Incoming:
{"recordId":16,"patient":{"id":33,"name":"Patient-0ee1407e-2d2b-4c6c-a57b-e2fad24fafa5","email":"0ee1407e-2d2b-4c6c-a57b-e2fad24fafa5","age":50,"sex":"MALE","accessLevel":"patient"}}
As you can see, the returned object doesn't have a Doctor entity at all.
However, when I try to save the doctor entity with patients in it, it is saved with no problem. i.e. the following test case passes:
#Test
public void testDoctorPatientOneToMany() throws Exception {
PatientEntity p1 = TestData.getPatientEntity(patient1);
PatientEntity p2 = TestData.getPatientEntity(patient2);
DoctorEntity d = TestData.getDoctorEntity(doctor);
d.addPatient(p1);
d.addPatient(p2);
d = doctorService.addDoctor(d);
DoctorEntity d2 = doctorService.getDoctorById(d.getRecordId());
assertNotNull(d2);
assertEquals(d2.getRecordId(), d.getRecordId());
assertEquals(d2.getDoctor().getEmail(), d.getDoctor().getEmail());
}
Transactions for the above test case:
Outgoing:
"{"recordId":17,"doctor":{"id":43,"name":"Doctor-e4baeee7-eaaa-443e-8845-e0b12d7be82f","email":"e4baeee7-eaaa-443e-8845-e0b12d7be82f","age":50,"sex":"MALE","accessLevel":"doctor"},"patients":[{"recordId":21,"patient":{"id":44,"name":"Patient-d8aab5ad-d3d9-4442-b8de-678de9e3b1ce","email":"d8aab5ad-d3d9-4442-b8de-678de9e3b1ce","age":50,"sex":"MALE","accessLevel":"patient"}},{"recordId":22,"patient":{"id":45,"name":"Patient-5c9cfa3c-ee79-4aea-a193-4d8762f58431","email":"5c9cfa3c-ee79-4aea-a193-4d8762f58431","age":50,"sex":"MALE","accessLevel":"patient"}}]}[\r][\n]"
Incoming:
{"recordId":17,"doctor":{"id":43,"name":"Doctor-e4baeee7-eaaa-443e-8845-e0b12d7be82f","email":"e4baeee7-eaaa-443e-8845-e0b12d7be82f","age":50,"sex":"MALE","accessLevel":"doctor"},"patients":[{"recordId":21,"patient":{"id":44,"name":"Patient-d8aab5ad-d3d9-4442-b8de-678de9e3b1ce","email":"d8aab5ad-d3d9-4442-b8de-678de9e3b1ce","age":50,"sex":"MALE","accessLevel":"patient"}},{"recordId":22,"patient":{"id":45,"name":"Patient-5c9cfa3c-ee79-4aea-a193-4d8762f58431","email":"5c9cfa3c-ee79-4aea-a193-4d8762f58431","age":50,"sex":"MALE","accessLevel":"patient"}}]}
I apologize for along post, but I think I have exhausted all my resources. I'd absolutely worship anyone who decides to take a look at it and points out where the problem is. At this point, I am not even sure if I am testing this thing right, or my expectations are correct.
This is because by specifying mappedBy="doctor" in the DoctorEntity class
#Entity
public class DoctorEntity {
#OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL)
#JsonManagedReference
private Collection<PatientEntity> patients = new ArrayList<PatientEntity>();
public DoctorEntity() {
}
}
you are saying that DoctorEntity is no more the owner of the one-to-many relationship. PatientEntity is the owner. Hence during the save of PatientEntity (in the first test case) the foreign key of doctor entity is not updated in the Patient table.
mappedBy is equivalent to specifying inverse=true in an xml format.
Follow this link for a detailed explanation on what queries are executed when inverse=true or inverse=false is specified in the one-to-many mapping.

Resources