CodeGym /Cursos /Hibernate deep-dive /Estrategias de id: IDENTITY, SEQUENCE, UUID

Estrategias de id: IDENTITY, SEQUENCE, UUID

Hibernate deep-dive
Nivel 15, Lección 1
Disponible

1. ¿Qué comparamos: estrategias de id y SQL?

Ya hemos desglosado el id en varias preguntas, y ahora reducimos el enfoque a lo más doloroso: ¿quién emite el valor y en qué momento Hibernate logra conocerlo? Cuando miras la anotación @GeneratedValue, quieres pensar: "Bueno, es solo cómo se llena el campo, ¿qué diferencia hay?". Pero Hibernate percibe el identificador como parte de su matemática interna: mientras un objeto no tenga una identidad clara, es más difícil considerarlo en el persistence context, más difícil ordenar los INSERT y más difícil garantizar una sincronización correcta del grafo. Por lo tanto, la estrategia de id no es un "adorno del modelo", sino un interruptor real del comportamiento.

En esta lección compararemos cuatro estrategias prácticas precisamente desde el punto de vista del flujo de inserción: quién genera el valor y cuándo Hibernate puede conocerlo. Es importante que mantengas los dos ejes, porque casi todos los efectos posteriores (inserción temprana, imposibilidad de batching, viajes de ida y vuelta adicionales a la base de datos) resultarán ser solo una consecuencia de estas dos preguntas.

Estrategia Quién genera el id Cuándo Hibernate conoce el id
IDENTITY base de datos solo durante el INSERT
SEQUENCE base de datos (secuencia) antes del INSERT (Hibernate puede obtener nextval de antemano)
UUID Java (aplicación / Hibernate) antes del INSERT (sin consultar a la BD)
id asignado tu código incluso antes de persist() (si no lo olvidas)

Para no ser meras palabras, a continuación veremos la misma idea a través de tres lentes: qué sucede en la memoria, cómo se ve el trace SQL y por qué esto afecta la "sensación de control" sobre las inserciones.

2. IDENTITY: id desde la BD → INSERT inmediato

IDENTITY a menudo parece la opción más "natural": la columna tiene autoincremento, la base de datos misma emite el número y la vida es hermosa. Pero Hibernate tiene un pequeño problema: para asignar el id al objeto Java, necesita ejecutar un INSERT real y obtener el valor generado. Esto significa que pierde el principal placer del enfoque ORM: la posibilidad de acumular INSERT en una cola y enviarlos en un solo lote al flush().

En la práctica, esto significa que persist() para IDENTITY muy a menudo "de repente" lleva a SQL antes de lo esperado. Y no es un capricho del ORM, sino una consecuencia lógica: Hibernate no puede decirle al objeto "tu id = ..." hasta que la base de datos se lo diga a Hibernate. Imagina una cola en una oficina: hasta que no te den un ticket con número, no puedes participar "oficialmente" en la cola. Con IDENTITY, el ticket se entrega solo en la entrada, y cada persona debe acercarse al mostrador por separado.

Mini‑ejemplo de entidad con IDENTITY

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
class LabIdentityProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // id aparecerá solo después de la inserción real en la BD
    private Long id;

    private String name; // campo normal: por sí mismo no afecta el momento de generación de id
}

Cómo se ve en SQL (simplificado)

Para PostgreSQL, el patrón típico es insert ... returning id (u obtener generated keys mediante JDBC), y verás una idea similar:

insert into lab_identity_product (name) values (?) returning id;

No importa el "returning" concreto, sino el hecho: la inserción debe ocurrir para que el id sea conocido.

Por qué IDENTITY se combina mal con el batching

No entraremos en la configuración del batching (será otra historia grande), pero el sentido se puede entender ahora mismo. El batch insert es cuando Hibernate intenta enviar varios INSERT como un "lote" de statements idénticos. Con IDENTITY, después de cada INSERT Hibernate debe obtener el id concreto y escribirlo de vuelta en el objeto concreto. Como resultado, las inserciones se vuelven secuenciales y "síncronas": insertaste — obtuviste id — insertaste — obtuviste id. Incluso si habilitas el batching con un flag, IDENTITY en la mayoría de los casos hará que el insert batching para la entidad no funcione, porque la mecánica de obtención del id lo rompe.

Un detalle que a menudo sorprende

Si estás acostumbrado a pensar: "El SQL se irá en el flush/commit", con IDENTITY sentirás regularmente que "Hibernate me traicionó". En realidad, simplemente cumple honestamente el contrato de generación de id. Por eso, cuando analices el log SQL y veas un INSERT antes de lo esperado, una de las primeras preguntas debería ser: "¿Qué estrategia de id tenemos?".

3. SEQUENCE: id por adelantado, INSERT después

