CodeGym /Cursos /Hibernate deep-dive /ORM checklist: red flags en Hibernate

ORM checklist: red flags en Hibernate

Hibernate deep-dive
Nivel 30, Lección 2
Disponible

1. Checklist para la capa de persistencia

Si en el código Java normal solemos ver causa y efecto casi directamente (llamé a un método, obtuve un resultado), Hibernate ama las sorpresas: cambié un campo — el UPDATE aparecerá después, no «ahora mismo»; tomé order.getCustomer() — y de repente se fue un SQL; hice un bulk update — y el persistence context se quedó tranquilamente viviendo en su realidad paralela. Por eso, el code review de la capa de datos no es solo «si está bien escrito», sino «qué hará este código realmente en la BD».

La matriz de selección es útil mientras miras el escenario desde arriba. Pero en un PR nadie etiqueta el código con palabras como «aquí list-read a través de entity» o «aquí accidentalmente creé un stale state». En la revisión primero ves service, mapper, saveAndFlush(), recorrido del grafo y solo después lo relacionas con la matriz. El checklist es justo para eso: detectar rápidamente un red flag y entender qué pregunta hacer.

El checklist aquí funciona como la lista pre-vuelo de un piloto. El piloto también sabe volar sin papel, pero el papel salva del factor humano: cansancio, apuro, «pero esto es obvio». En ORM todo se complica porque parte del comportamiento es implícito: transacciones, flush, proxies, cascadas, lazy loading, versiones. El checklist ayuda a convertir la revisión de «opiniones» en un proceso repetible: hacemos las preguntas correctas, buscamos red flags y, si es necesario, los convertimos en un finding con un riesgo claro.

2. Un buen ORM-checklist sin prohibiciones

Un checklist para código Hibernate no es una «lista negra de anotaciones» ni un intento de prohibir todo excepto select * from .... Su tarea es más simple y útil: convertir los ejes de auditoría en preguntas para el PR concreto. Cada pregunta debe llevar a una decisión de ingeniería: o bien confirmamos que todo está bien (y fusionamos tranquilos), o documentamos el riesgo y pedimos aclaración/cambio.

Es útil pensar en un elemento del checklist como una pequeña construcción «señal → pregunta → riesgo». La señal es un red flag, es decir, un patrón reconocible en el código. La pregunta es lo que le hacemos al autor (o a nosotros mismos) para averiguar si hay una razón para hacerlo así. El riesgo es lo que ocurrirá en tiempo de ejecución si no hay razón. Y después de eso elegimos una acción: dejar como está, solicitar una corrección, proponer una alternativa de la matriz de soluciones.

A continuación se muestra un «mapa» compacto de los bloques que normalmente componen el checklist para nuestro Commerce Persistence Lab.

Bloque del checklist Qué miramos en el código Riesgo típico si es «porque sí»
Transiciones de estado save(), saveAndFlush(), merge(), clear(), detach() SQLs innecesarios, flush prematuro, trampas de merge, grafo impredecible
Límite transaccional dónde está @Transactional, dónde no, y dónde es demasiado amplio LazyInitializationException, lecturas inconsistentes, transacciones largas, SQL «flotante»
Fetch & read-model findAll()+stream(), recorrido del grafo, EAGER, toString() en entity N+1, overfetching, consultas accidentales, dirty checking pesado en read-path
Calidad del mapeo métodos helper, mappedBy, cascade, orphanRemoval, equals/hashCode FK no se actualiza como esperamos, delete/insert innecesarios, colecciones rotas, trampa de proxies
Concurrencia @Version, modos de bloqueo, secciones críticas lost update, bloqueos, mentalidad de deadlock «ya llegará (no)»
Bulk & read-only @Modifying bulk, hints/transacciones read-only stale persistence context, «actualizamos la BD pero el objeto piensa otra cosa», efectos extraños al leer

Para que esto no quede en teoría, es útil tener en mente una ruta de revisión simple y práctica:

flowchart TD
    A["Seleccionamos use case / hot path del PR"] --> B["Entendemos: ¿read o write?"]
    B --> C["Recorremos los bloques del checklist"]
    C --> D["Registramos red flags como preguntas"]
    D --> E["Si el riesgo es real: proponemos solución de la matriz"]

Observen: aquí no hay un paso de «discutir sobre anotaciones de inmediato». Primero entendemos el escenario y solo después vemos qué tan adecuado es el código para él.

Pero el checklist por sí mismo no es una prueba. Es útil hasta el punto en que dices: «parece N+1», «parece un flush innecesario», «parece un stale state». Luego esa sospecha hay que confirmarla con SQL, contadores y pruebas; de lo contrario, la revisión se queda a nivel de intuición.

3. Transiciones de estado: save, flush, merge

En este bloque verificamos que el autor del PR comprende en qué estado vive la entidad y por qué exactamente invoca ciertos métodos. Hibernate no es cuestión de «llamé a save() — se guardó», sino de unit of work: la entidad managed cambia en memoria y el SQL va a la BD en los momentos de flush/commit. Por lo tanto, cualquier acción «extra» alrededor del estado es una bandera roja: casi siempre esconde una falta de comprensión o arregla un síntoma en lugar de la causa.

Red flag: saveAndFlush() dentro de una transacción «por si acaso».
Esta es una de las señales más frecuentes. saveAndFlush() no solo «guarda», sino que también fuerza el flush inmediatamente. Si no es necesario para la corrección (por ejemplo, no necesitamos que los datos sean visibles en la BD antes de la siguiente consulta), entonces es un SQL innecesario y una posible impredecibilidad.

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
class OrderAdminService {

    @Transactional
    public void changeStatus(PurchaseOrder order, OrderStatus status) {
        // La entidad está managed dentro de la transacción: Hibernate verá los cambios por sí mismo (dirty checking)
        order.setStatus(status);

        // Red flag: forzamos el flush ahora mismo.
        // Solo es necesario si más adelante en el mismo método hay una consulta/restricción
        // que necesita ver los cambios antes del commit.
        // orderRepository.saveAndFlush(order);
    }
}

En la revisión, una buena pregunta no es «quita eso», sino «¿por qué necesitas un flush temprano aquí? ¿hay alguna consulta/restricción que requiera sincronización antes del final de la transacción?». Si no hay respuesta, probablemente el método es innecesario.

Red flag: save() después de findById() y modificar una entidad managed.
Psicológicamente dan ganas de «asegurar la persistencia». Pero Hibernate ya «tiene» la entidad: dirty checking hará su trabajo. save() aquí o no aporta nada, o (dependiendo de newness/identificador) puede llevarnos indirectamente a una semántica de merge, lo que es especialmente desagradable en grafos.

import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameProduct(long id, String newName) {
    // Cargamos la entidad: de ahora en adelante estará managed dentro de la transacción
    Product product = productRepository.findById(id).orElseThrow();

    // Basta con cambiar el campo: el UPDATE se emitirá en flush/commit automáticamente
    product.setName(newName);

    // Red flag: casi siempre es una operación innecesaria que crea la ilusión de «lo guardé manualmente».
    // productRepository.save(product);
}

Red flag: «DTO → entity → save» para un update-path.
Si ves código que crea un nuevo objeto Product, le asigna un id, copia campos del DTO y hace save(), entonces casi siempre es una invitación a una fiesta de merge. Y merge, como ya sabemos, ama los SELECT ocultos, la copia de estado y las sorpresas en los grafos.

En la revisión aquí conviene preguntar: «¿por qué no find + mutate?». Generalmente es más simple y predecible.

import org.springframework.transaction.annotation.Transactional;

@Transactional
public void updateProduct(long id, String name) {
    // Update-path predecible: encontramos la entidad managed
    Product product = productRepository.findById(id).orElseThrow();

    // Y cambiamos puntualmente los campos necesarios (sin merge y sus sorpresas)
    product.setName(name);
}

Red flag: clear() / detach() sin una razón muy clara.
Estos métodos no son una «optimización», sino un cambio en las reglas del juego: desactivas manualmente la supervisión de Hibernate sobre los objetos. Esto puede ser necesario en escenarios bulk o en laboratorios específicos, pero en un flujo CRUD normal, sin explicación, parece sospechoso.

Si ves entityManager.clear() en medio de un servicio, haz una pregunta simple: «¿qué problema de stale-state o memoria lo motiva?». Si la respuesta suena como «para que funcione», es casi garantía de una deuda arquitectónica.

4. Límite transaccional y Session

