I am using EclipseLink for this. I have a ternary relation between three entities called Staff, Person and Job. I introduced the Embeddable class StaffItem that consists solely of a Person and Job. Staff has an ElementCollection of StaffItems.
I have no problem persisting new StaffItems to the Database, that were added to a Staff Entity, but whenever I change one item or delete it and try to merge the existing Staff Entity, the EntityManager seems to run into an infinite loop on the flushing. I do not get an error or exception, I simply do not return from the flush().
Staff.java
#Entity
public class Staff {
private List<StaffItem> staffItems;
#ElementCollection
#CollectionTable(name = "staff_items", joinedColumns = #JoinedColumn(name = "staff"))
public List<StaffItem> getStaffItems() { ... }
// setter, etc.
}
StaffItem.java
#Embeddable
public class StaffItem {
private Person person;
private Job job;
#ManyToOne
#JoinColumn(name = "person", referencedColumn = "id")
public Person getPerson() { ... }
#ManyToOne
#JoinColumn(name = "job", referencedColumn = "id")
public Job getJob() { ... }
// setter, etc.
}
Related
I followed the example of Modeling With a Shared Primary Key as below:
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
//...
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
#PrimaryKeyJoinColumn
private Address address;
//... getters and setters
}
#Entity
#Table(name = "address")
public class Address {
#Id
#Column(name = "user_id")
private Long id;
//...
#OneToOne
#MapsId
#JoinColumn(name = "user_id")
private User user;
//... getters and setters
}
However, if there are already a record with id 123456 in address table, then I tried to update the record like below:
Address po = new Address();
po.setId(123456L);
po.setCountry("TW");
AddressRepository.save(po);
Duplicate entry '123456' for key Exception will occur. Why JPA will insert a new record instead of merging it? How to solve this problem?
I know the reason finally. It is because the entity has version field and the version field in the new entity is null.
We need to dig into the source of of save() method in JPA.
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
Then, if we don't override the isNew(), it will use the default isNew() of JpaMetamodelEntityInformation.
#Override
public boolean isNew(T entity) {
if (!versionAttribute.isPresent()
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity);
}
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
Here, we can see that if version is present and the version is different from the existing record in the database, the entity will be a new entity and JPA will execute the insert action. Then, it will occur the error of duplicate entry.
Let's say we have the following three domain model entities: Company, Departament, and Employee.
#Getter #Setter #NoArgsConstrutor
public class Employee {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "department_id", nullable = false, insertable = false, updatable = false)
private Department department;
#JoinColumn(name = "department_id", nullable = false)
private int department_id;
}
#Getter #Setter #NoArgsConstrutor
public class Department {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id", nullable = false, insertable = false, updatable = false)
private Company company;
#JoinColumn(name = "company_id", nullable = false)
private int company_id;
#OneToMany(mappedBy = "department")
private List<Employee> employees;
}
#Getter #Setter #NoArgsConstrutor
private class Company {
private String name;
#OneToMany(mappedBy = "company")
private List<Department> departments;
}
For each entity, we have Repositories which extend JpaRepository, Services, and Controllers. In each Service we #Autowire the respective Repository, and in each entity Controller we call methods from the entity Service.
My issue is the following: I cannot save an entire Company, because the Departments require a Company ID, and Employees a Deparment ID. So, firstly, in my CompanyService I save and then clear the departments list, do a saveAndFlush which assigns an ID to my company. I assign the received ID to every company_id in each entity of the previously saved departments list, then attach the list back to the company and do another saveAndFlush, and I do this one more time for the employee list.
#RestController
public class CompanyController {
#Autowire
private CompanyService companyService;
#PostMapping("/companies")
public Company createCompany(#RequestBody Company newCompany) {
return companyService.createCompany(newCompany);
}
}
#Service
public class CompanyService {
#Autowire
private CompanyRepository companyRepository;
public Company createCompany(Company company) {
List<Department> departments = new ArrayList<>(company.getDepartments());
company.getDepartments().clear();
companyRepository.saveAndFlush(company);
int company_id = company.getId();
departments.forEach (department ->
department.setCompany_id(company_id);
);
//here I save a copy of the previously saved departments, because I still need the employees
company.getDepartments().addAll(departments.stream().map(department -> department.clone(department)).collect(Collectors.toList()));
company.getDepartments().forEach(department -> department.getEmployees().clear());
companyRepository.saveAndFlush(company);
//here I assign each employee it's corresponding department ID
for (int i = 0; i < company.getDepartments().size(); i++) {
Department departmentInSavedCompany = company.getDepartments().get(i);
Department departmentWhichStillHasEmployees = departments.get(i);
departmentWhichStillHasEmployees.setId(departmentInSavedCompany.getId());
departmentWhichStillHasEmployees.getEmployees().forEach(employee -> employee.setDepartment_id(departmentInSavedCompany.getId()));
}
company.getDepartments.clear();
company.getDepartments.addAll(departments);
return companyRepository.saveAndFlush(company);
}
}
#Repository
public interface CompanyRepository extends JpaRepository<Company, Integer> {
}
I currenty do not like this implementation neither do I find it good. Which is the correct approach for this situation?
When working with JPA, do not work with IDs, work with object references.
In your case, this means removing the id attributes that duplicate the references.
In order to obtain the proper entities for IDs use JpaRepository.getOne. It will return either the entity if it is already in the 1st level cache or a proxy just wrapping the id, so it won't hit the database.
This allows you to assemble your object graph and persist it in one pass starting with the entity having no references to other entities.
You might also consider configuring cascading, if you consider entities to be part of the same Aggregate, i.e. they should be loaded and persisted together.
I have a Company entity that I fetch with a JPQL query with Hibernate. The entity has a many-to-many association with a Keyword entity. Since the join table has an additional column is_active, this table has been mapped to a CompanyKeyword entity. So the association is like this:
Company <-- CompanyKeyword --> Keyword
Now, the association from the Company entity is lazy, and it is not initialized by my JPQL query, as I want to avoid creating a cartesian product performance problem. That is why I want to initialize the association after running the JPQL query, e.g. like this:
#Service
class CompanyServiceImpl implements CompanyService {
#Autowired
private CompanyRepository companyRepository;
#Transactional
public Company findOne(int companyId) {
Company company = this.companyRepository.findOneWithSomeCustomQuery(companyId);
Hibernate.initialize(company.companyKeywords());
return company;
}
}
For a "normal" many-to-many association, this would work great, as all of the associated entities would be fetched in a single query. However, since I have an entity between Company and Keyword, Hibernate will only initialize the first part of the association, i.e. from Company to CompanyKeyword, and not from CompanyKeyword to Keyword. I hope that makes sense. I am looking for a way to initialize this association all the way without having to do something like this:
Company company = this.companyRepository.findOneWithSomeCustomQuery(companyId);
Hibernate.initialize(company.getCompanyKeywords());
for (CompanyKeyword ck : company.getCompanyKeywords()) {
Hibernate.initialize(ck.getKeyword());
}
The above code is neither clean, nor good in terms of performance. If possible, I would like to stick to my current approach of using a JPQL query to fetch my Company entity and then initializing certain associations afterwards; it would take quite a bit of refactoring to change this in my project. Should I just "manually" fetch the association with a second JPQL query, or is there a better way of doing it that I haven't thought of?
Below are my mappings. Thanks in advance!
Company
#Entity
#Table(name = "company")
public class Company implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
#Size(max = 20)
#OneToMany(fetch = FetchType.LAZY, mappedBy = "company")
private Set<CompanyKeyword> companyKeywords = new HashSet<>();
// Getters and setters
}
CompanyKeyword
#Entity
#Table(name = "company_service")
#IdClass(CompanyServicePK.class)
public class CompanyKeyword implements Serializable {
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Company.class)
#JoinColumn(name = "company_id")
private Company company;
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Keyword.class)
#JoinColumn(name = "keyword_id")
private Keyword keyword;
#Column(nullable = true)
private boolean isActive;
// Getters and setters
}
CompanyKeywordPK
public class CompanyServicePK implements Serializable {
private Company company;
private Service service;
public CompanyServicePK() { }
public CompanyServicePK(Company company, Service service) {
this.company = company;
this.service = service;
}
// Getters and setters
// hashCode()
// equals()
}
Keyword
#Entity
#Table(name = "keyword")
public class Keyword {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
// Fields and getters/setters
}
You'll indeed need to execute an additional JPQL query, fetching the company with its companyKeyWords and with the keyword of each CompanyKeyWord.
You could also doing it by simply looping and initializing every entity, and still avoid executing too many queries, by enabling batch fetching.
Newbie to Hibernate here. I'm building a simple app to play around with Hibernate and I'm getting the hang of most of the annotations but the mappings are really confusing me.
I have a Person class and I have a Note's class. A person can have many notes associated with them but a single note will only ever correspond to a specific person.
I'm trying to set it up so that the note table has a column called person_id such that I won't need an extra person_note table in the database for the associations.
How would I go about setting up the annotations such that an extra table is not created in the database and I can associate multiple notes to a single person via an extra column in the note's table?
I've tried a few options after searching on Google such as using annotations like this but with no luck:
#JoinColumn(name="id_person", nullable=false)
Person Class
#Entity
#Table(name = "person")
public class Person {
#OneToMany()
private Set<Note> notes;
public Set<Note> getNotes() {
return notes;
}
public void setNotes(Set<Note> notes) {
this.notes = notes;
}
...
}
Note Class
#Entity
#Table (name = "note")
public class Note {
#ManyToOne
private Person person;
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
...
}
Any help would be appreciated. Thank you.
Final Working Solution
For the benefit of anyone looking in the future, I now don't have a separate table for mapping note objects to people objects. The final code (with extra lines removed) now looks like this:
Person Class
#Entity
#Table(name = "person")
public class Person {
#OneToMany(fetch = FetchType.EAGER, mappedBy = "person", cascade = CascadeType.ALL)
private Set<Note> notes;
...
}
Note Class
#Entity
#Table (name = "note")
public class Note {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "id_person", nullable = false)
private Person person;
...
}
Misc Points
I had to add cascade = CascadeType.ALL in the Person class to ensure that when I saved a Person object that all the Note objects inside were saved.
I combined the code from chsdk and Saif. Thank you to both of you.
In your mapping annotations you should map the entities with a mappedBy property in the #OneToMany annotation and specify a joinColumn under the #ManyToOne annotation using the #JoinColumn annotation, Change your code like this:
Person class:
#OneToMany(mappedBy = "person") // "person" here refers to the person property in the notes class
private Set<Note> notes;
Notes class:
#ManyToOne
#JoinColumn(name = "id_person", nullable = false)
private Person person;
Take a look at Hibernate Collection mapping for further information.
Edit Person
#OneToMany(fetch = FetchType.EAGER , mappedBy = "person")
private Set<Note> notes;
And Note
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "person_id", nullable = false)
private Person person;
I agree with you. One can implement what you're asking with only the two tables you already have.
Here is what I would do:
Database wise:
Table Person, with:
PK column: person_id
Table Note, with:
PK column: note_id
column: person_id (nullable)
column: assigned_person_id (nullable with unique index)
Purpose:
person_id - reference to the PK of the Person table. That will define the [Person : Note] relationship, where 1 person can have multiple notes.
assigned_person_id - reference to the person assigned to the note. However to guarantee your requirement that only one person can be assigned to a note - add an unique index by that column. That will guarantee that the same person_id was on used for another note record.
Hibernate wise:
Note that the code snippet is not complete! It is just pointing the important moments.
You can look at one of the many complete examples, for instance:
http://www.mkyong.com/hibernate/hibernate-one-to-many-relationship-example-annotation/
public class Person {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "person")
public setNotes(Set<Note> notes) {
this.notes=notes;
}
...
}
#Table(name = "note", catalog = "schema_name",
uniqueConstraints = #UniqueConstraint(columnNames = "assigned_person_id"))
public class Note {
private Person person;
private Person assignedPerson;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "person_id", nullable = false)
public Person getPerson() {
return person;
}
public Person getAssignedPerson() {
return assignedPerson;
}
...
}
I have two classes as following,
Human.java
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
public class Human implements Serializable {
private long id;
private String name;
....
}
Student.java
#Entity
#DynamicUpdate
public class Student extends MyFactories {
private List<Know> KnowList;
#OneToMany(fetch = FetchType.LAZY)
public List<Know> getKnowlist() {
return knowlist;
}
public void setKnowlist(List<Know> KnowList) {
return Knowlist;
}
}
Know.java
#Entity
public class Know implements Serializable {
private long id;
private Human hu;
private Student st;
....
#ManyToOne
public Person getHu() {
return hu;
}
#ManyToOne
public Client getSt() {
return st;
}
.... setters .....
}
Code
Know kw = new Know();
kw.setSt(studentObject);
kw.setHu(humanObject);
session.save(kw);
tx.commit();
I am able to insert into Know table but hibernate does not insert any record to student_know table which it has created.
I have found this answer but it says I need to use that method if I always want to retrieve all the records. Which I do not (at times, I may just need to retrieve the student class not list of its know)
System.out.println(this.student.getKnowList().size());
When I try to access the list it runs into following exception.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.myproject.Student.knowList, could not initialize proxy - no Session
for select case change that #OneToMany(fetch = FetchType.LAZY) to #OneToMany(fetch = FetchType.EAGER) so you can get data inside it's list.
and for the insert i need your clarification about where is your relation or getter setter of the private Factory fac;?
you should have at least something like this :
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "YOUR_FACTORY_ID_COLUMN")
private Factory fac;
public Factory getFac(){
return fac;
}
public void setFac(Factory fac){
this.fac=fac;
}
and did factory have any id?
You need to use session.Update(studentObject) as well, to insert a row into student_know table.
Please also be aware that access to a lazy association outside of the context of an open Hibernate session will result in an exception. Link