Después de IDENTITY, la estrategia SEQUENCE suele sentirse como "amigable con ORM". La razón es simple: una secuencia es un objeto separado de la BD que puede emitir nuevos valores (nextval) sin insertar una fila. Hibernate puede tomar el id por adelantado, asignarlo a la entidad en la memoria, marcar la entidad como managed, y luego, en flush(), enviar el INSERT. Así se obtiene un flujo más controlable: el id existe, pero puede que aún no haya una fila en la tabla.

Si hacemos una analogía, SEQUENCE es como tomar un ticket de la cola electrónica desde casa. Ya tienes el número, ya eres "participante del proceso", pero te acercarás a la ventanilla más tarde, cuando llegues a la oficina. Hibernate está contento: puede acumular un lote de inserciones y enviarlas de manera más eficiente.

Mini‑ejemplo de entidad con SEQUENCE

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;

@Entity
class LabSequenceProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "lab_seq_product_seq") // id se puede obtener antes del INSERT
    @SequenceGenerator(
            name = "lab_seq_product_seq",
            sequenceName = "lab_seq_product_seq" // nombre de la sequence en la BD (es necesario crearla con una migración)
    )
    private Long id;

    private String name;
}

Observa: en un proyecto real con Flyway, estás obligado a crear la secuencia con una migración. Hibernate no debe "inventar el esquema" por ti.

Fragmento de migración Flyway (idea)

create sequence lab_seq_product_seq start with 1 increment by 1;

create table lab_sequence_product (
    id   bigint not null primary key,
    name text   not null
);

Aquí hay una idea importante: la columna id no tiene por qué tener default nextval(...). Hibernate mismo puede llamar a nextval antes del INSERT (y normalmente lo hace). Tener default es una elección aparte, pero para entender el flujo de inserción no es fundamental.

Cómo se ve en el trace SQL

A menudo verás dos pasos: primero obtener el valor de la secuencia, luego la inserción (la inserción puede ser más tarde, en flush()):

select nextval('lab_seq_product_seq');
insert into lab_sequence_product (name, id) values (?, ?);

Sí, es una consulta adicional a la BD, pero permite que Hibernate no vincule el id con el hecho de la inserción.

Por qué SEQUENCE se lleva bien con el batching

Como el id se conoce de antemano, Hibernate puede formar un lote de INSERT de la misma forma y enviarlos como batch, en lugar de una serie de "inserté — obtuve id — inserté — obtuve id". No tiene que "detenerse" después de cada fila por el id.

Hay otra ventaja sutil: cuando creas un grafo de entidades y necesitas asignar referencias (por ejemplo, "un hijo referencia a un padre"), el id conocido de antemano a veces simplifica la vida. No confundas esto con que Hibernate "usa id para todas las relaciones" — se trata más bien de la organización interna y la previsibilidad.

Sobre los "saltos" en los números

Intuitivamente uno piensa: "Secuencia significa 1,2,3,4 sin saltos". En la práctica, los saltos son normales e inevitables. Una transacción puede revertirse, la aplicación puede caerse, Hibernate puede tomar rangos de valores "por si acaso" (optimizaciones). Por lo tanto, una secuencia es un generador de números únicos, no un dispositivo para obtener números de orden "perfectamente consecutivos" para contabilidad.

4. UUID: id sin base de datos, precio en índices

Después de los id numéricos, UUID a menudo se percibe como una opción "moderna y conveniente": se pueden generar identificadores sin consultar la base de datos, es útil para sistemas distribuidos, se puede tener el id antes de insertar la fila, y en general "todo se ve profesional". En realidad, es una herramienta realmente conveniente, pero como cualquier herramienta, tiene un precio. Y el precio no está principalmente en Hibernate, sino en la física del almacenamiento y la indexación en la base de datos.

Desde el punto de vista del flujo de inserción, UUID se comporta de manera muy similar a SEQUENCE en un aspecto clave: el id se conoce antes del INSERT. La diferencia es que UUID se genera en el lado de Java, es decir, no hay un viaje de ida y vuelta adicional a la base de datos por nextval. Pero el valor insertado es más grande, menos legible en los logs y generalmente peor para la localidad del índice (si el UUID es aleatorio).

Mini‑ejemplo de entidad con UUID

import java.util.UUID;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
class LabUuidProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID) // id se genera en el lado de la aplicación, sin contacto con la BD
    private UUID id;

    private String name;
}

Cómo se ve la inserción

El SQL se verá como un INSERT normal, solo que con un parámetro UUID:

insert into lab_uuid_product (name, id) values (?, ?);

El "truco" ocurre antes del SQL: Hibernate ya ha asignado el id al objeto en memoria. Esto se puede ver incluso sin base de datos: puedes imprimir el id después de persist() y ya estará.

