[JPA] Relación One to One basada en clave foránea

JPA es el API de persistencia más extendida y usada en la plataforma Java, creada para JEE. JPA es un Framework Object Relational Mapping (ORM), que permite interactuar con la base de datos por medio de objetos, de esta forma, JPA es el encargado de convertir los objetos Java en instrucciones para el Manejador de Base de Datos (MDB).

JPA está definido bajo el paquete javax.persistence. JPA en sí es una especificación que provee un grupo de interfaces que luego los ORM en particular implementan efectivamente y son ellos lo que finalmente terminan realizando el trabajo, entre lo más populares está Hibernate.

La especificación provee interfaces para la realización de las siguientes actividades:

  • Mapeo de entidades
  • Mapeo de tipos personalizados,(ej: AttributeConverter).
  • Lenguaje de consultas de nivel superior cercano a las entidades (JPQL).
  • Soporte para consultas nativas.
  • API Criteria.
  • Herramientas de validación.
  • Entre otras.

Relación One to One basada en clave foránea

Sin entrar en más detalles históricos, vamos al objetivo del artículo. Una de las tareas de un ORM es lograr mapear las relaciones entre clases a tablas de bases de datos. Para ello JPA provee en Spring JPA 3 anotaciones importantes @OneToOne, @OneToMany y @ManyToMany.

En esta entrada hablaremos de una de las variantes para la implementación de OneToOne, la idea es en próximos artículos hablar de otras variantes.

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.

Veamos un ejemplo en la siguiente relación

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 y una de las formas que tenemos para modelar efectivamente esta relación es usando el enfoque incluir la llave foránea de una de las entidades en la otra.

Veamos en código como sería:

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.Data;
@Data
@Entity
@Table(name = "user")
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;
	private String name;
	private int age;
	@OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "dni_id", referencedColumnName = "number")
    private Dni dni;
}
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.Data;
@Data
@Entity
@Table(name = "dni")
public class Dni {
	@Id
	private Long number;
	private String country;
	@Column(name = "issue_date")
	private LocalDate issueDate;
	@OneToOne(mappedBy = "dni")
	private User user;
}

Entendamos las definiciones de las entidades anteriores:

@Entity: Anotación que nos especifica que la clase definida es una entidad, y deber convertida a una instancia en la base de datos.
@Table(name = “user”): Anotación que define las propiedades con las que se registrará la tabla asociada a la clase Java en la base de datos.

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

En el código anterior indicamos la llave primaria de la tabla asociada a la clase e indicamos la estrategia que se va a seguir para generar ese valor con la anotación @GeneratedValue. Sino se especifica como es el caso de la clase Dni indica que la clave se establecerá manualmente.

En el siguiente fragmento presenta en la clase User, establecemos la relación con la clase DNI, la definición del objeto a nivel de estructura muestra una relación de composición/agregación. Lo fundamental radica en la anotación @JoinColumn(name = “dni_id”, referencedColumnName = “number”), la misma esta diciendo que vamos a crear una llave foránea (FK), llamada dni_id en la tabla user, la cual será una referencia a la llave primaria de la tabla dni, llamada en este caso number. Todo lo importante pasa en estas líneas de código.

@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "dni_id", referencedColumnName = "number")
private Dni dni;

A continuación se indica simplemente que será una relación bidireccional indicando en la clase Dni el objeto User correspondiente, además esto será mapeado a través del objeto dni que se agregó a la clase User

@OneToOne(mappedBy = "dni")
private User user;

Se concluye la definición del ejemplo con las siguientes clases, que no aportan nada en si, son solo para probar lo programado.

El repositorio que extiende de CrudRepository para poder realizar operaciones sobre User, que es la entidad primaria.

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

Un controlador REST que nos permitirá probar ese ejemplo creando usuarios.

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

Se agrega además la configuración del proyecto y el archivo de dependencias.

spring.datasource.url=jdbc:mysql://localhost/jpao2o1
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=update
<?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>

Para realizar pruebas, puede usar la siguiente línea CURL

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

Espero te haya gustado el artículo y hayas entendido como funciona la relación @OneToOne basada en FK. Nos vemos en la próxima entrada.

Nota importante: En MySQL por defecto algunas instalaciones traen como motor MyIsam como predeterminado. Este motor NO SOPORTA FK, el proyecto funcionará igual, pero se recomienda cambiar las tablas a InnoDB.

1 thought on “[JPA] Relación One to One basada en clave foránea

Comments are closed.