how to manage jpa bidirectional relation ships correctly? - jpa

I am using EJB3.2 and JPA 2.1.
In my application I have these entities (in brief) :
#Entity
public class Festival
{
int Id;
String name;
...
#ManyToOne
#JoinColumn(name = "merchant", referencedColumnName = "Id")
Merchant merchant;
}
#Entity
public class Merchant
{
int Id;
String name;
...
#ManyToOne
#JoinColumn(name = "category", referencedColumnName = "Id")
Category category;
#ManyToOne
#JoinColumn(name = "city", referencedColumnName = "Id")
City city;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "festival")
List<Festival> festivalList;
}
#Entity
public class Category
{
int Id;
String name;
...
#OneToMany(cascade = CascadeType.ALL, mappedBy = "merchant")
List<Merchant> merchantList;
}
#Entity
public class City
{
int Id;
String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "merchant")
List<Merchant> merchantList;
}
thus, (Festival, Merchant) has a bidirectional relationship like (Merchant, Category) and (Merchant, City)
the problem is when i remove or change a merchant, still there is an unchanged copy of it in categories merchantList and cities merchantList ! and so on..
how should I manage these changes?! why JPA doesn't do the changes to other copies it self? doesn't it increase the wrong copies risk?!

JPA treats your entities like regular java objects and does not maintain relationships for you - the application is responsible for ensuring that both sides of a bidirectional relationship are kept in sync when you change one side. So when you dereference a City from a Merchant, you must remove the reference to the Merchant from the City's merchantList and then merge if necessary.
Otherwise your view of the data via the java objects will become out of synch with the database until the objects are refreshed from the database. You can weigh the value and costs of keeping both sides in sync or refreshing when needed or even not mapping the non-owning side and determine what is better for your application on an entity by entity basis.

Make sure to persist(merchant) before calling categories.getMerchantList().

I think this quote from "Java Persistence With Hibernate" (p. 261) by Christian Bauer and Gavin King may be useful for you :
Contrary to EJB 2.0 CMR, Hibernate and JPA associations are all
inherently unidirectional.
So the association from Merchant to Category is different than association from Category to Merchant, and you need to manage them separately.
edit:
I adjusted your initial bean config to look as it should:
(Of course, you will use an appropriate primary key generation strategy.)
#Entity
public class Festival
{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
int Id;
String name;
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name = "merchant")
Merchant merchant;
}
#Entity
public class Merchant
{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
int Id;
String name;
#ManyToOne
#JoinColumn(name = "category")
Category category;
#ManyToOne
#JoinColumn(name = "city")
City city;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "merchant")
List<Festival> festivalList;
}
#Entity
public class Category
{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
int Id;
String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "category")
List<Merchant> merchantList;
}
#Entity
public class City
{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
int Id;
String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "city")
List<Merchant> merchantList;
}

Related

Spring Data Jpa OneToMany save bidirectional

I have a problem with saving child entities.
Here is my example. My model classes look like this:
#Entity
public class ImportDocument {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
private boolean imported;
#Transient
private Status status;
#Basic
private char statusValue;
#OneToMany(mappedBy = "importDocument" , cascade = {CascadeType.ALL})
private List<ImportDocumentItem> importDocumentItems;
}
#Entity
public class ImportDocumentItem {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "import_document_id")
#JsonIgnore
private ImportDocument importDocument;
}
I have implemented JpaRepository interfaces for both domain classes.
I try to save with:
importDocumentRepository.save(importDocument);
When I save ImportDocument object, everything is inserted. But the problem is that, the import_document_item.import_document_id (which is foreign key of import_document_id) attribute is filled with null value, not with id of import_document that I expected. How can I fix this issue?
Thanks a lot.
You have to set entity relations on both side before saving. Here an example
ImportDocument importDocument = new ImportDocument();
//...
importDocument.setImportDocumentItems(items);
items.forEach(ImportDocumentItem::setImportDocument);
importDocumentRepository.save(importDocument);

Spring Data Envers Entity must not be null