Por qué UUID puede ser más costoso de lo que parece (a nivel de base de datos)

UUID generalmente ocupa más espacio que bigint. Los índices sobre UUID son más pesados, la comparación de UUID es más pesada, y si el UUID es aleatorio, las inserciones se "dispersarán" en el índice B‑Tree y pueden provocar más page splits. PostgreSQL con el tipo uuid funciona adecuadamente, pero "adecuadamente" no significa "gratis".

En un proyecto de laboratorio esto se nota especialmente en entidades que participan en inserciones masivas o en joins muy frecuentes. Sin embargo, no hay que convertirlo en una religión: a veces UUID está realmente justificado, especialmente si necesitas formar el id en el lado de la aplicación antes de la inserción, o si la entidad vive en el límite de la integración y el id no proviene de tu base de datos.

5. Id asignado: el id lo define tu código

El id asignado es la situación en la que no pones @GeneratedValue y tú mismo asignas el valor del @Id. Para Hibernate esta es la estrategia más "transparente": sin magia, sin generación, sin "dónde está mi sequence". Pero por la transparencia pagas con responsabilidad: debes garantizar la unicidad, debes establecer el id antes de persist(), y debes entender de dónde viene en absoluto.

Desde el punto de vista del flujo de inserción, el id asignado también es "cómodo": el id se conoce de antemano, Hibernate puede acumular INSERT en una cola, el batching es posible (al menos conceptualmente). El problema no suele ser el rendimiento, sino el factor humano: olvidaste establecer el id — obtienes una excepción; pusiste un duplicado — obtienes una violación de restricción; cambiaste la fuente del id — rompiste la compatibilidad.

Mini‑ejemplo de entidad con id asignado

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
class ExternalCatalogItem {

    @Id
    private String externalId; // clave viene de fuera: debes completarla tú mismo antes de persist()

    private String name;
}

Dónde se usa más a menudo el id asignado

El escenario más sano es cuando el id realmente viene de fuera y no puedes o no quieres reemplazarlo con un número interno. Por ejemplo, importación de un catálogo externo, integración con un socio, datos de un sistema legacy. En ese mundo, el id externo no es "solo un campo", sino parte del contrato: debe ser estable.

Cómo se ve un problema típico en el código

ExternalCatalogItem item = new ExternalCatalogItem();
// item.setExternalId("EXT-123"); // olvidaste
entityManager.persist(item);      // ¡pum! — Hibernate no sabe qué insertar en la PK

Hibernate no podrá insertar la fila porque la primary key no es algo "opcional". Dependiendo de la configuración, obtendrás una excepción ya sea en persist() o en flush(), pero el sentido es el mismo: el id asignado requiere disciplina.

6. Comparación por flujo de inserción

Después de los detalles, es fácil ahogarse en matices, por lo que es útil reunir la comparación en una tabla. No es un "ranking de las mejores estrategias", sino un chuleta: qué pagas y qué obtienes precisamente en las inserciones.

Criterio IDENTITY SEQUENCE UUID id asignado
El id aparece en el objeto después del INSERT real antes del INSERT antes del INSERT antes de persist()
La inserción puede posponerse hasta flush normalmente no (a menudo "antes") sí sí sí
Viaje adicional de ida y vuelta no (pero hay necesidad de insertar) sí (nextval) no no
Insert batching (conceptualmente) casi siempre malo generalmente bueno generalmente bueno generalmente bueno
Simplicidad desde la perspectiva del novato parece simple "un poco más complejo" "más moderno" "lo haré yo mismo"
Riesgos típicos inserciones tempranas, peor flujo batch saltos en valores, se necesita DDL índices grandes, claves pesadas disciplina, unicidad, duplicados

También es útil un diagrama para ver literalmente el momento en que IDENTITY "rompe" la inserción diferida:

flowchart TD
    A["persist(entity)"] --> B{"¿Estrategia de id?"}

    B -->|IDENTITY| C["Hibernate se ve obligado a hacer un INSERT"]
    C --> D["La BD genera el id"]
    D --> E["id asignado al objeto"]

    B -->|SEQUENCE| F["Hibernate hace nextval()"]
    F --> G["id asignado al objeto"]
    G --> H["INSERT se pospone hasta flush/commit"]

    B -->|UUID| I["Hibernate genera UUID en Java"]
    I --> J["id asignado al objeto"]
    J --> K["INSERT se pospone hasta flush/commit"]

    B -->|asignado| L["Tu código ya asignó id"]
    L --> M["Hibernate acepta id"]
    M --> N["INSERT se pospone hasta flush/commit"]

Si recuerdas solo este diagrama, ya será suficiente. Los demás matices son variaciones alrededor de él.

