Clases base JPA

El arquetipo adopta DDD pragmatico: JPA esta permitido en el dominio, y las entidades heredan de una de tres clases base segun quien genera el ID. Esta pagina documenta el contrato de cada una.

Para la decision "que clase uso al crear una entidad", ver Como definir el modelo de dominio.

Diagram

Las tres clases son variaciones del mismo patron: implementan Persistable<PK>, aportan auditoria y equals/hashCode identicos, y se diferencian solo en el tipo de PK y quien la genera.

AlphaIdEntity

Clase base con PK String alfanumerica generada en memoria por @AlphaIDSequence.

Tipo de PK

String

Generacion

Automatica antes del save() (in-memory)

Contrato del dev

Heredar — no declarar @Id ni @GeneratedValue

Caso de uso tipico

Aggregate root expuesto por API REST (ID no enumerable, mitiga IDOR)

@Entity
@Table(name = "customers")
public class Customer extends AlphaIdEntity {
    // sin @Id — viene del padre
}

Generacion del AlphaID

El generador vive en commons/id/AlphaIdentifierGenerator y se invoca via la anotacion @AlphaIDSequence (un @IdGeneratorType de Hibernate). El algoritmo tiene dos pasos:

  1. IdGenerator.generateId() produce un long via Math.abs(UUID.randomUUID().MSB ^ LSB) — entropia efectiva ~63 bits.

  2. AlphaEncoder.encode(long) lo codifica en base 64 con el diccionario URL-safe 0-9 A-Z a-z - _.

Resultado: hasta 11 caracteres (log_64(2^63) ~ 10.5).

Caracteres visualmente ambiguos (0/O, 1/l/I): el alfabeto base64 no los evita. Si el ID se copia a mano desde papel o pantalla, puede generar errores — considerar un generador custom con alfabeto reducido.

Personalizacion: AlphaEncoder admite un diccionario alternativo via constructor, pero @AlphaIDSequence solo expone el nombre del generador. Cambiar longitud o alfabeto requiere implementar un IdentifierGenerator paralelo.

SequenceEntity

Clase base con PK Long generada por una sequence de base de datos.

Tipo de PK

Long

Generacion

Hibernate la asigna antes del INSERT desde la sequence

Contrato del dev

Declarar @SequenceGenerator(name = "entity_seq", sequenceName = "…​") en la entidad concreta

Caso de uso tipico

Tablas de alto volumen donde importa orden numerico o JOINs por FK numerica

@Entity
@Table(name = "invoices")
@SequenceGenerator(name = "entity_seq", sequenceName = "seq_invoices", allocationSize = 50)
public class Invoice extends SequenceEntity {
}

El allocationSize es configurable por entidad: valores altos mejoran performance a costa de gaps ante reinicios del proceso.

Multitenancy: si la entidad no declara @SequenceGenerator, Hibernate recae en una sequence global compartida. En un modelo schema-per-tenant esto es peligroso — puede producir colisiones de IDs entre tenants. El @SequenceGenerator por entidad no es opcional.

CustomIdEntity<PK>

Clase base generica sin generacion automatica de ID. El dominio conoce y asigna la PK.

Tipo de PK

Parametro de tipo: String, Long, UUID, o cualquier Serializable

Generacion

Ninguna — responsabilidad del dev

Contrato del dev

Asignar el ID antes de llamar a repository.save(). Si el ID es null al save, Hibernate intenta un UPDATE en lugar de un INSERT

Caso de uso tipico

Claves naturales definidas por el dominio (codigo ISO de pais, RUC, CUIL)

@Entity
@Table(name = "countries")
public class Country extends CustomIdEntity<String> {

    private Country(String isoCode) {
        this.id = isoCode;   // asignacion obligatoria antes del save
    }

    public static Country create(String isoCode) {
        return new Country(isoCode);
    }
}

// Uso:
persistencePort.save(Country.create("PE"));

¿Por que es critico asignar el ID antes de save()? Spring Data decide entre INSERT y UPDATE consultando Persistable.isNew(). Para CustomIdEntity, "nuevo" significa id == null.

  • Si save() se llama con id == null, la fila termina con PK null — viola la invariante de la clave primaria.

  • Si save() se llama con un id asignado sobre una entidad que el EntityManager no conoce, Spring Data la considera "existente" y ejecuta UPDATE sobre una fila que puede no existir — fallo silencioso o sobrescritura equivocada.

La disciplina se captura mejor en una factory (Country.create(…​)) que en un setId() libre — unifica la ruta a save().

Que incluyen todas las clases base

Las tres clases aportan tres comportamientos comunes:

Aspecto Resumen

Auditoria

createdBy, createdDate, lastModifiedBy, lastModifiedDate poblados automaticamente. Ver detalle.

equals / hashCode

Basados en el ID, compatibles con proxies de Hibernate. Ver gotcha con entidades sin persistir.

Persistable<PK>

isNew() resuelve el estado transient/managed — lo que Spring Data usa para elegir INSERT vs UPDATE.

Auditoria

Los cuatro campos se pueblan via AuditingEntityListener de Spring Data, que consulta dos beans configurados en config/JpaConfig:

  • AuditorAware<AuditUser> auditorProvider — extrae el usuario actual del SecurityContextHolder y mapea User.getUsername() a AuditUser.of(subject). El email queda null salvo que un bean custom lo provea.

  • DateTimeProvider dateTimeProvider — retorna Instant.now() del clock del proceso (UTC).

Ambos estan declarados con @ConditionalOnMissingBean, asi que pueden reemplazarse declarando un bean propio.

Cuando los campos de auditoria quedan null:

  • El codigo corre fuera de un request HTTP (tarea programada, handler de evento de dominio, arranque de la app). SecurityContextHolder.getContext().getAuthentication() retorna null y AuditorAware entrega Optional.empty().

  • En tests unitarios sin contexto Spring, @EnableJpaAuditing no esta activo y los listeners no se disparan.

Solucion para ejecuciones sin HTTP: declarar un AuditorAware que devuelva un usuario "de sistema" (AuditUser.of("system")) — el @ConditionalOnMissingBean deja que tu bean reemplace el default.

equals y hashCode

Las tres clases base implementan equals y hashCode sobre el ID, con soporte para proxies de Hibernate (ProxyUtils.getUserClass(obj)). No redeclarar estos metodos en las entidades hijas.

Gotcha con entidades sin persistir: como equals compara por ID, dos entidades recien creadas con id == null no son iguales entre si — y el hashCode colapsa al mismo valor (17) para todas.

Implicancia practica: al armar un Set de entidades nuevas antes del primer save(), contains() puede retornar false para una instancia que ya esta dentro, y usar entidades nuevas como claves de un Map es inseguro. La invariante de igualdad se recupera despues del save(), cuando el ID queda asignado.

Fuera de alcance

IDs compuestos (@EmbeddedId / @IdClass)

No estan soportados por las clases base. Para integrar con esquemas legacy que los requieran, declarar la entidad directamente sin heredar de ninguna base.

UUID automatico

No existe una clase base dedicada. Usar CustomIdEntity<UUID> asignando UUID.randomUUID() antes del save(). Si el patron se vuelve recurrente en el equipo, abrir un issue para evaluar una clase UuidEntity dedicada.

Referencias

Las clases base son un mecanismo — el diseno del modelo que las usa se apoya en literatura DDD establecida:

  • Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003). Definiciones canonicas de aggregate root, entidad, value object y factory.

  • Martin Fowler — DDD Aggregate. Definicion concisa del patron.

  • Vaughn Vernon — Effective Aggregate Design. Tres articulos sobre diseno de aggregates, consistencia transaccional y relacion con persistencia. Lectura recomendada para profundizar en por que las transacciones atraviesan el aggregate root y en criterios para decidir tamano y limites.