Generar en el dominio los identificativos de las entidades aplicando DDD antes de persistirlas en la base de datos

Escrito por picodotdev el .
java planeta-codigo
Enlace permanente Comentarios

Las bases de datos tiene la capacidad de generar identificativos para los datos que se insertan. En el caso de las bases de datos relacionales con secuencias que generan en el momento de inserción la clave primaria de la fila en una tabla, normalmente es un número y utilizando Java con JPA con las anotaciones Id, GeneratedValue y SequenceGenerator en la clase Java que representa a la entidad. Para Domain Driven Design delegar en el momento de inserción la generación del identificativo de la entidad es un problema ya que hace que la entidad sea inválida al no tener identidad hasta persistirla y la base de datos es un elemento externo que debe ser independizado del dominio de la aplicación. En este artículo comento una implementación siguiendo los principios de DDD para dar solución a estos dos problemas.

Java

Tradicionalmente la tarea de generar los identificativos de las entidades de dominio se delega en la base de datos en el momento de persistir la entidad. La base de datos en la columna de la tabla de la entidad para el identificativo generalmente es de tipo numérico y la base de datos le asigna un valor incremental para cada fila o entidad guardada.

Este modelo de delegar en la base de datos el generar la identificativos de las entidades tiene dos problemas en la teoría de Domain Driven Design o DDD:

  • La aplicación requiere y es dependiente de un sistema externo para asignar la identidad de una entidad del dominio creada en la aplicación.
  • La entidad no tiene identidad inicialmente, lo que significa que la entidad es creada con un estado inválido por ser incompleto.

Que la entidad no tenga identidad asignado y esté incompleta en el momento de creación tiene inconvenientes ya que al implementar los métodos equals y hashCode en Java para una entidad estos se basa en el identificativo de la entidad para determinar si dos instancias de un objeto es la misma, si la entidad no tiene identidad el método equals es ineficaz. Al mismo tiempo el método hashCode, y también el método equals, es utilizado por la API de colecciones de Java en su mayoría con lo que la entidad no es posible guardarla en colecciones que dependan de estos métodos para su correcto funcionamiento. Para usar los métodos equals y hashCode de las entidades es necesario esperar a guardar la entidad en la base de datos para que se le asigne el identificativo.

También en DDD se suelen utilizar eventos como mecanismo de comunicar que en el sistema se ha sucedido algo, si la entidad no tiene identificativo no es posible comunicar que ha ocurrido algo, al menos no incluyendo el identificativo.

Identificativos universales como identificadores

Una posibilidad es generar identificativos universales para los identificativos de las entidades, sin embargo, la clase UUID depende de elementos externos al dominio como el tiempo del sistema. Al mismo tiempo la entidad no es consciente de la existencia de otras entidades y no le es posible determinar la unicidad del identificativo.

En DDD todo elemento que dependa de algo externo ha de se independizado del dominio. De modo que el UUID aplicando DDD no se genera en la entidad sino en la capa de servicio mediante un elemento externo que en la terminología de DDD es un adaptador, el identificativo se le proporciona a la entidad en el momento de creación en el constructor como parámetro.

1
2
3
System.out.println(UUID.randomUUID().toString());
...
95ba87c1-f0ac-4c55-9efa-257dbe291a7d
UuidGenerator.java

Delegar la generación de identificativos en el repositorio

Dado que en DDD se utiliza un repositorio para persistir las entidades en una base de datos externa a la lógica de dominio, la tarea de generar los identificadores que depende de un elemento externo es posible ubicarla en la misma clase repositorio, de esta manera la lógica queda con cohesión ya que todo lo relativo a la entidad está ubicada en su repositorio.

Al mismo tiempo delegar la tarea de crear el identificativo en el repositorio permite variar la implementación, una opción es delegar en la base de datos la obtención del identificativo o utilizar el método de identificativo universal anterior. En el caso de delegar en la base de datos la generación del identificativo, es la base de datos la que lo genera igual que en el caso de la autogeneración pero ahora no de manera implícita sino de forma explícita.

Ejemplo utilizando JPA y Spring Data

Utilizando Spring Data con JPA para añadir métodos personalizados en la clase del repositorio hay que crear una interfaz que los incluya y construir una implementación de esa interfaz. La misma interfaz es implementada por la interfaz de Spring Data, y Spring haciendo su magia y por composición crea un repositorio que tiene tanto los métodos implementados por Spring como la implementación de los métodos personalizados, en este caso el de generar el identificativo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.entitiesid.domain.product;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface ProductRepository extends PagingAndSortingRepository<Product, ProductId>, JpaSpecificationExecutor<Product>, CustomProductRepository {

    @Override
    @Modifying
    @Query("delete from Product")
    void deleteAll();
}
ProductRepository.java
1
2
3
4
5
6
package io.github.picodotdev.blogbitix.entitiesid.domain.product;

public interface CustomProductRepository {

    ProductId generateId();
}
CustomProductRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.entitiesid.domain.product;

import org.springframework.beans.factory.annotation.Autowired;

import java.math.BigInteger;
import javax.persistence.EntityManager;

public class CustomProductRepositoryImpl implements CustomProductRepository {

    @Autowired
    private EntityManager entityManager;

    @Override
    public ProductId generateId() {
        BigInteger id = (BigInteger) entityManager.createNativeQuery("select nextval('product_id_seq')").getSingleResult();
        return ProductId.valueOf(id);
    }
}
CustomProductRepositoryImpl.java

En la clase de la entidad no se usa la anotación GeneratedValue. En vez de esa anotación en este ejemplo se utiliza la anotación EmbeddedId y la anotación Embeddable, aplicando otro de los principios de DDD que es utilizar un tipo especico que representa el identificativo de la entidad en vez de un tipo proporcionado por el lenguaje como un Long o BigInteger. Un tipo específico para la identidad tiene varias ventajas como aprovechar los beneficios del compilador para detectar errores y de los IDE con asistencia de código.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package io.github.picodotdev.blogbitix.entitiesid.domain.product;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Objects;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "Product")
public class Product {

    @Id
    @EmbeddedId
    private ProductId id;

    private String name;
    private LocalDate date;
    private BigDecimal price;
    private Integer units;

    public Product() {
    }

    public Product(ProductId id, String name, LocalDate date, BigDecimal price, Integer units) {
        this.id = id;
        this.name = name;
        this.date = date;
        this.price = price;
        this.units = units;
    }

    ...

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;
        if (!(o instanceof Product))
            return false;

        Product that = (Product) o;
        return Objects.equals(this.id, that.id);
    }
}
Product.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package io.github.picodotdev.blogbitix.entitiesid.domain.product;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.Objects;
import javax.persistence.Embeddable;

@Embeddable
public class ProductId implements Serializable  {

    private BigInteger id;

    protected ProductId() {
    }

    protected ProductId(BigInteger id) {
        this.id = id;
    }

    ...

    public static ProductId valueOf(BigInteger id) {
        return new ProductId(id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;
        if (!(o instanceof ProductId))
            return false;

        ProductId that = (ProductId) o;
        return Objects.equals(this.id, that.id);
    }
}
ProductId.java

De esta forma ahora las entidades creadas son completamente válidas desde el momento de generación en el dominio ya que tienen su identificador. Dado que la entidad tiene su propio identificativo desde el inicio de su existencia es posible guardar la entidad en colecciones y lanzar eventos de dominio que incluyan su identificador sin tener que esperar que la base de datos le autogenere uno.

En este caso de prueba se observa que la entidad Product creada se crea en el constructor con su identificativo asignado sin esperar a que la base de datos lo genere, la base de datos y JPA simplemente persisten el valor que tiene asignado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.github.picodotdev.blogbitix.entitiesid.domain.product;

...

@SpringBootTest
@ContextConfiguration(initializers = { DefaultPostgresContainer.Initializer.class })
public class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void testRepositoryGenerateId() {
        // given
        ProductId id = productRepository.generateId();
        Product product = new Product(id, "Raspberry Pi", LocalDate.now(), new BigDecimal("80.0"), 10);

        // and
        productRepository.save(product);

        // then
        assertEquals(product, productRepository.findById(id).get());
    }
}
ProductRepositoryTest.java

En las trazas se observa la SQL para obtener el valor de la secuencia y la SQL de insert para guardar la entidad.

1
2
3
Hibernate: select nextval('product_id_seq')
Hibernate: select product0_.id as id1_0_0_, product0_.date as date2_0_0_, product0_.name as name3_0_0_, product0_.price as price4_0_0_, product0_.units as units5_0_0_ from product product0_ where product0_.id=?
Hibernate: insert into product (date, name, price, units, id) values (?, ?, ?, ?, ?)
System.out
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew test

Comparte el artículo: