1. El papel del flush() explícito
El flush() explícito se necesita principalmente como punto de control de la sincronización. Permite no esperar a que Hibernate elija el momento por sí mismo. Para una operación de negocio habitual normalmente no es necesario, pero en el diagnóstico sin ese punto rápidamente empieza el caos: no está claro qué bloque de código exactamente generó el SQL, dónde apareció la restricción de BD y si estás verificando la base o todavía mirando la misma copia managed.
El flush() explícito no es el botón "hazlo todo más fiable" y no es un sustituto del commit. Es una herramienta de observación y diagnóstico: le dices explícitamente a Hibernate "sincroniza el contexto exactamente aquí, para que yo vea el SQL, capture el error de BD más cerca del lugar del problema o relea honestamente el estado de la base".
flush(): memoria, BD y commit
Aquí basta con una fórmula simple: flush() envía los INSERT/UPDATE/DELETE acumulados a la BD dentro de la transacción actual, y el commit termina la transacción y fija los cambios definitivamente. Por eso después del flush() el SQL ya puede verse, el error de constraint ya puede capturarse, pero el rollback todavía puede cancelar el resultado.
Esa distinción es suficiente para no confundir el flush() diagnóstico con la fijación final de la transacción. Más adelante precisamente será útil esa frontera: flush()+clear() ayuda a leer honestamente la BD, y el flush() explícito — a capturar errores antes.
2. El binomio flush() + clear() para la lectura honesta de la BD
Si quieres verificar que "en la base realmente quedó así", hay una trampa sutil: dentro del persistence context Hibernate intenta ser útil y económico. Si haces find(Product.class, 1L) dos veces en una transacción, la segunda vez el SQL puede no ir en absoluto — obtendrás el mismo objeto managed del first-level cache. Y eso es correcto: el ORM protege el identity map y garantiza "una fila — un objeto".
Por eso si quieres ver conscientemente la diferencia entre "en memoria hay el nombre nuevo" y "en la base hay el nombre nuevo", muy a menudo se necesita el binomio: primero flush(), luego clear(), luego la relectura. El sentido es simple: flush() empuja los cambios a la BD, y clear() tira la copia managed, para que el siguiente find() realmente vaya a la base.
El mini-escenario con Product dentro de una transacción puede tener este aspecto:
import jakarta.persistence.EntityManager;
// 1) Leemos la entidad: se vuelve managed en el persistence context actual
Product p = entityManager.find(Product.class, 1L);
// 2) Cambiamos el campo: por ahora es solo un cambio en memoria (se marca como dirty)
p.setName("Phone Pro");
// 3) Empujamos explícitamente los cambios acumulados al SQL ahora mismo
entityManager.flush(); // en el log SQL aparece UPDATE
// 4) Tiramos los objetos managed del persistence context,
// para que la siguiente lectura no devuelva la misma referencia Java del cache nivel 1
entityManager.clear();
// 5) Ahora esto será una relectura de la BD (SELECT), no devolución desde el first-level cache
Product reread = entityManager.find(Product.class, 1L);
Y el detalle importante que es fácil perderse: la variable p después del clear() no desaparece a ningún sitio. Es el mismo objeto Java, pero para el persistence context actual ya es detached. Si después del clear() sigues llamando a p.setName(...), Hibernate no lo verá: el dirty checking funciona solo con las entidades managed, hasta que no relea el objeto de nuevo o no lo devuelvas explícitamente al contexto.
Aquí hay un efecto psicológico importante: después del clear() empiezas a confiar solo en lo que puedes volver a leer de la base. Y eso es un hábito muy útil cuando depuras "por qué las consultas se comportan raro" o "por qué en el log apareció el UPDATE inesperado".
En nuestro Commerce Persistence Lab ese binomio es conveniente usarlo en pequeños escenarios de laboratorio — por ejemplo, en un componente que arranca con la aplicación en el perfil dev y hace un experimento comprensible mientras miras el SQL trace. Pero incluso sin runners especiales es un recurso excelente para el análisis en el depurador.
3. clear() sin flush(): cómo "borrar" los cambios
Si el binomio flush() + clear() ayuda a releer honestamente el estado de la BD, entonces el binomio inverso clear() sin flush() es una forma de hacer como que los cambios nunca existieron. A veces eso es útil (por ejemplo, si quieres deliberadamente tirar todos los cambios managed acumulados), pero más a menudo es una trampa: la persona hace clear() "para releer", y luego se sorprende de que la base "no se actualizó".
Imagina que cambiaste la entidad managed, pero todavía no hubo ni el flush explícito ni el automático. El cambio vive solo en el persistence context. Si haces clear(), tiras ese contexto. Hibernate ya no está obligado a recordar tu cambio, porque tú mismo le pediste "olvídalo todo". Y la base, naturalmente, no se enteró de nada, porque el SQL nunca se envió.
He aquí un ejemplo corto que es útil recordar como anti-encantamiento:
import jakarta.persistence.EntityManager;
// 1) La entidad es managed
Product p = entityManager.find(Product.class, 1L);
// 2) El cambio solo está en memoria (todavía no sincronizado con la BD)
p.setName("Draft name");
// 3) Reseteamos el persistence context: Hibernate olvida los cambios acumulados
entityManager.clear();
// 4) Volvemos a leer de la base: obtenemos el valor antiguo, porque el SQL UPDATE no se envió
Product reread = entityManager.find(Product.class, 1L);
Como resultado reread.getName() devolverá el nombre antiguo de la BD, y eso será absolutamente honesto. No hay ninguna mística: no "perdiste los datos", perdiste el estado no sincronizado del persistence context.
Subrayaré por separado: clear() es una operación sobre el contexto, no sobre la base. Si lo percibes como "recargar los datos de la base", casi seguro sacarás conclusiones incorrectas. Para "recargar" existe otra herramienta — refresh(), y para "empujar" — flush().
4. Detección temprana de errores de BD a través del flush()
Ahora la razón más práctica por la que el flush() gusta en el diagnóstico: obliga a que el SQL vaya a la base ahora mismo, lo que significa que la base puede ahora mismo verificar las restricciones. Eso es útil cuando el método de servicio es largo, hace muchos pasos, y no quieres enterarte del error de unicidad o de NOT NULL solo al final en el commit, cuando "todo ya ocurrió" y el lugar del error está difuminado.
En nuestro proyecto hay al menos un candidato obvio: la unicidad de Product.sku. Si accidentalmente creas dos productos con el mismo SKU dentro de una transacción, hasta el flush todo puede parecer "normal": tienes dos objetos Java, ambos managed, ambos bonitos. Pero PostgreSQL en algún momento dirá: "Para. La unicidad está violada".
Con el flush explícito puedes ver eso más cerca del lugar donde creaste el problema:
import jakarta.persistence.EntityManager;
// 1) Creamos y guardamos el primer producto en el persistence context
Product a = new Product();
a.setSku("SKU-123");
entityManager.persist(a); // el SQL INSERT puede estar pospuesto hasta flush/commit
// 2) Creamos el segundo producto con el mismo SKU (el problema todavía no está obligado a manifestarse)
Product b = new Product();
b.setSku("SKU-123");
entityManager.persist(b);
// 3) Enviamos forzosamente el SQL a la BD: aquí la base verificará UNIQUE y puede fallar
entityManager.flush(); // aquí es probable la violación de constraint
El sentido no es "curar" la unicidad con el flush, sino recibir la señal antes y localizar el error. En la depuración es muy conveniente: pones flush() después de un bloque potencialmente peligroso (creación de entidad, creación de relación, cambio del campo importante) y ves de inmediato — ¿la base está de acuerdo o no?
Hay otro matiz importante: si el error ocurre en el flush, eso igualmente no significa que parte de los datos "ya se guardó". En la mayoría de los casos la transacción se marcará como rollback-only y todo hará rollback. Pero para el diagnóstico ganas lo principal: capturas el problema no "a la salida del método", sino casi en el punto donde se creó.
5. Verificación a través de consulta a la BD después del flush()
A veces en los escenarios educativos y de depuración se quiere hacer algo así: "Cambié el nombre del producto, y ahora directamente dentro de la misma transacción quiero verificar que en la tabla product ya hay el nombre nuevo". Sí, Hibernate en el modo AUTO a menudo de todas formas hará el flush antes de la consulta que se superpone, pero apoyarse en "a menudo" — mal estilo en el diagnóstico. Si te importa que el SQL ya definitivamente se fue, hazlo explícitamente.
Por ejemplo, quieres verificar después de cambiar el nombre cuántas filas ya corresponden al nuevo valor:
import jakarta.persistence.EntityManager;
// 1) Leemos la entidad y la cambiamos en memoria
Product p = entityManager.find(Product.class, 1L);
p.setName("Phone Pro");
// 2) Garantizamos que el UPDATE ya se envió a la BD (dentro de la transacción actual)
entityManager.flush();
// 3) Hacemos la consulta directamente a la tabla, para "preguntarle a la base",
// no obtener el valor del persistence context
Number count = (Number) entityManager
.createNativeQuery("""
select count(*)
from product
where name = 'Phone Pro'
""")
.getSingleResult();
// 4) Dentro de la transacción PostgreSQL ve los cambios después del flush
System.out.println("count = " + count); // esperamos, por ejemplo: count = 1
Aquí usamos deliberadamente el SQL nativo para "preguntar directamente a la tabla", sin participación del persistence context. Dentro de la transacción PostgreSQL ve los cambios ya enviados, por eso el resultado de la consulta debe corresponder a lo que acabas de escribir (de nuevo: mientras la transacción no haya hecho rollback).
Ese es un buen recurso cuando investigas el comportamiento de los flush modes. Por ejemplo, si cambiaste el flush mode a COMMIT o MANUAL, la consulta JPQL que se superpone puede comportarse de otra manera, y quieres hacer el comportamiento máximamente explícito: "primero flush, luego la consulta". En esos experimentos flush() es como una lámpara brillante en el cuarto oscuro: no repara los muebles, pero permite finalmente ver qué exactamente tienes torcido.
6. El flush() apropiado e innecesario
Después de todo lo dicho es fácil enamorarse del flush() y empezar a insertarlo cada dos pasos. En un ejemplo educativo corto eso realmente da una sensación agradable de control: siempre ves el SQL, todo ocurre "de inmediato". Pero eso tiene un precio. El flush() no es una operación gratuita: lanza todo el procedimiento del flush, puede enviar un paquete de SQL, puede verificar las restricciones antes de tiempo, puede desplazar el orden de las operaciones a donde no lo esperabas, y en general hace la ejecución más ruidosa y pesada.
La actitud apropiada hacia el flush() es esta: es una herramienta de control puntual. Es útil en los laboratorios, en el diagnóstico, en algunos lugares del código donde hay que ver el error de BD antes, y en los tests donde quieres hacer el estado de la BD observable antes del final del método. Pero si el flush() se convierte en un ritual obligatorio "después de cada cambio", empiezas a luchar no con el problema, sino con tu propia desconfianza en la unit of work.
El "marcador" muy simple del código sano: en un método de servicio habitual normalmente no estás obligado a llamar al flush() manualmente. Hibernate lo hará solo antes del commit y en los momentos necesarios para la corrección de las consultas (dependiendo del flush mode). Y en el código que investiga el comportamiento del ORM (nuestro curso trata exactamente de eso), el flush() es un excelente "conmutador de fases" que hace los experimentos reproducibles.
Y lo último que conviene decir de nuevo, porque el cerebro ama olvidarlo: el flush() no sustituye al commit. Si pusiste el flush() y luego lanzaste una excepción — no "guardaste a medias", simplemente enviaste el SQL dentro de la transacción que luego hizo rollback. Eso es normal, eso es esperado y esa es una de las fuentes más frecuentes de conclusiones incorrectas de los principiantes.
7. Errores típicos al trabajar con flush()
Aquí es especialmente útil fijar los errores, porque el flush() es una cosa traicionera: parece el "botón de guardar", pero en la práctica es el "botón de enviar el SQL ahora mismo". La mayor parte de los problemas surge no de Hibernate, sino de que empezamos a atribuirle al flush lo que no prometió. A continuación el conjunto de los peores errores en los que tropiezan incluso los desarrolladores confiados, si hace mucho que no miraban el SQL trace.
Error #1: percibir el flush() como commit.
Si después del flush() ves el SQL en el log, es fácil decidir que "los datos ya están guardados definitivamente". En realidad el flush() simplemente envió los comandos a la BD, pero la transacción todavía puede hacer rollback. Eso es especialmente notable cuando al final del método lanzas una excepción y ves que en la base no cambió nada — y eso es correcto.
Error #2: hacer "verificación de la base" sin clear() y creer en el resultado.
El escenario frecuente: el desarrollador cambió la entidad, llamó a find() de nuevo, vio el nuevo valor y decidió que la base se actualizó. Pero dentro de una transacción find() puede devolver el mismo objeto managed del first-level cache. Si quieres releer honestamente de la BD, necesitas clear() (u otra forma de tirar la copia managed) antes de la relectura.
Error #3: llamar al clear() antes del flush() y perder los cambios no sincronizados.
Es un error sutil pero muy costoso, porque no siempre produce una excepción — simplemente "se come" tu trabajo. Los cambios en los objetos managed vivían solo en el persistence context. Hiciste clear() — y tú mismo le pediste a Hibernate que los olvidara. La base no recibió nada, porque el SQL no se envió.
Error #4: convertir el flush() en un ritual "después de cada llamada al setter".
Ese código normalmente aparece por el miedo: "¿y si Hibernate no guarda?". Pero Hibernate existe precisamente para eso: guardar automáticamente al final de la unit of work. El flush manual constante hace la ejecución más pesada, complica la depuración del orden de las operaciones SQL y puede provocar errores tempranos de restricciones donde deberían haber surgido solo en la fijación final de la operación de negocio.
Error #5: sacar conclusiones sin el log SQL y "adivinar" el momento del flush.
El flush() existe para la observabilidad. Si no miras el SQL trace (o al menos las Hibernate statistics), pensarás que el flush fue "aquí", y fue "allí", y todo el diagnóstico se irá al garete. En nuestro curso el hábito correcto es este: cualquier conversación sobre el flush se confirma con lo que realmente se fue a PostgreSQL.
GO TO FULL VERSION