Spring Data JPA Entity cross-reference and its notes

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.

Cross-reference between Entity classes

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.

image

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.

DB registration order

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);
}

Data storage

By the above process, we were able to register the company co and the employee ʻempin the DB. However, when I take outco 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:

image

Referenced by mappedBy

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 ʻEmployees with the setEmployees method of Company.

Circular reference

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.

Avoid infinite expansion of circular references

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 namedget ○○. 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>

Bonus: About the development of jsonization

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).

Reference page

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

Spring Data JPA Entity cross-reference and its notes
Until the use of Spring Data and JPA Part 2
Until the use of Spring Data and JPA Part 1
[How to install Spring Data Jpa]
Spring Data JPA SQL log output
See the behavior of entity update with Spring Boot + Spring Data JPA
Creating REST APIs with Spring JPA Data with REST and Lombok incredibly easy.
OR search with Spring Data Jpa Specification
Spring with Kotorin --2 RestController and Data Class
Exists using Specification in Spring Data JPA
Implementation method for multi-data source with Spring boot (Mybatis and Spring Data JPA)
Spring Data JPA save select-insert is only insert
Sort by Spring Data JPA (with compound key sort)
Creating a common repository with Spring Data JPA
Spring Boot + Spring Data JPA About multiple table joins
Check the behavior of getOne, findById, and query methods in Spring Boot + Spring Data JPA
Testing JPA entities and repositories using Spring Boot @DataJpaTest
A memorandum when trying Spring Data JPA with STS
I tried to get started with Spring Data JPA
Make the where clause variable in Spring Data JPA
[Spring Data JPA] Can And condition be used in the automatically implemented method of delete?