Suppose we have audited entities with #OneToOne relation:
#Entity
#Audited
#Table(name = "product")
public class Product {
#Id
#GeneratedValue
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#Column(name = "active")
private boolean active;
#OneToOne(mappedBy="product", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private ProductPrice productPrice;
}
#Audited
#Entity
#Table(name = "product_price")
public class ProductPrice {
#Id
#GeneratedValue
#Column(name = "id")
private Long id;
#Column(name = "amount")
private Long amount;
#OneToOne(optional = false, fetch = FetchType.LAZY)
#JoinColumn(name = "product_id", nullable = false)
private Product product;
}
And RevisionService with the method to get revisions and find changes:
#Transactional
public Page<Revision<Long, Product>> getGroupRevisions(Long productId, int page) {
Page<Revision<Long, Product>> revisions = productRepository.findRevisions(productId, PageRequest.of(page, 5, RevisionSort.desc()));
Long priceId = revisions.getContent().get(0).getEntity().getProductPrice().getId();
Page<Revision<Long, ProductPrice>> priceRevisions = productPriceRepository.findRevisions(priceId, PageRequest.of(page, 5, RevisionSort.desc()));
return revisions;
}
Now, If I create new Product and ProductPrice records and then make changes into Product more then 5 times (5 RevInfo records would generated), I get exception:
java.lang.IllegalArgumentException: Entity must not be null!
at org.springframework.util.Assert.notNull(Assert.java:198)
at org.springframework.data.history.AnnotationRevisionMetadata.<init>(AnnotationRevisionMetadata.java:55)
at org.springframework.data.envers.repository.support.EnversRevisionRepositoryImpl.getRevisionMetadata(EnversRevisionRepositoryImpl.java:237)
at org.springframework.data.envers.repository.support.EnversRevisionRepositoryImpl.lambda$toRevisions$1(EnversRevisionRepositoryImpl.java:223)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
at java.base/java.util.HashMap$EntrySpliterator.forEachRemaining(HashMap.java:1837)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
at org.springframework.data.envers.repository.support.EnversRevisionRepositoryImpl.toRevisions(EnversRevisionRepositoryImpl.java:226)
at org.springframework.data.envers.repository.support.EnversRevisionRepositoryImpl.getEntitiesForRevisions(EnversRevisionRepositoryImpl.java:196)
at org.springframework.data.envers.repository.support.EnversRevisionRepositoryImpl.findRevisions(EnversRevisionRepositoryImpl.java:163)
After debugging I saw that this "null" entity was proxied by Hibernate and Spring Data envers could not resolve revision number in this point:
Number revNo = this.enversService.getRevisionInfoNumberReader().getRevisionNumber(revision);
Here is the link to github test project: https://github.com/aquariusmaster/spring-data-envers-bug
So my question is this a bug in Spring Data Envers or I miss something in the configuration?
As spring-data-envers team replied, upgrading boot version to 2.3.1.RELEASE solve the problem:
https://github.com/spring-projects/spring-data-envers/issues/34#issuecomment-651681687

JPA Get an entity by intermediate entity

I have 3 entities named Student, Course, and StudentCourse as follows
#Entity
#Table(name = "student")
public class Student {
#Id
#GeneratedValue
private Integer id;
private String fullName;
}
#Entity
#Table(name = "course")
public class Course {
#Id
#GeneratedValue
private Integer id;
private String courseName;
}
#Entity
#Table(name = "student_course")
public class StudeCourse {
#Id
#GeneratedValue
private Integer studentId;
private Integer courseId;
private String extraColumn;
}
Restrictions: There are a couple of restrictions
One student can have only one course or no course at all
An extra entity (StudentCourse) is required to hold the relation with primary key as studentId only
StudentCourse is required and hence cannot be skipped
Get Student with Course entity if there is one registered
Help required in some magical code to retrieve Course of Student if there is one assigned.
#Entity
#Table(name = "student")
public class Student {
#Id
#GeneratedValue
private Integer id;
private String fullName;
// this is not correct code but just what I want
#JoinEntity(entity=StudentCourse, column="courseId")
private Course course;
}
StudentCourse is required and hence cannot be skipped
Ok, lets work with that.
One student can have only one course or no course at all
Implies that there is a #OneToOne relationship between Student and StudentCourse.
With the given information, the following entity model will work:
#Entity
#Table(name = "student")
public class Student {
#Column(name = "id")
#Id
#GeneratedValue
private Integer id;
#Column(name = "full_name")
private String full_name;
#OneToOne
#PrimaryKeyJoinColumn
private StudentCourse studentCourse;
}
#Entity
#Table(name = "student_course")
public class StudentCourse {
#Column(name = "id")
#Id
#GeneratedValue
private Integer id;
#JoinColumn(name = "id", nullable = false, updatable = false)
#MapsId
#OneToOne
private Student student;
#JoinColumn(name = "course_id")
#ManyToOne
private Course course;
...
}
A quick review:
#OneToOne on the Student.studentCourse field signifies that for every Student, there can be only one StudentCourse, and no more.
#PrimaryKeyJoinColumn on the Student.studentCourse field signifies that the value of the primary key column for Student should be used as the foreign key for the related entity, that is, StudentCourse.
#OneToOne on the StudentCourse.student field signifies that for every StudentCourse, there can be only one Student.
#MapsId on the StudentCourse.student field signifies that the primary key column for StudentCourse should be used as the join column for the association.
To check if a student has a course assigned, simply check if student.getStudentCourse() != null and then get the assigned course as student.getStudentCourse().getCourse().