A continuación verificamos que el código se ejecute dentro de un marco transaccional correcto. En nuestro curso y en el proyecto base, open-in-view=false, por lo que no podemos confiar en que «bueno, en el controlador aún había sesión». Cualquier acceso lazy fuera de la transacción o fallará, o te obligará a activar OSIV y esconder el problema (y eso no lo hacemos).

Red flag: un método de servicio que hace varias operaciones de escritura sin @Transactional.
A veces el autor «reparte» la lógica entre repositorios y espera que cada save() sea «atómico» por sí mismo. Pero el unit of work debe estar al nivel de la operación de negocio.

import org.springframework.stereotype.Service;

@Service
class InventoryService {

    public void reserve(long itemId, int qty) {
        // Sin @Transactional: el objeto puede volverse detached inmediatamente después del método del repositorio
        InventoryItem item = inventoryRepository.findById(itemId).orElseThrow();

        // Red flag: los cambios pueden no llegar a la BD porque no hay unit of work ni flush/commit
        item.reserve(qty);
    }
}

En la revisión es importante preguntar: «¿cuál es el límite del unit of work? ¿dónde está @Transactional y qué modo (read-only o write)?». Si el método es write-path, la transacción debe ser explícita.

Red flag: self-invocation e ilusión de transacción.
Este es un bug sutil que parece «todo correcto», pero la transacción no se abre porque Spring no puede aplicar el proxy a una llamada dentro del mismo objeto.

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
class OrderService {

    public void cancel(long id) {
        // Red flag: la llamada a un método dentro de la misma clase elude el proxy de Spring,
        // por lo que @Transactional puede no funcionar
        cancelTx(id);
    }

    @Transactional
    void cancelTx(long id) {
        // La transacción se abrirá garantizadamente solo si el método se invoca a través del proxy
        /* ... */
    }
}

La pregunta del checklist aquí es simple: «¿está seguro de que el método se invoca a través del proxy de Spring?». A menudo la solución es extraer el método transaccional a un bean separado o reorganizar la orquestación.

Red flag: transacción demasiado amplia en un read-path.
Sí, la transacción es necesaria incluso para lectura (por consistencia y para una carga lazy predecible), pero «mantener una transacción alrededor de todo el mundo» también es peligroso. En la revisión conviene prestar atención a que dentro de un escenario read-only no ocurran cambios accidentales en entidades managed. Si ves @Transactional(readOnly=true) y dentro alguien hace setName(), ya no es solo «estilo», es un riesgo de comportamiento inesperado.

5. Fetching y read-model: patrones N+1

Este bloque a menudo sorprende a los novatos: «¿cómo puedo hablar de N+1 si aún no he visto el SQL?». Spoiler: no afirmamos que N+1 exista con certeza, pero sabemos reconocer patrones que casi siempre lo provocan. Es como ver en el código un while(true) sin break: teóricamente puede ser normal, pero mejor preguntar.

Red flag: findAll() + stream() + navegación por relaciones.
Este es el clásico «escondedor de consultas»: en el código un solo método, pero en la BD una serie completa de SELECT.

import java.util.List;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
public List<String> findOrderEmails() {
    // Red flag: findAll carga las entidades raíz, y luego recorremos las relaciones
    return orderRepository.findAll().stream()
            // Potencial N+1: getCustomer() puede ser LAZY y lanzar un SELECT adicional por cada fila
            .map(o -> o.getCustomer().getEmail())
            .toList();
}

En la revisión, la pregunta del checklist suena así: «¿es un list-use case? ¿estamos seguros de que necesitamos entidades? ¿por qué no una proyección o un fetch-plan explícito?». Y noten: es una pregunta sobre la solución, no sobre «gustos».

Una respuesta normal a este red flag es la proyección.
Por ejemplo, en lugar de cargar entidades, podemos hacer una consulta que devuelva directamente los valores necesarios. Será más simple tanto para Hibernate como para tu cerebro.

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {

    // Proyección: devolvemos datos «planos» sin cargar el grafo de entidades
    @Query("select o.customer.email from PurchaseOrder o")
    List<String> findAllCustomerEmails(); // Se espera una sola consulta
}

Red flag: modelo de entidad «universal» para listas.
Si el PR añade un método como List<PurchaseOrder> findByStatus(...) y luego en el servicio/mapper empiezan a extraer de la orden todo lo que hay (cliente, posiciones, totales, direcciones), casi siempre es señal de que se mezclaron read-model y write-model. Pregunta del checklist: «¿qué forma de salida se necesita? ¿se puede separar el DTO de fila de lista del detalle?».

Red flag: toString()/logging que toca relaciones lazy.
Si ves un toString() que imprime items, addresses, categories — es una mina ORM. En el mejor caso habrá SQL ruidoso, en el peor una excepción si toString() se llama fuera de una transacción.

@Override
public String toString() {
    // Red flag: acceder a una colección lazy en toString() puede:
    // - iniciar consultas SQL inesperadas (¡dentro del logging!)
    // - lanzar LazyInitializationException fuera de la transacción
    return "PurchaseOrder{id=" + id + ", items=" + items + "}";
}

Pregunta del checklist: «¿es seguro toString()? ¿no toca colecciones lazy?». En un proyecto educativo es mejor hacer un toString() mínimo: solo id y un par de campos simples.

6. Calidad del mapeo: relaciones, cascadas, equals

Pasamos a lo que a menudo rompe el runtime, incluso si las consultas están perfectamente escritas: el mapeo. Es como los cimientos de una casa: por fuera todo bonito, pero si los cimientos están en «palabra de honor», la casa empieza a agrietarse en el momento más inoportuno. Hibernate añade su propia idiosincrasia: el owning side gestiona la FK, cascade refleja el ciclo de vida, equals/hashCode interactúa con proxies, y las colecciones no son «simples ArrayList», sino persistent collections.

Red flag: relación bidireccional no sincronizada mediante un método helper.
Si en el PR ves acceso directo a la colección order.getItems().add(item) — es motivo de alerta. Es muy fácil olvidar establecer el owning side, y entonces la clave foránea o no se actualizará, o se actualizará «mal».

import java.util.ArrayList;
import java.util.List;

class PurchaseOrder {
    private final List<OrderItem> items = new ArrayList<>();

    public void addItem(OrderItem item) {
        // Modificamos la colección en el lado parent (inverse/no owning en el mapeo típico)
        items.add(item);

        // Importante: sincronizamos el owning side, de lo contrario la FK podría no actualizarse como esperamos
        item.setOrder(this);
    }
}

Pregunta del checklist: «¿a través de qué se gestiona la relación: mediante un helper o mediante la colección directamente?». En nuestro proyecto la respuesta correcta es casi siempre: mediante un helper.

Red flag: cascade “ALL” en asociaciones compartidas.
Si PurchaseOrder contiene una referencia a Customer, entonces Customer definitivamente no es una entidad hija de la orden. Cascade en un ManyToOne hacia Customer fácilmente provoca consecuencias accidentales: desde persist inesperados hasta «uy, parece que eliminamos al cliente junto con la orden».

import jakarta.persistence.CascadeType;
import jakarta.persistence.ManyToOne;

class PurchaseOrder {

    // Red flag: Customer es una entidad compartida. CascadeType.ALL puede arrastrar:
    // - un persist/merge accidental sobre el cliente
    // - y en configuraciones peores, un remove (que definitivamente no es lo que queremos)
    @ManyToOne(cascade = CascadeType.ALL)
    private Customer customer;
}

En la revisión preguntamos: «¿cuál es el ciclo de vida? ¿debe el hijo vivir y morir junto con el padre?». Es una excelente pregunta porque obliga a hablar del modelo de dominio, no de magia de anotaciones.

Red flag: equals() mediante getClass() (trampa de proxy).
Hibernate puede proporcionar una clase proxy, y la comparación con getClass() rompe la igualdad. Esto no es «teoría», es una causa real de bugs extraños en Set, orphan removal y en la comparación de entidades dentro del persistence context.

import org.hibernate.Hibernate;

@Override
public boolean equals(Object o) {
    if (this == o) return true;

    // Importante: la comparación con Hibernate.getClass tiene en cuenta las clases proxy
    if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;

    Product other = (Product) o;

    // Estrategia básica: comparación por id, pero solo si ya está asignado
    return id != null && id.equals(other.id);
}

Pregunta del checklist: «¿la igualdad considera la existencia de proxies? ¿depende de campos mutables?». Y sí, este es el momento en que @Data de Lombok se convierte en tu enemigo, no en tu amigo.

7. Concurrencia: versiones y bloqueos

