Ejemplo de NaturalId de Hibernate con Spring Boot
En la mayoría de los casos, nuestros objetos de base de datos tendrán un identificador natural, por ejemplo, tu número de identificación, el ISBN de un libro, el número de una cuenta bancaria. Este identificador podría ser considerado como clave primaria (PK), pero por lo lo general, es más común, generar números o secuencias para un Id. Es por ese motivo que vamos a ver un ejemplo de NaturalId de Hibernate con Spring Boot.
¿Qué es un NaturalId?
Un identificador NaturalId es aquel que representa un objeto de Base de Datos o entidad con un identificador natural.
Un identificador natural (NaturalId) no tiene que porque tener correlación con el identificador de la base de datos, es más, es poco probable, aunque a veces se use como clave primaria. Esta funcionalidad nos la proporciona Hibernate, ya que nos va a permitir tratar nuestro NaturalId como nuestro identificador principal.
Por ejemplo, como hemos visto antes un NaturalId podría ser el ISBN de un libro, tu número de cuenta, o la matrícula de un coche, que pueden ser identificadores pero no tienen que porque ser el ID de Base de Datos.
Para ello vamos a ver un ejemplo en el que poder ver esta funcionalidad que nos proporciona Hibernate.
Hands On con NaturalId en Hibernate
A continuación vamos a desarrollar un ejemplo con Spring Boot e Hibernate donde vamos a poder ver el tratamiento con NaturalId.
Vamos a crear un ejemplo sobre una librería. Los libros que tiene nuestra librería están guardado en una base de datos por un Id secuencial, y por su identificador de libro (ISBN). Las búsquedas que se van a hacer es por ISBN, por lo que favorecería el rendimiento que fuera PK, pero la PK es long secuencial, por lo que vamos a ver como crearíamos un NaturalId para ISBN.
Para descargarte el ejemplo completo lo puedes hacer aquí.
Generación del proyecto
El primer paso es la generación de nuestro proyecto, para ello vamos a ir a la página de Spring Initialzr, y a crear nuestro proyecto. Hay que tener en cuenta que debemos añadir el starter de Spring Data y si queremos una base de datos en memoria podemos marcar H2.
Nuestro pom quedaría de la siguiente manera:
<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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Creación de una entidad con NaturalId en Hibernate
A continuación vamos a generar una entidad de libro haciendo uso de la anotaciones típicas y además haremos uso de la anotación NaturalId, que irá junto con ISBN.
@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = “id”, updatable = false, nullable = false) private Long id; @NaturalId private String isbn; … }
El Id de nuestra entidad será gestionado de manera automática y secuencial con cada insercción, pero además tendremos un identificador anotado con @NaturalId el cual vendrá informado siempre.
Por defecto NaturalId es inmutable, por lo que no va a poder llevar un método setter, pero en el caso en el que necesitemos hacer uso de un objeto mutable podemos anotar @NaturalId con valor true.
Si quieres añadir más campos o entidades al ejemplo puedes ver el tratamiento de Hibernate con las relaciones ManyToMany o OneToMany.
Creación de interfaz para buscar por NaturalId
Hibernate nos proporciona dos diferentes métodos para leer una entidad de base de datos por su identificador natural, estos métodos son byNaturalId y bySimpleNaturalId.
A continuación nos definiremos una interfaz en la que añadiremos ambos métodos de búsqueda que extenderá de JpaRepository. Esta inferfaz nos permitirá ser capaces de leer cualquier objeto por su NaturalId.
@NoRepositoryBean public interface BookRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { // use this method when your entity has a single field annotated with @NaturalId Optional<T> findBySimpleNaturalId(ID naturalId); // use this method when your entity has more than one field annotated with @NaturalId Optional<T> findByNaturalId(Map<String, Object> naturalIds); }
Creación de implementación para búsqueda por NaturalId
A continuación implementaremos la interfaz anterior que hemos creado y además añadiremos funcionalidad extendiendo de SimpleJpaRepository.
A continuación vamos a definir un entityManager, a través del cual podremos hacer búsquedas por NaturalId. Una vez hemos conseguido hacer la búsqueda por NaturalId haremos uso del método Load para hacer la carga de la entidad.
@Transactional(readOnly = true) public class NaturalRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements NaturalRepository<T, ID> { private final EntityManager entityManager; public NaturalRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; } @Override public Optional<T> findBySimpleNaturalId(ID naturalId) { Optional<T> entity = entityManager.unwrap(Session.class) .bySimpleNaturalId(this.getDomainClass()) .loadOptional(naturalId); return entity; } @Override public Optional<T> findByNaturalId(Map<String, Object> naturalIds) { NaturalIdLoadAccess<T> loadAccess = entityManager.unwrap(Session.class).byNaturalId(this.getDomainClass()); naturalIds.forEach(loadAccess::using); return loadAccess.loadOptional(); } }
Una vez hemos generado nuestras interfaces para poder realizar las búsquedas por NaturalId, es momento de ver su uso. Para ello, partiendo de la entidad Book generada anteriormente vamos a hacer búsquedas por su NaturalId, por lo que vamos a crear un repositorio para book.
Creación repositorio para un objeto para búsqueda por NaturalId
Anteriormente hemos creado las clases necesarias para dotar a nuestra aplicación de búsquedas por NaturalId. Por lo que para poder hacer uso de esta funcionalidad para nuestra entidad Book, y poder hacer la búsqueda por su NaturalId necesitamos que extienda del repositorio NaturalRepository, creado anteriormente.
@Repository public interface BookRepository<T, ID> extends NaturalRepository<Book, Long> { }
Este repositorio va a heredar los dos métodos para buscar por NaturalId y además todos los métodos que nos proporciona JPARepository.
Creación de controller en búsqueda por NaturalId
A continuación vamos a crear un controlador en el que habilitaremos dos endpoints que nos permitirán buscar por simpleNaturalId y a poder guardar un objeto book.
@RequestMapping("/api") @RequiredArgsConstructor @RestController public class BookController { private final BookRepository bookRepository; @GetMapping("/books/{naturalId}") public ResponseEntity<Book> getBookByNaturalId(@PathVariable String naturalId) { return ResponseEntity.ok((Book)bookRepository.findBySimpleNaturalId(naturalId).orElseThrow()); } @PostMapping("/books") public ResponseEntity<Book> saveBook(@RequestBody Book book) { var save = bookRepository.save(book); return new ResponseEntity<>(HttpStatus.CREATED); } }
Creación de Test para comprobar búsqueda por NaturalId
Y finalmente para poder testar nuestros endpoints hemos generado un test haciendo uso de SpringBootTest y MockMvc para verificar que nuestros test funciona correctamente.
@ExtendWith(SpringExtension.class) @SpringBootTest @AutoConfigureMockMvc public class BookControllerIT { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test void finbBookByNaturalId() throws Exception { var book = createBook(); mockMvc.perform( MockMvcRequestBuilders.post("/api/books") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(book))) .andExpect(status().isCreated()); var findByNaturalId = mockMvc.perform( get("/api/books/1A2s-3f").accept(MimeTypeUtils.APPLICATION_JSON_VALUE)) .andExpect(status().isOk()) .andReturn(); var b = objectMapper.readValue(findByNaturalId.getResponse().getContentAsString(), Book.class); assert b.getIsbn() == "1A2s-3f"; } private Book createBook() { Book book = new Book(); book.setIsbn("1A2s-3f"); book.setTitle("The Count of Monte Cristo"); book.setPrice(34); return book; } }
Búsqueda en Hibernate por NaturalId
Una vez hemos probado nuestra aplicación, por ejemplo lo podemos hacer con los test. Vemos como Hibernate efectúa la búsqueda de nuestros objectos por NaturalId en Base de Datos haciendo dos queries:
Hibernate: select book_.id as id1_0_ from book book_ where book_.isbn=? Hibernate: select book0_.id as id1_0_0_, book0_.isbn as isbn2_0_0_, book0_.price as price3_0_0_, book0_.title as title4_0_0_ from book book0_ where book0_.id=?
Estas son las queries que ha realizado Hibernate en nuestra clase de test para hacer la búsqueda por NaturalId. Hibernate va a realizar dos queries, una por isbn (NaturalId) y otra por el id del objeto.
La razón para realizar dos queries es que Hibernate necesita la PK internamente para verificar el primer y segundo nivel de cache. Por lo general esto no tiene un gran impacto en el rendimiento de la aplicación, ya que Hibernate va a cachear el NaturalId como PK para la sesión y cachearlo en el segundo nivel de cache por si es necesario devolverlo.
Conclusión
En esta entrada hemos visto un ejemplo de NaturalId de Hibernate con Spring Boot, en el que hemos mostrado las diferentes partes para lograr una aplicación para poder buscar por NaturalId. Esta aproximación nos será de gran utilidad en aquellas ocasiones en las que nuestras búsquedas tengan que ser por un identificador Natural. Y aunque el número de queries que realiza Hibernate para poder lograr el resultado es mayor que haciendo una búsqueda por Id directamente, el rendimiento apenas se verá afectado.
No dudes en echar un ojo al ejemplo completo en nuestro github.
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!