JPA two Entities one Relationship: How do I obtain a Set of an entity that is linked through a relationship?

I have three tables each mapping to one of these entities. The 'assigned' table acts as the relationship between 'users' and 'roles' with a foreign key to each table. How would I map this on my entities so that I can get a Set of EntityRoles from the UserEntity? I can't quite figure out how to make this work. Is this even possible?
#Entity
#Table(name = "users")
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="user_id")
private long id;
#Column(name="user_username")
private String username;
#Column(name="user_password")
private String password;
#Column(name="user_email")
private String email;
//I want to be able to get a set of RoleEntities
#OneToMany(fetch = FetchType.LAZY, mappedBy = "id")
private Set<RoleEntity> roles;
}
#Entity
#Table(name = "assigned")
public class AssignedEntity implements Serializable {
#Id
//#Column(name = "assigned_role")
#ManyToOne(targetEntity = RoleEntity.class, fetch = FetchType.LAZY)
#JoinColumn(name = "fk_role")
private long roleId;
#Id
//#Column(name = "assigned_user")
#ManyToOne(targetEntity = UserEntity.class, fetch = FetchType.LAZY)
#JoinColumn(name = "fk_user")
private long userId;
}
#Entity
#Table(name = "roles")
public class RoleEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="role_id")
#OneToOne(fetch = FetchType.LAZY, mappedBy="roleId")
private long id;
#Column(name="role_name")
private String name;
}
You are using an incorrect/inconvenient mapping. Always keep things as simply as possible.
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue
private Long id;
#ManyToMany(fetch = FetchType.LAZY)
private List<Role> roles;
}
#Entity
#Table(name = "roles")
public class Role {
#Id
private Long id;
#Column
private String name;
}
A persistent provider will create a (valid) join table for you. You can specify the name of the join table using #JoinTable annotation. Also you will need to think about auto generation values of id for the Role entity: the roles table is something like a reference data table. So, probably, you will need to hardcode the id values.
To get user roles (in the persistent context):
user.getRoles()

Can I somehow cascade.PERSIST the map key in a Map<Entity, Entity>?

The situation: I have a class with a Map like this:
#Entity
public class Bar {
#Id
#GeneratedValue
private Long id;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "fooMap",
joinColumns = #JoinColumn(name = "One_ID", referencedColumnName = "ID") ,
inverseJoinColumns = #JoinColumn(name = "Two_ID", referencedColumnName = "ID") )
#MapKeyJoinColumn(name = "Bar")
private Map<FooOne, FooTwo> fooMap;
}
with key/value-classes like this (FooTwo looks more or less the same):
#Entity
public class FooOne {
#Id
#GeneratedValue
private Long id;
#Basic
private String name;
If I populate the map and try to persist it, I get the following exception:
UnitOfWork(117839394)--java.lang.IllegalStateException: During synchronization >a new object was found through a relationship that was not marked cascade >PERSIST: jpa.test.minimalExample.FooOne#2e4389ed.
because JPA cascades only to the target of the -toMany association which is the value entity.
Does JPA in general or EclipseLink as implementation offer any form of annotation to cascade actions to the key class of this map? What are my options here ?
I know adding a reference to FooOne in the FooTwo-class works (like this):
#Entity
public class FooTwo {
#Id
#GeneratedValue
private Long id;
#OneToOne(cascade = cascadeType.ALL)
private FooOne foo1;
}
I would rather not add any additional fields to my classes if I can avoid it.