Ejemplo de relación OneToMany en Hibernate
En esta entrada vamos a mostrar un ejemplo de relación OneToMany en hibernate, en la cual veremos como podemos mapear este tipo de relaciones haciendo uso de JPA.
En algún artículo anterior ya hemos visto algunas opciones con Hibernate como los soft delete y las herencias, ambas características muy importantes para simplificar la codificación y limpieza de nuestras aplicaciones.
¿Cómo funciona una Relación One To Many?
Por ejemplo un banco tiene muchas cuentas bancarias de diferentes clientes. En este caso podríamos decir que tenemos una relación 1..N. Es decir la relación one-to-many significa que una columna en una tabla de base de datos es mapeada a múltiples columnas en otra tabla. Vamos a verlo de manera gráfica:
La relación anterior refleja una relación 1 .. N, un banco tiene de 1 a N cuentas bancarias. En la tabla Bank Account tendremos una columna que hará referencia al banco, mediante una clave ajena.
Al transformar nuestro modelo relacional a Java, añadiremos un objeto de la clase principal en nuestra clase secundaria, es decir, en nuestra clase BankAccount vamos a añadir un objeto de bank haciendo uso de @ManyToOne. Esta anotación nos va a permitir mapear la columna con la clave ajena en nuestra tabla, y así la otra entidad tendrá una referencia de objeto a su entidad principal. Esta es por lo general la forma más eficiente de mapear una asociación.
Por otro lado, también tenemos la anotación @OneToMany que es ofrecida por JPA como un mecanismo dirty checking (de verificación sucia), por lo que al aplicarse tendremos una colección de objetos en la clase principal.
Dependiendo de las necesidades de nuestra aplicación o proyecto, podremos aplicar alguna de las dos siguientes aproximaciones:
- relación unidireccional con @OneToMany
- relación bidireccional con @OneToMany
Relación bidirectional en @OneToMany
En este apartado vamos a ver como definir nuestras entidades para que funcionen de manera bidireccional haciendo uso de las dos anotaciones que vimos anteriormente, @OneToMany y @ManyToOne.
Este tipo de relación es la mejor aproximación cuando queremos crear una relación OneToMany y necesitamos una colección de objetos hijos en el padre.
Veamos el ejemplo:
@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; }
Como podemos ver hemos hecho uso de las dos anotaciones comentadas para mantener una relación bidireccional y además hemos añadido alguna particularidad más que vamos a ver:
- Uso de Lazy: En la relación @ManyToOne hemos hecho uso de lazy para evitar una carga activa y traer todo, lo que podría afectar en el rendimiento de nuestra aplicación.
- Asociación bidireccional: Para poder tener sincronizadas ambos lados necesitamos añadir y eliminar elementos de nuestra colección de bankAccounts para poder tener una relación estable. Por eso hacemos uso de los métodos addBankAccount y removeBankAccount.
- Uso de Cascade ALL: Hacemos uso de la característica de cascada en modo ALL para que se propague ante cualquier operación.
Vamos a ejecutar el siguiente test para ver el resultado de una insercción de bank con 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); } }
Al ejecutar el test podemos ver como se van a realizar dos inserts el primero para crear una nueva fila en bank y el segundo para crear un bankAccount asociado al bank.
Hibernate: insert into Bank (name, id) values (?, ?) Hibernate: insert into BankAccount (amount, bank_id, user, id) values (?, ?, ?, ?)
Por lo que como hemos comentado anteriormente, esta aproximación es la mejor cuando queremos que el objeto padre tenga una colección de objetos hijos.
Relación unidireccional en @OneToMany con tres tablas
Para una relación unidireccional con @OneToMany vamos a eliminar la @OneToMany de modo que eliminamos el objeto padre en la clase hija. Veamos el ejemplo partiendo de las entidades creadas anteriormente:
@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; }
Para crear la relación unidireccional lo que hemos hecho ha sido eliminar el mapeo que se haría en la tabla de BankAccount, de manera que perdemos la clave ajena.
Vamos a ver con un test como sería el resultado de esta ejecución.
@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); } }
En el test anterior únicamente creamos un bank y una bankAccount asociada al bank. Vamos a ver el resultado de la ejecución:
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 (?, ?)
Al analizar el resultado de la ejecución vemos que se han hecho tres diferentes inserts, uno en Bank otro en BankAccount y uno último en Bank_bankAccounts. Como puedes ver esa tabla no pertenece a nuestro modelo, es una tabla nueva que se ha formado con las claves de bank y de bankAccount.
Al hacer este tipo de relaciones unidireccional, se crea una tabla intermedia de modo que la representación sería una relación N..M, una relación de muchos a muchos.
El problema de esta aproximación es el uso de más recursos y un peor rendimiento para realizar cualquier query o insercción.
Para evitar este problema, podemos añadir la anotación @JoinColumn, con lo que eliminaríamos la tabla intermedia creada, vamos a ver como quedaría.
Relación unidireccional en @OneToMany con dos tablas
En el anterior apartado hemos visto como crear una relación unidireccional con la anotacioń @OneToMany, pero nos hemos encontrado el problema de la creación de una tabla adicional. La creación de una tabla adicional se transforma al final en un peor rendimiento de nuestras aplicaciones, por lo que para solventar ese problema vamos a hacer uso de la anotaicón @JoinColumn.
La anotación @JoinColumn nos ayudar a indicar que queremos una foreign key en la tabla hija con la que se define la relación.
@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<>(); }
Vamos a ejecutar un test para ver el resultado de añadir @JoinColumn.
Hibernate: insert into Bank (name, id) values (?, ?) Hibernate: insert into BankAccount (amount, user, id) values (?, ?, ?) Hibernate: update BankAccount set bank_id=? where id=?
Podemos ver como al añadir la anotación @JoinColumn, ya no tenemos una tabla intermedia pero el rendimiento no es del todo óptimo ya que primero hacemos inserts y a continuación actualizamos la foreign_key.
El mismo proceso ocurriría para el borrado.
Ejemplo Spring Boot de relación OneToMany de manera Bidireccional
A continuación vamos a definir un ejemplo de relación OneToMany en Hibernate haciendo uso de Spring Boot. Este ejemplo se basará en la relación bidireccional, ya que que probablemente es la más óptima. Para ello vamos a empezar creando el proyecto através de la página initializr.
En los siguientes apartados vamos a ir creando las capas necesarias de nuestra aplicación paso a paso.
Las dependencias que vamos a seleccionar serán Spring Web, Spring Data, Lombok y H2.
- Spring Data lleva incorporado toda la parte de la persistencia por lo que nos incopora Hibernate + JPA.
- Lombok nos ayudará a reducir el boilerplate de nuestro código.
- H2, es una base de datos en memoria perfecta para realizar pruebas, no utilizar en entornos productivos.
Para ver el ejemplo completo en el que podrás ver tanto la relación unidireccional como bidireccional pulsa aquí.
Dependencias Maven
<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>
Configuración de Base de Datos H2
A continuación vamos a añadir la configuración necesaria para hacer uso de una Base de Datos en memoria H2. Es importante tener en cuenta que para poder acceder a la consola de H2, esta tiene que ser activada mediante la propiedad 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
Además vamos a hacer una importanción de datos, para ello añadimos un fichero llamado import.sql en nuestra carpeta resources.
INSERT INTO BANKACCOUNT VALUES (1,1, 1.20,1); INSERT INTO BANK VALUES (1,'TBC');
Creación de entidades y relación OneToMany
Vamos a crear las dos clases bank y BankAccount con una relación bidireccional entre ellas. Para ello vamos a hacer uso de @Entity para indicar que las dos clases van a ser Entidades. Y mediante la anotación @OneToMany y @ManyToOne vamos a expresar una relacción bidireccional entre ambas clases:
@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); } }
Como comentamos anteriormente al explicar la relación bidireccional, en nuestra base de datos se creará la foreign-key en la tabla hija de bankAccount. Además para mantener la integridad de nuestra base de datos vamos a añadir los métodos remove y add.
Creación de repositorio
Para la creación de un repositorio para bank vamos a hacer uso de JpaRepository, que es proporcionado por spring data. Esta clase nos va a permitir hacer uso de todos los métodos y acciones necesarias para hacer operaciones en nuestra base de datos.
public interface BankBidirectionalRepository extends JpaRepository<BankBidirectional, Long> { }
Creación de servicio Bank
Vamos a crear una capa service que hará de pass through, entre el controlador y el repositorio. Es decir, el controlador hará una llamada al servicio y esté llamará al repositorio para operar con la Base de Datos.
En el servicio haremos la inyección de dependencia de 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(); } }
Creación controlador en una aplicación OneToMany
Vamos a crear nuestra clase controladora para poder acceder a nuestra aplicación. El controller tendrá dos endpoints, uno para obtener todas las cuentas, y otro para obtenerlas por 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); } }
Conclusión
En este ejemplo de relación OneToMany en Hibernate hemos podido ver las diferentes maneras que tenemos de crear este tipo de relación.
Por un lado hemos visto las dos formas de uso de la relación unidireccional, las cuales tendrán un impacto en el rendimiento de nuestra aplicación. Y por otro lado hemos visto la manera bidireccional, la cual es la más típica de usar, la que más nos encontraremos en las aplicaciones y la que mejor rendimiento nos dará.
El ejemplo completo lo puedes ver aquí.
Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!
1 pensamiento sobre “Ejemplo de relación OneToMany en Hibernate”