Ejemplo de NaturalId de Hibernate con Spring Boot

NaturalId con Hibernate

NaturalId con Hibernate


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!


Deja una respuesta

Tu dirección de correo electrónico no será publicada.