[JPA] Relación One to One basada en clave compartida

En la entrada anterior comenzamos a hablar de JPA, dimos una introducción y comenzamos a hablar sobre la relación de tipo 1 a 1. La vez anterior vimos como implementar esta relación usando el enfoque de clave foránea. En esta entrada veremos la variante de llave compartida.

Una relación de uno a uno indica que una entidad solo puede relacionarse con un elemento de la otra entidad y viceversa, es una relación no siempre muy presente en la vida real, pero en ocasiones requerida por los sistemas para representar casos en los que aplica.

Supongamos, como en el artículo anterior la relación siguiente:

Relación 1:1 entre usuario y dni

En el caso anterior un User solo puede tener un DNI y un DNI solo puede pertenecer a un User (en la práctica no siempre sucede).

Para modelar a nivel de ORM la relación anterior en Spring JPA tenemos la anotación @OneToOne. Además del uso de llave foránea otra de las formas que tenemos para modelar la relación es usando la clave compartida.

En este enfoque de clave compartida por la general la “entidad fuerte o principal” de la relación 1 a 1 cede su clave primaria para que sea clave única de la otra entidad, compartiendo ambas entidades una misma clave. De esta forma al ser la PK de la entidad User la PK de DNI se asegura la relación 1 a 1.

Veamos esto en código como se implementa. Para ello hemos desarrollado un proyecto Spring Boot con JPA, pondremos las clases que conforman el ejemplo y explicaremos solo las relaciones que se detallan a nivel de entidad que son en realidad donde está la parte interesante.

Las dependencias de nuestro ejemplo, archivo pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.yoandyv.jpa</groupId>
	<artifactId>jpa-one-to-one</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>jpa-one-to-one</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<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>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

De arriba resaltar solamente que hemos importado spring-boot-starter-data-jpa, este starter nos trae -entre otras dependencias- todo el ORM Hibernate que necesitamos.

El fichero de configuración application.properties

spring.datasource.url=jdbc:mysql://localhost/jpao2o2
spring.datasource.username=root
spring.datasource.password=12qwaszx
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop

El controlador REST que forma parte del ejemplo:

package com.yoandyv.jpa.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.yoandyv.jpa.entities.User;
import com.yoandyv.jpa.repository.UserRepository;
@RestController
@RequestMapping("users")
public class UserController {
	private final UserRepository userRepository;
	@Autowired
	public UserController(UserRepository userRepository) {
		this.userRepository = userRepository;
	}
	@PostMapping
	public User save(@RequestBody User user) {
		return this.userRepository.save(user);
	}
}

El Repository para aprovechar las bondades de Spring Data.

package com.yoandyv.jpa.repository;
import org.springframework.data.repository.CrudRepository;
import com.yoandyv.jpa.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
}

Y ahora la parte importante, las entidades User y Dni que es donde está la parte buena y útil.

@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "user")
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	private int age;
	@OneToOne(mappedBy = "user", cascade = CascadeType.PERSIST)
    @PrimaryKeyJoinColumn
    private Dni dni;
	public void setDni(Dni dni) {
        this.dni = dni;
        dni.setUser(this);
    }
}
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "dni")
public class Dni {
	@Id
	private Long id;
	private Long number;
	private String country;
	@Column(name = "issue_date")
	private LocalDate issueDate;
	@JsonIgnore
	@OneToOne
    @MapsId
	private User user;
	public Dni(User user) {
        this.user = user;
        user.setDni(this);
    }
}

Cosas importantes de las dos clases de entidades:

En la clase User > @OneToOne(mappedBy = “user”, cascade = CascadeType.PERSIST), el atributo de la anotación mappedBy indica que este campo se enlaza con un campo de nombre “user” presente en la clase Dni. Por su parte @PrimaryKeyJoinColumn indica que llave primaria de la tabla actual será usada como elemento de unión con la tabla de la clase anotada (en este caso Dni), pasando a dicha clase, esto es importante.

En la clase Dni > @MapsId en este contexto en particular nos indica que la clave primaria de la clase anotada de usará como @Id de la clase actual.

A nivel de base de datos, cuando creamos objetos para este modelo, los datos quedan así:

Tabla user
Tabla dni

Cómo se puede apreciar la PK de la tabla user, pasó a ser PK de la tabla dni, si nos fijamos a nivel de estructura de la tabla dni, vemos lo siguiente:

Estructura final de la tabla dni

Finalmente, si te animas a levantar el ejemplo y probar te dejo acá la pegada con curl para que la importes a Postman o pruebes directamente la API.

curl --location --request POST 'http://localhost:8080/users' \
--header 'Content-Type: application/json' \
--data-raw '{
   "name":"Pepe",
   "age":39,
   "dni": {
       "number": 1276346656776,
       "country":"XXXX",
       "issueDate": "2021-04-11"
   }
}'

Hasta acá la entrada de hoy, espero te sea útil y aprendas un poco más sobre la relación de 1 a 1 usando JPA .

Si aún no te has suscrito a nuestro canal de Youtube puedes hacerlo, estamos subiendo videos nuevos todas las semanas.

Nos vemos 🙂

1 thought on “[JPA] Relación One to One basada en clave compartida

Comments are closed.