A framework for persisting data in Spring Spring Data JPA allows you to define relationships between two Entity classes. At that time, you can define a reference to the other object in each other's class, which is called a cross reference. There were various points that got stuck when using cross-references, so I will summarize them. The version I used is Spring Data JPA 1.10.6, and I am creating a project using Spring Boot 1.4.3.
Relationships between Entity classes can be specified using annotations such as "@OneToOne", "@OneToMany", "@ManyToOne", and "@ManyToMany". The relations expressed by "@OneToMany" and "@ManyToOne" seem to be the same, but you can specify how the Entity class on the other side is related to the Entity class.
Specifically, let's look at the relationship between employee and company data. The ER diagram is as follows.
If this is made into a cross-referenced Entity class, it will be as follows.
Company.java
@Entity
class Company {
@Id
private long id;
private String name;
@OneToMany //Many to 1
List<Employee> employees;
/*Accessor abbreviation*/
}
Employee.java
@Entity
class Employee {
@Id
private long id;
private String name;
@ManyToOne //Many
Company company;
/*Accessor abbreviation*/
}
Since it is a cross-reference, the company has a list of employee classes, which is not in the ER diagram. The company ID on the employee side becomes a reference to the company class when it is classified.
As shown in the ER diagram, the company ID (reference to company class) in the employee entity is treated as a foreign key. In a general RDBMS, if a foreign key is set, the target element must exist first. The same is true for JPA, where the company object must be registered first before the employee object can be registered in the DB.
@Autowired
EmployeeRepository empRepository;
@Autowired
CompanyRepository coRepository;
@PostConstruct
public init() {
/*Company registration*/
Company co = new Company();
co.setName("Company A");
coRepository.saveAndFlush(co);
/*Employee registration*/
Employee emp = new Employee();
emp.setName("Sato");
emp.setCompany(co); //Registered company settings required
empRepository.saveAndFlush(emp);
}
By the above process, we were able to register the company co
and the employee ʻempin the DB. However, when I take out
co from
coRepository and look at the contents of ʻemployees
, Mr. Sato's data is not stored.
In fact, even if you create a cross-referenced Entity, the information of the corresponding object is not automatically stored in the corresponding data structure.
Therefore, in order to refer to Mr. Sato's object from the object of company A, it is necessary to explicitly set the data and save it in the DB.
public init() {
/*Company registration*/
/*Employee registration*/
co.setEmployees(Arrays.asList(emp));
coRepository.saveAndFlush(co);
}
In order to register, the target employee object must be registered in the DB first. The reason this happens is that employee-to-company references have data in the employee table, while company-to-employee references manage referral relationships in a separate table. Because it is. That is, the two cross-reference entities are managed in three tables:
However, this method manages the information "company for employees" and "employee for company" separately.
Therefore, use the mappedBy
option provided by @OneToMany.
Employee.java
/*abridgement*/
@OneToMany(mappedBy="company") //Many to 1
List<Employee> employees;
/*abridgement*/
The value specified for mappedBy will be the "corresponding field variable name (with @ManyToOne)".
Compnay.java
/*abridgement*/
@ManyToOne //Many
Company company;
/*abridgement*/
This will prevent the reference management table from being created.
If there is no reference management table, the contents of ʻemployees are automatically created from the employee table. This eliminates the need to set a list of ʻEmployee
s with the setEmployees
method of Company
.
Cross-references are circular references because they both have object references to each other. When dealing with circular reference objects, you need to be aware of the possibility of expanding references indefinitely. When converting to a JavaScript object with RestController or Thyemelaf of Spring, each object is converted to json format. However, if you json an object with a circular reference, it will expand indefinitely. For example, if you expand the Employee object:
{"id":1, "name":"Sato", "company":{"id":1, "name":"Company A", "employees":[{"id":1, "name":"Sato", "company":{…}}]}}
This will eventually result in a StackOverflowError and the program will crash.
One way to avoid infinite expansion is, of course, to stop circular references (cross-references).
You can avoid infinite expansion by removing the company
field of the Employee class or the ʻemployeesfield of the Company class to eliminate circular references. However, cross-references are convenient, so you may want to avoid infinite expansion while keeping cross-references. To do this, we'll poke a hole in Spring's object expansion process. The object expansion process expands the target getter, that is, the method named
get ○○. Therefore, you can avoid infinite expansion by renaming the getters of cross-referenced objects. For example, rename the
getEmployee method of the Company class to ʻacquireEmployee
.
Then, the ʻacquireEmployee` method is not included in the expansion target of jsonization, so you can avoid infinite expansion.
Company.java
@Entity
class Company {
/*abridgement*/
/*Change to private*/
private getEmployees(){
return employees;
}
/*Call getter*/
public acquireEmpployees(){
return getEmployees();
}
}
In the above example, instead of simply renaming it, I made the original getter private and called it from the public ʻacquireEmployees`. Even methods with the get prefix do not expand private methods, avoiding circular references.
{"id":1, "name":"Sato", "company":[{"id":1, "name":"Company A"}]}
RestController and JavaScript objects will not be able to see Employees in company. However, it can be used in Java and Thymeleaf by calling the ʻacquireEmployees` method.
thymeleaf
<ul>
<li th:each="emp: ${company.acquireEmployees()}" th:text="${emp}"></li>
</ul>
As mentioned above, the jsonization process calls a method that is public and has get as a prefix to perform expansion.
Conversely, if the method is public and has get as a prefix, it will be included in the property of the object at the time of jsonization even if it does not have the information returned by it in the field.
For example, if you want the property to include the number of employees in your company, add the getEmployeeNumber
method.
Company.java
@Entity
class Company {
/*abridgement*/
private getEmployees(){
return employees;
}
public getEmployeeNumber(){
return getEmployees().size();
}
}
{"id":1, "name":"Sato", "company":[{"id":1, "name":"Company A", "employeeNumber":1}]}
By using this, even if you cannot include an object that causes a circular reference in json, you can return the representative value of the data. You can also completely return each data by defining an inner class that excludes references (although it's tedious).
StackOverFlow when cross-referenced Entity with @OneToMany is called in JavaScript using Thymeleaf JPA Relations: @OneToMany and @ManyToOne First JPA--Simple and easy to use, learn the basics of Java EE's data persistence feature
Recommended Posts