1. El identificador es más profundo que @Id
Cuando ves una entidad JPA por primera vez, @Id parece una etiqueta aburrida y obligatoria: "sin ella Hibernate no arrancaría". Pero en un curso avanzado, esta etiqueta de repente se convierte en la mitad de la trama: del identificador depende cómo Hibernate sabe "quién es quién", cuándo hace SQL y por qué tus objetos a veces se comportan como personajes de una serie con pérdida de memoria.
A nivel básico, parece que el identificador solo sirve "para leer de la tabla": bueno, está product.id, con él hacemos SELECT. En la práctica, el identificador es el punto de apoyo sobre el que se construye toda la disciplina interna del ORM. Precisamente por @Id Hibernate decide si dos objetos se pueden considerar "el mismo" producto, cómo organizar la caché de primer nivel, cómo ejecutar correctamente merge() para datos detached (ya lo vimos), y por qué un find() repetido en la misma transacción devuelve el mismo objeto.
Otro punto importante: el identificador no es solo una historia "sobre la base de datos". Es una historia sobre tu memoria, colecciones, equals()/hashCode() e incluso sobre cómo escribes escenarios de servicio. El identificador responde a tres preguntas prácticas que hoy mantendremos en mente: quién crea el valor, cuándo aparece, y si tiene significado de negocio o sigue siendo una clave puramente técnica.
2. Tipos de identidad: objeto, BD, negocio
Es muy fácil confundirse, porque la palabra "identificador" suena como "algo único", y lo único, al parecer, debe ser uno solo. En la práctica, en la aplicación conviven al menos tres "identidades" diferentes, y si no las separas, tarde o temprano obtendrás errores extraños que serán difíciles de explicar a los compañeros sin gestos y risas nerviosas.
La primera — identidad de objeto en Java. Responde a la pregunta: "¿Es el mismo objeto en memoria?" y se verifica con el operador más honesto ==. Si a == b, entonces literalmente es la misma instancia, solo que tiene dos nombres. Hibernate mantiene esta identidad dentro del contexto de persistencia (identity map), pero el mundo JVM en sí no está obligado a pensar en la base de datos.
La segunda — identidad de tabla (database). Es la clave primaria de una fila en la tabla. En PostgreSQL suele ser BIGINT, a veces UUID, a veces (en sistemas heredados) una clave compuesta. En el modelo relacional, "quién eres" se define precisamente por la clave primaria. Una fila puede cambiar su name, status, price, pero si su clave primaria no ha cambiado — sigue siendo la misma fila.
La tercera — identidad de negocio. Es lo que una persona en el dominio considera identificador: sku de un producto, email de un cliente, orderNumber de un pedido. Es cómodo, legible y a menudo realmente único. Pero la identidad de negocio no tiene por qué coincidir con la clave primaria de la tabla, ni mucho menos con lo que es conveniente para Hibernate internamente.
Para no tener esto "en sensaciones", es útil verlo una vez lado a lado:
| Nivel | “¿Quién eres?” | Ejemplo | Cómo lo verificamos |
|---|---|---|---|
| Objeto Java | la misma instancia en memoria | Product@0xA1 | == |
| Base de datos | la misma fila de la tabla | product.id = 42 | clave primaria |
| Dominio (negocio) | el mismo producto "en sentido" | sku = "SKU-001" | campo único de negocio |
Hibernate vive en la intersección de estos mundos y hace algo importante: intenta organizar el trabajo de modo que la identidad de tabla dentro de una misma transacción corresponda a la identidad de objeto. Es decir, la "fila product(id=42)" debe estar representada por un único objeto managed Product, no por dos new Product() diferentes con el mismo id. Y adivina qué detalle se necesita para que esto sea posible. Sí — el identificador.
3. Contexto de persistencia e identity map
Si Hibernate fuera un bibliotecario, @Id sería el carnet de lector del libro. No el título, ni la descripción, ni el color de la portada, sino ese número por el cual el bibliotecario sabe con certeza: "Ajá, este libro ya está en mi escritorio, no lo traemos del almacén otra vez". El contexto de persistencia (en términos de Hibernate, Session) está organizado exactamente de ese estilo: almacena entidades managed y las recupera por la clave "tipo de entidad + identificador".
Es importante decirlo: solo id=1 no es suficiente, porque Product(id=1) y Customer(id=1) son entidades diferentes. Por lo tanto, la clave que maneja el ORM es lógicamente similar a EntityKey(entityName, id). De aquí se deriva el efecto básico que ya viste en días anteriores: un find() repetido con el mismo id dentro de una misma transacción devuelve el mismo objeto Java, sin una segunda consulta SQL.
Esquemáticamente, el contexto de persistencia se puede representar así:
flowchart LR
%% Clave: (tipo de entidad + id). Por ella Hibernate encuentra el objeto managed en una misma transacción.
subgraph PC["Contexto de Persistencia (Session / caché de 1er nivel)"]
K1["EntityKey(Product, 42)"] --> P1["Product @0xA1 (managed)"]
K2["EntityKey(Customer, 7)"] --> C1["Customer @0xB3 (managed)"]
end
%% Flush sincroniza los cambios del contexto de persistencia a la base de datos.
PC -->|flush| DB[(PostgreSQL)]
Observa una idea importante: el contexto de persistencia "recuerda" la entidad no por sku, ni por el nombre, ni por el conjunto completo de campos, sino precisamente por el identificador.
Mini‑experimento en nuestro proyecto: una fila — un objeto
En Commerce Persistence Lab este experimento se puede hacer cómodamente en un servicio (o en una prueba de integración), dentro de una transacción:
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductDebugService {
private final EntityManager em;
public ProductDebugService(EntityManager em) { this.em = em; }
@Transactional
public void checkIdentityMap(Long id) {
var p1 = em.find(Product.class, id);
var p2 = em.find(Product.class, id);
System.out.println(p1 == p2); // true
}
}
En el registro SQL verás un solo SELECT en el primer find(). En el segundo find() no habrá consulta, porque Hibernate ya tiene el objeto managed en el contexto de persistencia. Y esto no es un "caché para acelerar", sino una garantía fundamental de consistencia: si tuvieras dos objetos diferentes para una misma fila, el dirty checking y el flush se convertirían en un caos.
Por qué los proxies también "se sostienen" en @Id
Ya tocamos getReference() y los objetos proxy. Un matiz importante: un proxy puede existir sin una carga completa de la entidad, porque el identificador es conocido. Es decir, Hibernate puede decir: "Aquí tienes un objeto sustituto, conozco su id, y el resto lo traeré cuando lo pidas".
Este código muestra bien la idea (dentro de una transacción, para no caer por el límite lazy):
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void referenceIdOnly(EntityManager em, Long id) {
// Obtenemos un proxy: Hibernate conoce el identificador, pero no está obligado a hacer SELECT inmediatamente.
var ref = em.getReference(Product.class, id);
// Importante: leer el id normalmente no dispara la carga, porque el id ya es conocido por el ORM.
System.out.println(ref.getId()); // 42 (y a menudo sin SELECT)
}
El identificador aquí es un ancla. Permite a Hibernate almacenar una referencia a la entidad tanto en relaciones (FK) como en proxies y en el identity map. En este sentido, @Id no es un "campo de tabla", sino el "primer ladrillo" de todo el modelo en tiempo de ejecución.
4. Momento de aparición del identificador
En este punto suele comenzar un leve pánico: "Espera... ¿acaso el id no aparece siempre de inmediato?" Lamentablemente. O afortunadamente, si te gustan los sistemas complejos. El momento de aparición del identificador depende de cómo exactamente se genera: por la base de datos, por Hibernate del lado de Java, o por tu código. En las próximas lecciones compararemos las estrategias en detalle, pero ahora es importante fijar el modelo general sin nombres de anotaciones.
Una entidad en estado transient a menudo no tiene id (o lo tiene, pero es una elección consciente separada). Luego llamas a persist(): Hibernate registra la entidad en el contexto de persistencia. Y aquí son posibles variantes: a veces el id se puede obtener de inmediato, a veces solo cuando el ORM realmente hace el INSERT (normalmente durante el flush). A veces el id llega "desde fuera" antes de persist() — por ejemplo, si es un id asignado.
Si lo representamos como una línea de tiempo, quedaría algo así:
sequenceDiagram
%% El id puede aparecer en diferentes momentos: antes de persist(), después de persist() o solo en el INSERT.
participant App as Tu código
participant PC as Contexto de Persistencia
participant DB as PostgreSQL
App->>App: new Product()
Note over App: "transient: id puede ser null"
App->>PC: persist(product)
Note over PC: "la entidad se vuelve managed"
PC->>DB: flush
Note over DB: "INSERT: la base puede dar el id en la inserción"
DB-->>PC: id (si lo genera la BD)
¿Por qué nos importa este momento "cuándo el id se vuelve disponible"? Porque el id no es solo "para encontrarlo después". A veces se necesita ya mismo dentro de una operación de negocio. Por ejemplo, quieres relacionar la nueva entidad con otra, ponerla en un mapa, registrar "objeto X creado con id=...", o simplemente pasar el identificador más adelante en el código. Y si piensas mentalmente que id siempre se conoce de inmediato — empezarás a escribir código que "normalmente funciona", pero luego se rompe al cambiar la estrategia de generación.
En Commerce Persistence Lab mantendremos deliberadamente el registro SQL activo y observable para que estas cosas no sean conjeturas, sino hechos comprobables. Esto disciplina mucho: en lugar de "parece que el id aparece aquí", miras el momento del SQL y entiendes por qué id se volvió no nulo justo ahora.
5. Id técnico y campos de negocio
A veces uno quiere hacerlo bonito: ya que sku es único, ¿por qué no hacerlo clave primaria? Una persona busca el producto por él, así que es el "verdadero identificador". La lógica suena agradable, hasta que empiezas a vivir con este modelo uno o dos años y te das cuenta de que "agradable" tiene un precio. El curso de Hibernate trata precisamente del precio de las decisiones, así que nos detendremos en esto con calma y sin dogmas.
El id técnico (@Id) es una herramienta del ORM y la BD, que debe ser estable, conveniente para relaciones (FK), no demasiado ancha y no demasiado "inteligente". Un campo de negocio como sku es parte del modelo de dominio. Es legible, importante para las personas, a menudo participa en la unicidad y las integraciones. Pero la vida del dominio es complicada: sku puede tener un formato que el negocio quiera cambiar; email puede cambiar; orderNumber puede tener prefijos, migraciones, "cambio a una nueva numeración a partir del lunes".
Por lo tanto, un enfoque muy común y saludable es: mantener el id técnico como clave primaria, y el campo de negocio al lado, con una restricción de unicidad. En el ejemplo de un producto se ve de la manera más terrenal:
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id
// Id técnico: su tarea es ser un PK estable y la clave del identity map.
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(nullable = false, unique = true)
// Identificador de negocio: único para el dominio, pero no está obligado a ser PK.
private String sku;
}
Sí, aquí ya mostré SEQUENCE, pero por ahora tómalo como "algún generador". La comparación de estrategias será en la siguiente lección. En esta lección es importante otra cosa: id y sku resuelven problemas diferentes. Si empiezas a tratarlos como si fueran lo mismo, inevitablemente mezclarás responsabilidades y límites arquitectónicos.
En el repositorio esto suele leerse como "internamente vivimos por id, y en los escenarios de negocio sabemos buscar por sku":
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Entrada de dominio conveniente: búsqueda por campo de negocio, mientras el PK sigue siendo técnico.
Optional<Product> findBySku(String sku);
}
Observa: la presencia de findBySku no anula el sentido de @Id. Hibernate seguirá construyendo su identity map por la clave primaria. Simplemente ahora has añadido una entrada conveniente al modelo de dominio "a la humana".
6. Id en el código: equals()/hashCode() y Set
Si todos los problemas con el id se limitaran a elegir una columna en la BD, cerraríamos el portátil y nos iríamos a tomar un té. Pero el ORM es más interesante: el identificador afecta cómo se comportan tus entidades en las colecciones, cómo escribes equals()/hashCode(), e incluso qué operaciones "inocentes" rompen repentinamente el estado del objeto.
Ya viste en el Día 13 que para una entidad no se puede hacer equality "por todos los campos" sin pensar, y que el id generado es un apoyo incómodo para hashCode() antes de guardar. Hoy es importante relacionarlo con el modelo general: si el id no aparece de inmediato, entonces el objeto puede vivir primero con id=null, luego obtener un id, y si tu hashCode() depende del id — cambiará en mitad de la vida del objeto. Y HashSet y HashMap no soportan eso. Ellos no se apuntaron a la serie "el objeto cambió su hash y se fue a otro bolsillo".
Aquí tienes un mini‑ejemplo que en un proyecto real aparece más a menudo de lo que nos gustaría:
import java.util.HashSet;
// Imagina que lo siguiente se ejecuta en una transacción y em es un EntityManager disponible.
var set = new HashSet<Product>();
var p = new Product(); // id todavía null
set.add(p);
em.persist(p); // después de persist/flush el id puede aparecer y afectar a hashCode()
System.out.println(set.contains(p)); // puede volverse false si hashCode() es malo
El sentido no es que persist() "estropee colecciones". El sentido es que elegiste una estrategia de equality que no es compatible con el ciclo de vida de la entidad. Y en cuanto empiezas a percibir el identificador como parte del modelo en tiempo de ejecución, estos errores dejan de ser misterios: literalmente ves por qué la colección se comporta como lo hace.
Otra relación práctica — DTO/mapeo. Un error de novato: tomar un DTO de entrada y "volcarlo completamente en la entidad", incluyendo id. Como resultado, te organizas tú mismo o un intento de cambiar la clave primaria (lo que normalmente está prohibido y carece de sentido lógico), o una semántica extraña de "creé una nueva entidad pero ya con id". En nuestro proyecto mantenemos conscientemente el estilo find + mutate, donde el id se usa como "encontrar a quién cambiar", no como "un campo que editamos".
Y aquí hay un buen hábito de ingeniería: el id es algo que casi nunca "cambias", o lo obtienes del generador, o lo asignas una vez de forma consciente (id asignado), y luego lo tratas como la identidad de la fila.
7. Marco de elección del identificador: tres preguntas
Para no ahogarse en anotaciones, es útil mantener un marco simple. Suena casi como preguntas de entrevista, pero en el buen sentido: al responderlas, entiendes de inmediato qué clase de soluciones te conviene y cuál creará complejidad innecesaria. Ahora haremos estas preguntas sin una comparación profunda de estrategias — la comparación vendrá en las próximas lecciones del día.
Estas son las tres preguntas en versión "humana":
| Pregunta | Qué averiguas | Por qué es importante para el comportamiento de Hibernate |
|---|---|---|
| ¿Quién crea el valor del id? | BD / Hibernate (lado Java) / tu código | De esto depende dónde está la "fuente de verdad" |
| ¿Cuándo está disponible el id? | Antes de persist() / después de persist() / solo en el INSERT | Esto afecta al flujo de inserción, las relaciones y tu código alrededor de la entidad |
| ¿Tiene el id significado de negocio? | sí (business) / no (technical) | Esto afecta a la legibilidad del modelo y a qué se puede/no se puede cambiar |
Con estas mismas preguntas seguiremos adelante. Primero es importante entender cómo los diferentes modos de generar un PK técnico cambian el momento de aparición del id y la forma del flujo de INSERT. Luego — por qué al lado de este PK vive un business-id como sku o orderNumber. Y solo después — cuándo no es honesto fingir que la identidad cabe en un solo campo, porque la fila está determinada por una combinación de valores.
Si has respondido honestamente a estas preguntas, ya te has protegido en un 80% de dos extremos. El primer extremo — "en todos lados el mismo enfoque porque así estamos acostumbrados". El segundo — "hagámoslo bonito desde el dominio, pero luego nos sorprenderemos del precio para el ORM y el SQL". En nuestro proyecto, por regla general, las entidades centrales tendrán un id técnico, y los campos de negocio como sku, email, orderNumber vivirán al lado y expresarán el significado del dominio por separado. Pero hoy todavía no fijamos respuestas concretas, sino el hábito mismo de plantearlas.
8. Errores típicos al trabajar con identificadores
Los errores en torno a los identificadores la mayoría de las veces no se ven como un "error de compilación". Se ven como efectos secundarios extraños: consultas adicionales, comportamiento inesperado de las colecciones, confusión de "por qué Hibernate considera esto una nueva entidad", problemas con datos detached. La buena noticia es que casi todos estos errores se curan con la misma medicina: devolver el identificador a la imagen del mundo en tiempo de ejecución, en lugar de tratarlo como una "columna en la tabla".
Error nº 1: pensar que @Id solo sirve "para hacer SELECT por clave".
Cuando piensas así, pasas por alto que @Id es la clave del identity map en el contexto de persistencia. El resultado suele ser: la persona se sorprende de por qué dos objetos "con el mismo id" en una misma transacción son un problema, o por qué un find() repetido no hace SQL. En cuanto empiezas a pensar "tipo de entidad + id = clave del objeto managed", el comportamiento de Hibernate deja de parecer magia.
Error nº 2: mezclar el id técnico y la identificación de negocio, haciéndolos "una misma entidad".
Por ejemplo, hacer de sku la clave primaria solo porque es único. A veces está justificado, pero más a menudo pagas con la complejidad de las migraciones y las relaciones, y ganas solo una bonita cadena en la URL. Un modelo saludable normalmente mantiene un id técnico para las relaciones internas y un campo único de negocio separado para los escenarios de dominio.
Error nº 3: escribir código como si el id apareciera siempre inmediatamente después de new.
Esto lleva a una lógica del tipo "creo el objeto, enseguida muestro el id, enseguida pongo el id en otra estructura, enseguida lo devuelvo al exterior". Con algunas estrategias de generación simplemente no funciona: el id aparece más tarde, y empiezas a poner parches. El hábito correcto es no adivinar, sino entender cuándo y por qué el id se vuelve disponible (y verificarlo con el registro SQL).
Error nº 4: hacer equals()/hashCode() basados en el id generado y luego sorprenderse del comportamiento de HashSet.
Si el id no se genera instantáneamente, entonces antes de guardar id=null, después de guardar ya es un número/UUID, y el hash cambia. Las colecciones basadas en hash no están obligadas a "reorganizarse y perdonar". El resultado parece "el objeto desapareció del set", aunque tienes una referencia a él. Esto no es mística de Hibernate, es pura matemática de tablas hash multiplicada por el ciclo de vida de una entidad.
Error nº 5: permitir que un DTO externo "controle" el campo id.
A menudo se ve como "en la actualización llegó un id, simplemente lo puse en la entidad". En el mejor caso obtendrás una excepción, en el peor — confundirás la semántica de new/existing y obtendrás un comportamiento extraño de merge/dirty checking. Un estilo maduro es usar el id como puntero a quién cambiar, no como datos que cambian.
GO TO FULL VERSION