Diferencia en el log SQL de Commerce Persistence Lab

Es muy tentador convertir la discusión de estrategias en una filosofía y "guerra de enfoques". Pero estamos en un curso deep‑dive, por lo tanto el criterio es simple: mira el log SQL y pregúntate "¿por qué se fue precisamente ahora?".

Imagina que en el código de laboratorio haces aproximadamente este escenario: creas tres entidades en una transacción, pero no llamas a flush() manualmente ni haces consultas.

import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

class InsertFlowScenario {

    private final EntityManager em;

    InsertFlowScenario(EntityManager em) {
        this.em = em;
    }

    @Transactional
    void run() {
        // Importante: aquí solo registramos entidades en el persistence context.
        // Cuándo aparecerá el SQL depende de la estrategia de generación de id.
        em.persist(new LabSequenceProduct("A"));
        em.persist(new LabSequenceProduct("B"));
        em.persist(new LabSequenceProduct("C"));

        // commit finalizará la transacción y llamará a flush
        // (y entonces Hibernate normalmente enviará los INSERTs, si no tuvo que hacerlo antes)
    }
}

Si es SEQUENCE, a menudo verás: primero varios select nextval(...), y insert cerca del final de la transacción (en flush()). Si es IDENTITY, puedes ver insert inmediatamente después de cada persist(). Y ese es precisamente el momento en que la intuición del desarrollador "se rompe" si pensaba que persist siempre es "sin SQL".

Además, es útil mirar los logs no solo con ojos de "cuántas consultas", sino también con ojos de "qué forma tienen las consultas". IDENTITY a menudo emite la forma "insert returning", mientras que SEQUENCE emite "select nextval" separados más un insert normal.

7. Errores típicos al elegir la estrategia de id

Error nº 1: elegir IDENTITY "porque es más simple" y sorprenderse de los INSERT tempranos.
Los principiantes a menudo ponen IDENTITY en cualquier Long id porque parece la cantidad mínima de anotaciones. Luego activan el trace SQL y ven que las inserciones ocurren antes de flush(), y empiezan a buscar "quién llama a flush". El problema no es flush. El problema es que para IDENTITY la inserción es la forma de obtener el id. Si quieres un flujo de inserción más controlable, debes elegir conscientemente una estrategia en la que el id esté disponible de antemano.

Error nº 2: esperar de SEQUENCE una numeración bonita sin saltos.
Una secuencia emite valores únicos, no un "número de orden perfecto". Los rollbacks de transacciones, reinicios de la aplicación y optimizaciones de asignación de rangos crean gaps inevitables. Si necesitas un bonito "número de pedido sin saltos", eso es otro problema de negocio que se resuelve de otra manera, no con la esperanza puesta en una secuencia.

Error nº 3: tomar UUID como "simplemente más moderno", sin pensar en el coste de almacenamiento e índices.
UUID es conveniente porque aparece sin base de datos y antes de la inserción. Pero no es un bonus gratuito: almacenas claves más pesadas, construyes índices más pesados, y las lecturas/joins pueden volverse más costosos. UUID es una buena herramienta cuando resuelve un problema de ingeniería real (por ejemplo, se necesita el id antes del INSERT y no quieres o no puedes usar una secuencia). Si la tarea no lo requiere, UUID puede convertirse en un accesorio bonito pero caro.

Error nº 4: usar id asignado y olvidar asignar el valor antes de persist().
El id asignado es "máximo control", pero el control requiere disciplina. Si el id no se establece, Hibernate no podrá insertar la fila (la PK no puede ser null). Y esto suele salir a la luz no de inmediato: escribiste el código, "casi funciona", pero luego en un escenario olvidaste llenar el campo y todo falló en flush(). Con el id asignado es útil considerar el establecimiento del identificador como parte del constructor/fábrica, no como un "setter en algún momento después".

Error nº 5: intentar extraer un "significado humano" del id técnico.
Cuando un desarrollador ve un Long id secuencial, quiere usarlo como "número de entidad" y mostrarlo al usuario o vincularlo a lógica de negocio. Pero el id técnico es ante todo un mecanismo de identidad de la fila, no un atributo de negocio. En cuanto empiezas a tratarlo como significado de negocio, te limitas drásticamente la libertad de cambiar la estrategia, optimizar las inserciones y evolucionar el esquema.

1
Tarea
Hibernate deep-dive, nivel 15, lección 1
Bloqueada
Comparacion de IDENTITY y SEQUENCE segun el flujo SQL
Comparacion de IDENTITY y SEQUENCE segun el flujo SQL
1
Tarea
Hibernate deep-dive, nivel 15, lección 1
Bloqueada
UUID y assigned id antes de INSERT
UUID y assigned id antes de INSERT
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION