Ventajas de usar un tipo específico para los identificadores de las entidades en vez de un tipo básico

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

Java

Hibernate

jOOQ

Al persistir una entidad de dominio en la base de datos su identificador se guarda como un tipo de datos soportado por la base de datos. Si es una base de datos relacional habitualmente es el equivalente a un bigint o en una base de datos de documentos quizá un UUID. En las entidades de dominio el tipo de datos usado para el identificador es el equivalente de la base de datos en el lenguaje de programación. Por ejemplo, si en una base de datos la clave primaria es un bigint el identificador en la entidad de dominio es un Long. Esto es lo mas simple pero tiene algún inconveniente.

El inconveniente es que al ser el identificador un tipo de datos básico cualquier Long es aceptado, con lo que se hacen posibles errores o malos comportamientos al usar un identificador de otra entidad de dominio si también es un Long donde no se debería. El compilador no captura este tipo de errores porque entiende como correcto cualquier Long independientemente de su significado desde el punto de vista de la aplicación.

También en cierta medida es un problema en la legibilidad del código ya que el tipo de dato de una variable no es significativo para saber si es un identificador. También es un problema al trabajar con colecciones, los siguientes ejemplos de código demuestran que los tipos no son todo lo semánticos o significativos que deberían.

1
2
Long id = ...;
List<Long> ids = ...;
Collection-Long.java

La solución es crear un tipo para cada identificador de cada entidad y en vez de usar un Long pasar a usar un ProductoId, UsuarioId, CompraId o como en el ejemplo EventId. Estas serían unas posibles implementaciones.

1
2
3
4
package io.github.picodotdev.domain.misc;

public interface EntityId {
}
EntityId.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.domain.misc;

public abstract class LongId implements EntityId {

    private Long value;

    public LongId(Long value) {
        this.value = value;
    }

    public Long getValue() {
        return value;
    }
}
LongId.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
package io.github.picodotdev.domain.event;

import io.github.picodotdev.domain.misc.LongId;

import java.util.Objects;

public class EventId extends LongId {

    public EventId(Long id) {
        super(id);
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (!(o instanceof EventId)) { return false; }
        return Objects.equals(getId(), ((EventId) o).getId());
    }
}
EventId.java

El tipo de las colecciones ahora son más semánticas además de que el compilador realizará comprobaciones de tipos.

1
2
EventId id = ...;
List<EventId> ids = ...;
Collection-EventId.java

En la popular herramienta ORM de persistencia Hibernate o JPA se puede usar el tipo propio para el identificador usando la anotación @Converter y en otra alternativa de persistencia para Java como jOOQ especificando en el generador el tipo que se quiere usar para una columna. En ambos casos hay que proporcionar una implementación que convierta del tipo de la base de datos al del identificador en el dominio y viceversa. Son muy simples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package io.github.picodotdev.infrastructure.datasource.hibernate.converter;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

import io.github.picodotdev.domain.event.EventId;

@Converter
public class EventIdConverter implements AttributeConverter<EventId, Long> {

    @Override
    public Long convertToDatabaseColumn(EventId id) {
        return id.getValue();
    }

    @Override
    public EventId convertToEntityAttribute(Long value) {
        return new EventId(value);
    }
}
EventIdConverter-hibernate.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
package io.github.picodotdev.infrastructure.datasource.jooq.converter;

import io.github.picodotdev.domain.event.EventId;

public class EventIdConverter implements Converter<Long, EventId> {

    @Override
    public Class<Long> fromType() {
        return Long.class;
    }

    @Override
    public EventId from(Long value) {
        return new EventId(value);
    }

    @Override
    public Class<EventId> toType() {
        return EventId.class;
    }

    @Override
    public Long to(EventId id) {
        return id.getValue());
    }
}
EventIdConverter-jooq.java

En una entidad de Hibernate los identificadores se definen de la siguiente forma.

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

...

@Entity
public class Event {

    @Id
    @GeneratedValue
    @Convert(converter = EventIdConverter.class)
    private EventId id;

    ...
}
Event.java

En jOOQ en la configuración del generador hay que especificar que para un campo se use un converter.

 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
55
56
plugins {
    id 'java'
    ...
    id 'nu.studer.jooq' version '3.0.3'
}

...

jooq {
    version = versions.jooq
    edition = 'OSS'
    mysql(sourceSets.main) {
        jdbc {
            driver = 'com.mysql.cj.jdbc.Driver'
            url = "jdbc:mysql://localhost:3306/${mysqlDdlJooqSchema}"
            user = 'root'
            password = ''
        }
        generator {
            name = 'org.jooq.codegen.DefaultGenerator'
            database {
                name = 'org.jooq.meta.mysql.MySQLDatabase'
                inputSchema = mysqlDdlJooqSchema
                outputSchema = 'application'
                includes = """
                    event
                """
                excludes = ''
                ...
                forcedTypes {
                    ...
                    // Ids
                    forcedType {
                        types = 'BIGINT'
                        userType = 'io.github.picodotdev.domain.event.EventId'
                        converter = 'io.github.picodotdev.infrastructure.datasource.jooq.converter.EventIdConverter'
                        expression = '.*\\.event\\.id'
                    }                   
                }
            }
            generate {
                javaTimeTypes = true
                interfaces = false
                pojos = true
                records = true
                relations = true
            }
            target {
                packageName = 'io.github.picodotdev.infrastructure.datasource.jooq.entities'
                directory = 'src/main/java-generated'
            }
        }
    }
}

...
build.gradle

Con un tipo de datos propio para los identificadores es muy importante implementar correctamente los métodos equals y hashCode tanto en clases de identificadores como en las entidades de dominio ya que las colecciones de Java se basan en estos métodos para determinar si una colección contiene un elemento.