La concurrencia en la capa de persistencia no es «matemáticas superiores», sino una realidad normal. En nuestro proyecto el write-path en contención más obvio es el inventario: varias operaciones pueden reservar el mismo artículo. Por lo tanto, el checklist debe responder rápidamente a la pregunta: ¿estamos protegidos contra lost update y no estamos convirtiendo la BD en un campo de batalla de bloqueos?

Red flag: write-path sobre datos en contención sin @Version.
Si el PR modifica availableQty/reservedQty y la entidad no tiene versión, casi con seguridad obtendrás una pérdida silenciosa de actualizaciones en operaciones paralelas.

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

@Entity
class InventoryItem {
    @Id
    private Long id;

    // Optimistic locking: Hibernate agregará una verificación de versión en el UPDATE
    @Version
    private long version;
}

Pregunta del checklist: «¿hay versioning donde son posibles actualizaciones paralelas?».

Red flag: bloqueo pesimista sin conciencia del costo y del límite.
El bloqueo pesimista es necesario, pero es una herramienta pesada: debe estar en una transacción corta y con una política de espera clara. Si el PR añade PESSIMISTIC_WRITE «para estar seguro», preguntamos: «¿por qué el optimista es insuficiente? ¿qué tan frecuentes son los conflictos?».

import java.util.Optional;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;

interface InventoryItemRepository extends JpaRepository<InventoryItem, Long> {

    // Bloqueo pesimista: bloqueamos la fila en la BD (costoso, pero a veces necesario)
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<InventoryItem> findLockedById(Long id); // Red flag si no hay motivo
}

Red flag: acciones largas dentro de una sección de bloqueo.
Si ves que después de tomar el bloqueo dentro de una transacción se hace «algo largo» (cálculos complejos, esperas, bucles grandes), es motivo para detenerse. Incluso si es un proyecto educativo, el hábito es peligroso.

import org.springframework.transaction.annotation.Transactional;

@Transactional
public void reserve(long itemId, int qty) {
    // Tomamos el bloqueo: comienza la sección crítica
    InventoryItem item = inventoryRepository.findLockedById(itemId).orElseThrow();

    // Red flag: acción larga bajo bloqueo aumenta la probabilidad de contención/deadlock
    slowBusinessLogic();

    // Es mejor mantener solo la parte mínima necesaria bajo el bloqueo
    item.reserve(qty);
}

En la revisión la pregunta: «¿se puede acortar la sección crítica?». A menudo la respuesta es sí.

8. Operaciones bulk y read-only: stale state

Este bloque es necesario porque bulk update/delete es una «salida consciente del modelo de entidades» y tiene un efecto secundario: el persistence context no se sincroniza automáticamente. Incluso si el código parece «lo actualizamos todo», los objetos managed pueden quedarse con valores antiguos. Y esto no es un bug de Hibernate, es el precio del enfoque bulk.

Red flag: @Modifying bulk update y continuación del trabajo con entidades previamente cargadas.
Si el servicio primero hace find(), luego un bulk update, y después lee campos del objeto managed — es casi un libro de texto sobre stale persistence context.

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

interface ProductRepository {
    // Bulk update: omitimos el persistence context y el dirty checking
    @Modifying
    @Query("update Product p set p.status = :status where p.deleted = false")
    int changeStatusForCatalog(ProductStatus status); // Después de esto, las entidades managed pueden quedar stale
}

Pregunta del checklist para el lugar donde se usa bulk: «¿qué ocurre con el persistence context después del bulk? ¿se limpia? ¿recargamos los datos?». Aquí es importante que no preguntamos «cómo es correcto», sino «cómo controlas eso».

Red flag: transacción read-only, pero dentro se mutan entidades.
Sí, suena a «quién hace eso», pero en la vida real ocurre más a menudo de lo que nos gustaría. Alguien puso readOnly=true como «optimización», y luego agregó setX() y se olvidó.

import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
public void accidentalWrite(long productId) {
    // Transacción read-only: asumimos «solo lectura»
    Product p = productRepository.findById(productId).orElseThrow();

    // Red flag: mutación dentro de un escenario read-only crea sorpresas
    p.setName("Oops");
}

En la revisión la pregunta: «¿es realmente un use case de solo lectura?». Si es escritura, hay que hacer honestamente una transacción de escritura y no disfrazarla.

9. Comentarios constructivos en ORM-review

