In this post, we are going to show an example of a OneToMany relationship in Hibernate, where we will see how we can map this type of relationship using JPA.
In a previous article, we have already seen some options with Hibernate, such as soft delete and inheritance, both of which are very important features for simplifying coding and maintaining cleanliness in our applications.
How does a One-to-Many Relationship work?
For example, a bank has many bank accounts belonging to different customers. In this case, we can say that we have a 1..N relationship. That means the one-to-many relationship signifies that a column in one database table is mapped to multiple columns in another table. Let’s see it visually:
OneToMany Relationship in Hibernate
The above relationship reflects a 1..N relationship, where a bank has 1 to N bank accounts. In the Bank Account table, we will have a column that references the bank through a foreign key.
When transforming our relational model to Java, we will add an object of the main class in our secondary class. In other words, in our BankAccount class, we will add an object of the Bank class using the @ManyToOne annotation. This annotation allows us to map the column with the foreign key in our table, so the other entity will have an object reference to its main entity. This is generally the most efficient way to map an association.
On the other hand, we also have the @OneToMany annotation provided by JPA as a dirty checking mechanism, so when applied, we will have a collection of objects in the main class.
Depending on the needs of our application or project, we can apply one of the following approaches:
- Unidirectional relationship with @OneToMany
- Bidirectional relationship with @OneToMany
Bidirectional Relationship in @OneToMany
In this section, we will see how to define our entities to work bidirectionally using the two annotations we saw earlier, @OneToMany and @ManyToOne.
This type of relationship is the best approach when we want to create a OneToMany relationship and we need a collection of child objects in the parent.
Let’s see an example:
@Getter @Setter @Entity @NoArgsConstructor public class Bank { @Id private Long id; private String name; @OneToMany(mappedBy="bank") private Set<BankAccount> bankAccounts; public void addBankAccount(BankAccount bankAccount) { bankAccounts.add(bankAccount); bankAccount.setBank(this); } public void removeComment(BankAccount bankAccount) { bankAccounts.remove(bankAccount); bankAccount.setBank(null); } }
@Getter @Setter @Entity @NoArgsConstructor public class BankAccount { private Long id; private Long user; private BigDecimal amount; private LocalDate date; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="bank_id", nullable=false) private Bank bank; }
As we can see, we have used the mentioned annotations to maintain a bidirectional relationship, and we have also added some additional details that we will explore:
- Use of Lazy: In the @ManyToOne relationship, we have used lazy loading to avoid eager fetching and fetching everything, which could affect the performance of our application.
- Bidirectional association: To keep both sides synchronized, we need to add and remove elements from our bankAccounts collection to maintain a stable relationship. That’s why we use the addBankAccount and removeBankAccount methods.
- Use of Cascade ALL: We make use of the cascade feature with the ALL option to propagate operations in any situation.
Let’s execute the following test to see the result of inserting a bank with bankAccounts:
@SpringBootTest public class BankAccountRepositoryTest { @Autowired private BankAccountRepository bankAccountRepository; @Autowired private BankRepository bankRepository; @Test public void when_save_new_bank_with_multiples_accounts_then_bank_is_saved_correctly() { Bank bank = new Bank(); bank.setName("TGB"); var bankAccount = new BankAccount(); bank.addBankAccount(bankAccount); bankRepository.save(bank); } }
When executing the test, we can observe that two inserts will be performed. The first one is to create a new row in the “bank” table, and the second one is to create a “bankAccount” associated with the bank.
Hibernate: insert into Bank (name, id) values (?, ?) Hibernate: insert into BankAccount (amount, bank_id, user, id) values (?, ?, ?, ?)
As we mentioned before, this approach is the best when we want the parent object to have a collection of child objects.
Unidirectional Relationship in @OneToMany with Three Tables
For a unidirectional relationship with @OneToMany, we will remove the @OneToMany annotation in order to remove the parent object in the child class. Let’s see the example based on the previously created entities:
@Getter @Setter @Entity @NoArgsConstructor public class Bank { @Id private Long id; private String name; @OneToMany(mappedBy="bank") private Set<BankAccount> bankAccounts; public void addBankAccount(BankAccount bankAccount) { bankAccounts.add(bankAccount); bankAccount.setBank(this); } public void removeComment(BankAccount bankAccount) { bankAccounts.remove(bankAccount); bankAccount.setBank(null); } }
@Getter @Setter @Entity @NoArgsConstructor public class BankAccount { private Long id; private Long user; private BigDecimal amount; private LocalDate date; }
To create the unidirectional relationship, what we have done is remove the mapping that would be done in the BankAccount table, thus losing the foreign key.
Let’s see with a test how the result of this execution would look like.
@SpringBootTest public class BankRepositoryTest { @Autowired private BankRepository bankRepository; @Test public void when_save_new_bank_with_multiples_accounts_then_bank_is_saved_correctly() { Bank bank = new Bank(); bank.setName("TGB"); var bankAccount = new BankAccount(); bank.getBankAccounts().add(bankAccount); bankRepository.save(bank); } }
In the previous test, we only created one bank and one bankAccount associated with the bank. Let’s see the result of the execution:
Hibernate: insert into Bank (name, id) values (?, ?) Hibernate: insert into BankAccount (amount, user, id) values (?, ?, ?) Hibernate: insert into Bank_bankAccounts (Bank_id, bankAccounts_id) values (?, ?)
Upon analyzing the result of the execution, we can see that three different inserts have been made: one in the “Bank” table, another in the “BankAccount” table, and a third one in the “Bank_bankAccounts” table. As you can see, this table does not belong to our original model. It is a new table that has been formed with the keys from the “Bank” and “BankAccount” tables.
When creating this type of unidirectional relationship, an intermediate table is created, resulting in a representation of an N..M relationship, a many-to-many relationship.
The problem with this approach is the use of more resources and poorer performance when performing any query or insertion.
To avoid this issue, we can add the @JoinColumn annotation, which will eliminate the created intermediate table. Let’s see how it would look like:
Unidirectional Relationship in @OneToMany with Two Tables
In the previous section, we saw how to create a unidirectional relationship using the @OneToMany annotation, but we encountered the problem of creating an additional table. The creation of an additional table ultimately leads to poorer performance in our applications. To solve this problem, we will use the @JoinColumn annotation.
The @JoinColumn annotation helps us indicate that we want a foreign key in the child table that defines the relationship.
@Getter @Setter @Entity @NoArgsConstructor public class Bank { @Id @GeneratedValue private Long id; private String name; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "bank_id") private Set<BankAccount> bankAccounts = new HashSet<>(); }
Let’s execute a test to see the result of adding @JoinColumn.
Hibernate: insert into Bank (name, id) values (?, ?) Hibernate: insert into BankAccount (amount, user, id) values (?, ?, ?) Hibernate: update BankAccount set bank_id=? where id=?
We can see that by adding the @JoinColumn annotation, we no longer have an intermediate table. However, the performance is not completely optimal as we first perform inserts and then update the foreign key.
The same process would occur for deletion as well.
Example of OneToMany Relationship in Hibernate in a Bidirectional way
Next, we will define an example of a OneToMany relationship in Hibernate using Spring Boot. This example will be based on a bidirectional relationship, which is likely the most optimal approach. To do this, we will start by creating the project using the Initializr page.
In the following sections, we will gradually create the necessary layers of our application.
The dependencies we will select are Spring Web, Spring Data, Lombok, and H2.
- Spring Data includes all the persistence part, incorporating Hibernate + JPA.
- Lombok will help us reduce the boilerplate code.
- H2 is an in-memory database perfect for testing, not recommended for production environments.
To see the complete example, where you can see both the unidirectional and bidirectional relationships, click here.
Maven Dependencies.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
H2 Database Configuration
Next, we will add the necessary configuration to use an in-memory H2 database. It is important to note that in order to access the H2 console, it needs to be enabled using the property h2.console.enabled.
spring: application: name: one-to-many datasource: url: jdbc:h2:mem:testdb driverClassName: org.h2.Driver username: sa password: password initialize: true initialization-mode: always hikari: connection-timeout: 6000 initialization-fail-timeout: 0 jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl properties: hibernate: show_sql: true format_sql: true enable_lazy_load_no_trans: true h2: console: enabled: true
In addition, we are going to perform data import by adding a file named import.sql in our resources folder.
INSERT INTO BANKACCOUNT VALUES (1,1, 1.20,1); INSERT INTO BANK VALUES (1,'TBC');
Entity Creation and OneToMany Relationship
Let’s create two classes, Bank and BankAccount, with a bidirectional relationship between them. To do this, we will use the @Entity annotation to indicate that both classes are entities. And through the @OneToMany and @ManyToOne annotations, we will express a bidirectional relationship between the two classes.
@Getter @Setter @Entity @NoArgsConstructor public class BankAccountBidirectional { @Id @GeneratedValue private Long id; private Long user; private BigDecimal amount; @ManyToOne private BankBidirectional bank; }
@Getter @Setter @Entity @NoArgsConstructor public class BankBidirectional { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "bank", cascade = CascadeType.ALL, orphanRemoval = true) private Set<BankAccountBidirectional> bankAccounts; public void addBankAccount(BankAccountBidirectional bankAccount) { if (null == bankAccounts) { bankAccounts = new HashSet<>(); } bankAccounts.add(bankAccount); bankAccount.setBank(this); } public void removeComment(BankAccountBidirectional bankAccount) { bankAccounts.remove(bankAccount); bankAccount.setBank(null); } }
As mentioned earlier when explaining the bidirectional relationship, a foreign key will be created in the BankAccount child table in our database. Additionally, to maintain the integrity of our database, we will add the remove and add methods.
Repository Creation
To create a repository for Bank, we will use JpaRepository provided by Spring Data. This class will allow us to use all the necessary methods and actions to perform operations in our database.
public interface BankBidirectionalRepository extends JpaRepository<BankBidirectional, Long> { }
Bank Service Creation
We are going to create a service layer that will act as a pass-through between the controller and the repository. In other words, the controller will make a call to the service, and the service will then call the repository to operate with the database.
In the service, we will perform dependency injection of BankAccountBidirectionalRepository.
@RequiredArgsConstructor @Service public class BankAccountService { private final BankAccountBidirectionalRepository bankAccountRepository; public BankAccountBidirectional findById(Long id) { return bankAccountRepository.findById(id).orElseThrow(); } public List<BankAccountBidirectional> findAll() { return bankAccountRepository.findAll(); } }
Controller Creation in a OneToMany Application
We are going to create our controller class to access our application. The controller will have two endpoints, one to retrieve all accounts and another to retrieve accounts by their ID.
@RestController @RequiredArgsConstructor @RequestMapping("/banks") public class BankAccountController { private final BankAccountService bankAccountService; @GetMapping public ResponseEntity<List<BankAccountBidirectional>> getBankAccounts() { var bankAccounts = bankAccountService.findAll(); return new ResponseEntity<>(bankAccounts, HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity<BankAccountBidirectional> getBankAccountById(@PathVariable Long id) { var bankAccount = bankAccountService.findById(id); return new ResponseEntity<>(bankAccount, HttpStatus.OK); } }
Conclusion
In this example of OneToMany relationship in Hibernate, we have seen the different ways we can create this type of relationship.
On one hand, we have explored the two forms of using the unidirectional relationship, which can have an impact on the performance of our application. On the other hand, we have seen the bidirectional approach, which is the most commonly used, frequently encountered in applications, and provides better performance.
You can find the complete example here.
If you need more information, you can leave us a comment or send an email to refactorizando.web@gmail.com You can also contact us through our social media channels on Facebook or twitter and we will be happy to assist you!!