skip to content

Explain @SqlGroup and @SqlMergeMode: how do class-level and method-level @Sql scripts combine?

level: seniorimportance: should knowfreq 35%

answer

  1. @Sql @Repeatable -> @SqlGroup container
  2. default OVERRIDE: method replaces class
  3. @SqlMergeMode(MERGE) runs both
  4. class scripts first, then method
  5. method @SqlMergeMode overrides class setting

basics

~10 s

@SqlGroup is the container for multiple @Sql annotations (created automatically since @Sql is repeatable). By default, method-level @Sql OVERRIDES class-level @Sql. @SqlMergeMode(MERGE) changes that so method scripts run in addition to the class-level ones.

solid answer

~30 s

`@SqlGroup` is the repeatable-container annotation that aggregates several `@Sql` declarations; you rarely write it by hand because stacking multiple `@Sql`s produces it implicitly. `@SqlMergeMode` controls how method-level `@Sql` interacts with class-level `@Sql`. The default is `MergeMode.OVERRIDE`: if a method has any `@Sql`, the class-level `@Sql` is ignored for that method. Annotate the class (or method) with `@SqlMergeMode(MergeMode.MERGE)` to instead run class-level scripts *and then* method-level scripts. This is powerful for a class-wide baseline seed (`@Sql("/base.sql")` on the class) plus per-method extras, without repeating the base in every method. `@SqlMergeMode` at method level overrides the class-level merge mode for that method.

code

java · 12 lines
java
@SpringBootTest
@Sql("/base-schema-data.sql")          // class-wide baseline
@SqlMergeMode(SqlMergeMode.MergeMode.MERGE)   // don't let methods drop the baseline
class CatalogTest {

    @Test
    @Sql("/extra-products.sql")        // base runs first, THEN this
    void seesBaselinePlusExtras() { }

    @Test                              // no method @Sql -> just the baseline
    void seesOnlyBaseline() { }
}

go deeper

for a junior

Vaguely knows you can have multiple @Sql annotations.

for a middle

Knows @SqlGroup is the container and multiple @Sql stack.

for a senior

Explains OVERRIDE-by-default vs MERGE, placement on class/method, and script ordering.

for a principal

Designs baseline-plus-delta fixture strategies and knows the silent-drop pitfall to guard against in shared test base classes.

## @SqlGroup `@org.springframework.test.context.jdbc.SqlGroup` is simply the **container annotation** for `@Sql`. Because `@Sql` is declared `@Repeatable(SqlGroup.class)`, writing several `@Sql`s on one element is equivalent to one `@SqlGroup({ @Sql(...), @Sql(...) })`. You'd only write `@SqlGroup` explicitly in code that must be pre-repeatable style, or to be very explicit. Functionally the two forms are identical. ```java @SqlGroup({ @Sql("/seed.sql"), @Sql(scripts = "/cleanup.sql", executionPhase = AFTER_TEST_METHOD) }) ``` is the same as stacking those two `@Sql`s. ## The override problem By default, **method-level `@Sql` overrides class-level `@Sql`**. So: ```java @Sql("/class-base.sql") // class level class MyTest { @Test @Sql("/method-extra.sql") // ONLY this runs, class-base.sql is skipped void t() {} } ``` That surprises people — the class-level baseline silently doesn't run for any method that has its own `@Sql`. ## @SqlMergeMode `@org.springframework.test.context.jdbc.SqlMergeMode` (added in Spring 5.2) fixes this. Its value is the enum `SqlMergeMode.MergeMode`: - `OVERRIDE` — the default behavior: method scripts replace class scripts. - `MERGE` — method-level `@Sql` scripts are **merged with** class-level ones. Class-level scripts run first, then method-level, within the same execution phase. You can place `@SqlMergeMode` on the class (sets the default for all methods) and/or on a method (overrides the class setting for that one method): ```java @Sql("/class-base.sql") @SqlMergeMode(MergeMode.MERGE) // class default: merge class MyTest { @Test @Sql("/extra.sql") void merged() { /* class-base.sql THEN extra.sql */ } @Test @SqlMergeMode(MergeMode.OVERRIDE) // back to override for this method @Sql("/standalone.sql") void overridden() { /* only standalone.sql */ } } ``` ## Ordering with MERGE Within a phase, class-level scripts execute **before** method-level scripts. Across phases, BEFORE and AFTER groups are handled separately as usual. ## Interaction with @SqlConfig A class-level `@SqlConfig` still provides parsing defaults regardless of merge mode; merge mode only governs which `@Sql` *script sets* apply, not config inheritance. ## Gotchas - Default is OVERRIDE — forgetting `@SqlMergeMode(MERGE)` silently drops class-level seeds for methods that add their own `@Sql`. - `@SqlMergeMode` only affects the relationship between class-level and method-level `@Sql`; multiple method-level `@Sql`s always all run (they don't override each other). - MERGE runs class scripts first; if a method needs to run *before* the class baseline, MERGE won't reorder that. ## When to use Use `MERGE` when you have a common class-wide fixture plus per-method deltas and want to avoid repeating the baseline. Keep `OVERRIDE` (default) when methods need fully independent data sets.

  • Without @SqlMergeMode, what happens to a class-level @Sql for a method that has its own @Sql?
    It is ignored — the default MergeMode.OVERRIDE means the method-level @Sql fully replaces the class-level scripts for that method.
  • Do you have to write @SqlGroup manually to have multiple @Sql annotations?
    No. @Sql is @Repeatable(SqlGroup.class), so stacking @Sql annotations implicitly produces an @SqlGroup; writing it explicitly is optional and equivalent.
  • With MERGE, in what order do class-level and method-level scripts run?
    Class-level scripts run first, then method-level scripts, within the same execution phase.

saying these in an interview costs you the question

  • Thinking the default is MERGE
  • Believing method-level @Sqls override each other (they don't — only class-vs-method overrides)
  • Assuming @SqlMergeMode reorders class scripts after method scripts

context