El checklist más potente puede convertirse en una herramienta inútil si se usa como garrote. Especialmente en el tema de Hibernate, donde cada uno tiene sus traumas: alguien una vez vio LazyInitializationException y ahora le tiene miedo a la palabra LAZY, alguien sufrió N+1 y ahora pone JOIN FETCH incluso al leer un catálogo de «ciudades de Mesopotamia» (incluyendo ciudades que aún no existen). Por eso la forma importa.

Un buen comentario de ORM-review generalmente tiene la forma de pregunta + razón + sugerencia. La pregunta hace que la discusión sea respetuosa y verifica el contexto (tal vez saveAndFlush() realmente sea necesario). La razón explica el riesgo (flush timing, stale state, N+1). La sugerencia ofrece una salida (proyección, EntityGraph, find + mutate, @Version, clear después de bulk). Y adicionalmente es útil insinuar que la solución debe ser verificable, pero sin entrar en detalles: «asegurémonos por el comportamiento», no «yo lo digo».

Aquí hay una pequeña tabla de cómo podría sonar:

Cómo no escribirlo Cómo mejor
«¿Por qué está saveAndFlush()? Quítalo.» «Veo saveAndFlush(). ¿Necesitamos un flush antes del final del método? Si no, podemos quitarlo y confiar en el dirty checking al hacer commit — habrá menos SQL inesperado.»
«Otra vez findAll()…» «findAll() + navegación por relaciones parece candidato a N+1. ¿Es un list-use case? Tal vez sea mejor una proyección/plan de fetch explícito para el escenario.»
«CascadeType.ALL es malo.» «Aquí hay CascadeType.ALL en ManyToOne. Customer es una entidad compartida, la cascada puede causar efectos secundarios inesperados. ¿Qué ciclo de vida querías expresar?»

10. Errores típicos al usar un ORM-checklist

Error n.º 1: convertir el checklist en una religión en lugar de una herramienta.
Cuando un desarrollador ve saveAndFlush() y automáticamente escribe «prohibido», deja de pensar en las razones. Y a veces la razón existe: por ejemplo, se necesita que las restricciones de la BD se activen antes de la siguiente consulta, o se necesita obtener el id bajo cierta estrategia. El checklist debe obligar a hacer preguntas, no a apagar el cerebro.

Error n.º 2: revisar solo los repositorios e ignorar el escenario del servicio.
Los problemas de Hibernate no viven en «métodos del repositorio por sí solos», sino en la combinación: use case del servicio → transacción → lectura/escritura → navegación por el grafo. Si la revisión se limita a los archivos del Repository, te perderás la mitad de los riesgos reales, especialmente transacciones con ámbito excesivo y recorridos lazy ocultos.

Error n.º 3: buscar red flags solo en las anotaciones de las entidades.
Por supuesto, EAGER, CascadeType.ALL y mappedBy extraños son importantes. Pero muchos red flags viven en el «código Java normal»: stream().map(o -> o.getCustomer()...), toString() con colecciones, mappers que recorren todo el grafo, bulk update sin limpiar el contexto. El checklist debe mirar el código completo.

Error n.º 4: mezclar requisitos de lectura y escritura en una misma papilla.
Para el read-path es normal querer proyecciones y un mínimo de entidades managed. Para el write-path es normal querer una entidad raíz managed y find + mutate. Si en la revisión usamos el mismo conjunto de exigencias tanto para lecturas como para escrituras, aparece el caos: el autor no entiende qué se espera de él y la capa de datos se vuelve inconsistente.

Error n.º 5: olvidar que un red flag no es un bug, pero casi siempre es una deuda.
A veces un PR con red flags «funciona». Pero un red flag significa que el comportamiento es o costoso, o frágil, o impredecible. Si lo dejas pasar sin discusión, no solo fusionas código — fusionas futuras guardias nocturnas con logs de SQL. Hibernate, sin duda, es una herramienta potente, pero no está obligado a ser tu interlocutor nocturno.

1
Tarea
Hibernate deep-dive, nivel 30, lección 2
Bloqueada
REST-checklist para revisión de ORM
REST-checklist para revisión de ORM
1
Tarea
Hibernate deep-dive, nivel 30, lección 2
Bloqueada
Sincronizaci\u00f3n bidireccional de la relaci\u00f3n entre pedido y elemento
Sincronizaci\u00f3n bidireccional de la relaci\u00f3n entre pedido y elemento
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION