spring-data-neo4j icon indicating copy to clipboard operation
spring-data-neo4j copied to clipboard

Add global node filter / query restriction support

Open dpkass opened this issue 5 months ago • 2 comments

Problem

Common cases like soft delete and multi-tenancy require adding the same WHERE clause to every query. SDN has no built-in way to apply a predicate to all generated queries for an entity (derived queries, counts/exists, relationship loads). This is easy to miss and error-prone.

Proposal

Introduce an opt-in, entity-scoped restriction that SDN appends to generated Cypher.

@Node("Organization")
@QueryRestriction("deletedAt IS NULL")
@QueryRestriction("tenantId = $tenantId")       // maybe even allow SpEL
class Organization { Instant deletedAt; String tenantId; }
  • Support simple parameters via a small SPI:
@Bean
QueryRestrictionParameters params(TenantResolver r) {
  return () -> Map.of("tenantId", r.currentTenantId());
}
  • Allow per-method opt-out:
@IgnoreQueryRestrictions
@Query("MATCH (o:Organization) RETURN o")
List<Organization> adminListAll();

Scope

Applied to derived queries (findAll, findById, count, exists, paging) and relationship loads. Does not change custom @Query unless opted in/out as above.

Workarounds today

Duplicate predicates everywhere or change labels on delete—both brittle.

Prior art

Hibernate/JPA’s @SQLRestriction (and now deprecated predecessor @Where) provide similar per-entity SQL predicates.

dpkass avatar Aug 14 '25 17:08 dpkass

Sorry, I just realized this is a duplicate of https://github.com/spring-projects/spring-data-neo4j/issues/1910

Building upon what has been said there I have a few ideas and suggestions, that work alongside the idea of **acknowledging “graph is the truth”, and that global filters on entity loads are risky—filtered relations could be pruned on save. Still, teams need soft-delete/tenant scoping without repeating predicates. A few read-safe, opt-in ideas: 1. Projection-only filters (read-only): Apply filters only when returning projections/DTOs, never entities. 2. Per-query filter hint + guardrails: repo.withFilter("deletedAt IS NULL")… marks results as partial and blocks save(..) unless explicitly overridden. 3. Optional MERGE-style save mode: Per entity/relationship save strategy that doesn’t delete missing, filtered-out relations. 4. Repository-level default predicate (read paths): Append a predicate to derived/pageable methods when the return type isn’t an entity. 5. Built-in soft-delete helper: A standard soft-delete facility (props/labels + fragments) for “active” reads and softDelete/restore writes.

All are opt-in and avoid changing default replacement semantics—just reduce boilerplate for common read scenarios. Happy to hear if any of these shapes are acceptable.

dpkass avatar Aug 14 '25 17:08 dpkass

Thanks for the input and the idea. In contrast to a few years ago, we are not completely against this. But we need some time to see what options we have today and how we could create a solid experience when it comes to writing entities loaded this way.

meistermeier avatar Aug 20 '25 07:08 meistermeier