skip to content

Why must you flush to surface a database constraint violation inside a @DataJpaTest, and how does that interact with the test's transaction?

level: seniorimportance: must knowfreq 35%

answer

  1. write-behind: SQL deferred to flush/commit
  2. @DataJpaTest rolls back, never commits
  3. no commit -> constraints never checked
  4. flush forces SQL inside open tx -> DataIntegrityViolationException
  5. deferred constraints & H2 fidelity caveats

basics

~20 s

@DataJpaTest runs each test in a transaction that rolls back and never commits. JPA defers INSERT/UPDATE SQL until flush or commit, so with no commit the DB never checks constraints — unless you flush explicitly. flush() forces the SQL now, so unique/not-null/FK violations throw where you can assert on them.

solid answer

~50 s

Two facts combine. First, JPA uses write-behind: persist/merge only schedule SQL; it isn't sent until flush() or commit. Second, @DataJpaTest wraps each test in a transaction that is rolled back at the end — it never commits. So the usual moment a database would enforce a constraint (commit-time SQL) never arrives, and a test expecting a unique-key violation would pass falsely. Calling flush() — directly, or via persistAndFlush/persistFlushFind — pushes the pending INSERT to the database inside the still-open transaction, so the DB runs its checks now. A violation then surfaces as a DataIntegrityViolationException (Spring translates the JPA PersistenceException). You assert with assertThatThrownBy(() -> em.flush()). The transaction still rolls back afterward, so nothing leaks between tests. Caveat: some constraints are DEFERRED to commit and won't fire on flush, and H2's enforcement can differ from your production DB.

code

java · 16 lines
java
@DataJpaTest
class UniqueEmailConstraintTest {

    @Autowired TestEntityManager em;

    @Test
    void duplicateEmailViolatesUniqueConstraint() {
        em.persistAndFlush(new User("[email protected]"));

        // Without the flush inside persistAndFlush, the rollback-only
        // transaction would never send this INSERT and the test would
        // pass even though production rejects the duplicate.
        assertThatThrownBy(() -> em.persistAndFlush(new User("[email protected]")))
            .isInstanceOf(DataIntegrityViolationException.class);
    }
}

go deeper

for a junior

Know you must flush to make a constraint error appear and the test uses an in-memory DB that rolls back.

for a middle

Explain write-behind plus rollback and name DataIntegrityViolationException.

for a senior

Distinguish bean-validation vs DB constraints, flush vs commit, and Spring exception translation.

for a principal

Reason about deferred constraints, H2-vs-production fidelity, Testcontainers/replace=NONE, and implicit auto-flush pitfalls.

## The two mechanisms in play ### 1. Transactional write-behind (deferred SQL) When you `persist`, `merge`, or mutate a managed entity, JPA/Hibernate records the change in the persistence context but **delays the actual INSERT/UPDATE/DELETE**. The SQL is sent to the database only at a **flush** — which happens automatically before certain queries, on `EntityManager.flush()`, or at **transaction commit**. This batching is a performance feature (fewer round-trips, statement batching). ### 2. `@DataJpaTest` transaction semantics `@DataJpaTest` is meta-annotated `@Transactional`, and the Spring TestContext framework makes test transactions **roll back by default**. So each test method: opens a transaction, runs, and **rolls back** — there is **no commit**. ### Why the two together create a trap Because there's no commit, the *natural* flush-at-commit never happens. If your Arrange does `em.persist(duplicate)` and you expect a unique-constraint error, the INSERT is still sitting in the persistence context when the test ends and gets discarded on rollback. **The database never saw it, so it never complained** — your test passes even though production would fail. This is a classic false green. ### The fix: flush explicitly `flush()` forces the queued SQL to the database **now**, inside the open transaction. The database runs its integrity checks at that point. If a **UNIQUE**, **NOT NULL**, or **FOREIGN KEY** constraint is violated, the driver throws, Hibernate wraps it, and Spring's exception translation surfaces it as a `org.springframework.dao.DataIntegrityViolationException` (a subclass of `DataAccessException`). You can now assert on it: ```java assertThatThrownBy(() -> entityManager.persistAndFlush(dupUser)) .isInstanceOf(DataIntegrityViolationException.class); ``` Crucially, flush is **not** commit — the transaction is still open and will still roll back, so the offending row (and everything else) disappears after the test. You get to observe the violation without polluting other tests. ## Bean validation vs database constraints There are two different layers that can reject bad data: - **Bean Validation** (`@NotNull`, `@Size`, Jakarta Validation): Hibernate runs these on flush by default and throws `jakarta.validation.ConstraintViolationException` (or `TransactionSystemException` on commit) — no DB round-trip needed. - **Database constraints** (schema `NOT NULL`, `UNIQUE`, `FK`): only the database can enforce these, and only when the SQL reaches it — hence the flush requirement. Know which one you're testing; the exception type differs. ## Gotchas and edge cases - **Deferred constraints**: A constraint declared `DEFERRABLE INITIALLY DEFERRED` (Postgres) is only checked at **commit**, not on flush — so `flush()` won't trigger it and a rollback-only test can't observe it easily. - **Embedded H2 vs production DB**: `@DataJpaTest` defaults to an in-memory database. H2's constraint handling, case sensitivity, and error messages differ from Postgres/MySQL; add `@AutoConfigureTestDatabase(replace = Replace.NONE)` (often with Testcontainers) when constraint fidelity matters. - **Auto-flush before queries**: a JPQL/native query in the same transaction can trigger an implicit flush, sometimes surfacing the violation 'by accident' — relying on that is fragile; be explicit. - **clear() ≠ flush()**: `clear()` detaches entities (empties the first-level cache); it does not send SQL. Don't confuse the two. ## When to use Use an explicit `flush()` (or `persistAndFlush`/`persistFlushFind`) whenever a test's correctness depends on the database actually executing the write: verifying a unique index, a not-null column, a FK, or a generated key. For pure read tests where you've already inserted valid data, a single flush after Arrange (or `clear()` to force reload) is usually enough.

  • If you assert a unique violation with plain persist (no flush), what happens?
    The test likely passes for the wrong reason: the INSERT is deferred and the rollback-only transaction discards it before commit, so the database never runs the unique check. You get a false green. You must flush to make the DB enforce the constraint.
  • What exception type indicates a DB constraint violation here, and who produces it?
    org.springframework.dao.DataIntegrityViolationException. The JDBC driver throws a SQL exception on flush, Hibernate wraps it, and Spring's persistence exception translation converts it into the DataAccessException hierarchy.
  • Why might a Postgres deferred foreign key not surface on flush?
    A DEFERRABLE INITIALLY DEFERRED constraint is only validated at commit time, not on flush. Since @DataJpaTest rolls back, that check never runs, so you can't observe it with a normal flush-based assertion.

saying these in an interview costs you the question

  • Asserting constraint violations without any flush and trusting the green result
  • Confusing flush() with commit() — thinking flush persists data beyond the test
  • Assuming H2's constraint behavior matches the production database
  • Expecting a deferred (commit-time) constraint to fire on flush
  • Mixing up bean-validation ConstraintViolationException with DB-level DataIntegrityViolationException

context