What do the existsBy, countBy, and deleteBy derived prefixes do, and what must you add for derived deletes to work correctly?
answer
- existsBy = cheap SELECT 1 probe
- countBy = SELECT count(*)
- deleteBy needs @Transactional
- derived delete = load-then-remove (callbacks fire)
- bulk @Modifying skips lifecycle/cascades
basics
~10 sexistsBy returns a boolean (efficient existence check), countBy returns a long count, and deleteBy/removeBy delete matching rows. Derived deletes are modifying queries, so they must run inside a transaction (@Transactional).
solid answer
~50 sBeyond `findBy`, the derivation engine supports operation prefixes: `existsBy...` returns `boolean` via a cheap existence probe (SELECT 1 / limited count), avoiding loading the entity; `countBy...` returns `long`; `deleteBy...` / `removeBy...` delete matching rows and return either `void` or the deleted count. The crucial difference: `deleteBy` is a **modifying** operation and requires an active transaction — call it from a `@Transactional` service, or annotate the repository method. There are also two DIFFERENT delete strategies. A derived `deleteBy...` (or the `deleteAll`-style calls) **loads the entities and removes them one by one**, so JPA lifecycle callbacks (`@PreRemove`) and cascades fire and the deleted count is accurate. A bulk `@Modifying @Query("delete ...")` issues a single SQL DELETE that bypasses the persistence context, callbacks, and cascades — faster but skips lifecycle and can leave the first-level cache stale. Choose based on whether cascades/callbacks matter.
code
java · 23 linespublic interface AccountRepository extends JpaRepository<Account, Long> {
boolean existsByEmail(String email); // SELECT 1 ... LIMIT 1
long countByStatus(Status status); // SELECT count(*)
// Derived delete: loads matching entities, fires @PreRemove + cascades.
// Returns how many were deleted. MUST run in a transaction.
long deleteByStatus(Status status);
// Bulk delete: one SQL DELETE, skips callbacks/cascades.
@Modifying(clearAutomatically = true)
@Query("delete from Account a where a.status = :s")
int bulkDeleteByStatus(@Param("s") Status status);
}
@Service
class AccountService {
private final AccountRepository repo;
AccountService(AccountRepository repo) { this.repo = repo; }
@Transactional // required for the derived delete
public long purge(Status s) { return repo.deleteByStatus(s); }
}go deeper
Know existsBy returns boolean, countBy returns a count, deleteBy deletes.
Add that deleteBy requires @Transactional and existsBy avoids loading the entity.
Contrast derived load-then-remove (callbacks + cascades) with bulk @Modifying delete (single SQL, skips lifecycle) and the stale-context / clearAutomatically issue.
Reason about mass-delete performance, FK ordering, cascade correctness, and choosing per-operation between derived and bulk semantics.
## The operation prefixes Query derivation isn't only `findBy`. The subject keyword selects the operation: ### existsBy `boolean existsByEmail(String email)` — returns whether any row matches. Spring implements it as an **existence probe** (e.g. `SELECT 1 ... LIMIT 1` or a limited count), so it does **not** hydrate the entity. Prefer it over `findBy...().isPresent()`, which loads the whole row. ### countBy `long countByStatus(Status s)` — emits `SELECT count(*) ... WHERE`. Returns a `long`. ### deleteBy / removeBy `deleteByStatus(Status s)` — deletes matching rows. `remove` and `delete` are synonyms. Return type may be `void`, `long` (number deleted), or a `List<T>`/`int` in some versions. ## Transactions are mandatory for delete Repository **read** methods get a default read-only transaction from `SimpleJpaRepository`, but a **derived delete** mutates data. If no transaction is active you get `TransactionRequiredException` (or `InvalidDataAccessApiUsageException`). Fixes: - Call it from a `@Transactional` service method (normal case), OR - Put `@Transactional` on the repository method itself. ## Two very different delete mechanics This is the senior-level point: | | Derived `deleteBy...` | `@Modifying @Query("delete ...")` | |---|---|---| | Mechanism | Load matching entities, then `EntityManager.remove` each | Single bulk SQL `DELETE` | | Lifecycle callbacks (`@PreRemove`/`@PostRemove`) | Fire | **Skipped** | | Cascades / orphan removal | Applied | **Skipped** | | Performance | N selects + N deletes | One statement, fastest | | Persistence context | Kept consistent | Can go **stale**; add `clearAutomatically`/`flushAutomatically` | So a derived `deleteByAuthorId` on an `Author` with cascading children will delete children too and run callbacks; a bulk `@Modifying` delete will violate FK constraints unless you also delete children explicitly. ## Return-count nuance Derived deletes return the **actual number of entities removed** (because they load-then-remove). Bulk `@Modifying` deletes return the JDBC row-update count. They usually agree but can differ when cascades add rows the derived path counts differently. ## Gotchas - `existsBy` returning `Boolean` vs primitive `boolean` — both fine, but null never happens (exists is always true/false). - Combining `@Modifying(clearAutomatically = true)` matters so the L1 cache doesn't serve deleted entities afterward. - Derived delete of a large table is slow (loads every row) — for mass deletes prefer bulk `@Modifying`. - `deleteBy` still needs the criteria to resolve to real properties, same fail-fast rules as `findBy`.
- Why prefer existsByX over findByX().isPresent()?existsBy runs a lightweight existence probe (SELECT 1 / limited count) and never hydrates the entity, while findBy loads the full row and maps it into a managed object just to check presence.
- Your derived deleteByAuthorId ran and cascaded to child comments, but a colleague's @Modifying delete threw a foreign-key violation. Why?The derived delete loads each Author and removes it via the EntityManager, so JPA cascade/orphanRemoval deletes the children first. The bulk @Modifying DELETE bypasses the persistence context and cascades, hitting the FK constraint because children still reference the row.
- After a bulk @Modifying delete, a subsequent find returns a stale entity. Fix?The bulk delete didn't touch the first-level cache. Use @Modifying(clearAutomatically = true) (and often flushAutomatically = true) so the persistence context is cleared.
saying these in an interview costs you the question
- Saying existsBy loads the entity
- Forgetting derived deletes need a transaction
- Claiming @Modifying bulk delete fires @PreRemove and cascades
- Assuming derived delete and bulk delete are interchangeable for entities with cascades