Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Spring Data 4: Data Access Revisited

Spring Data 4: Data Access Revisited

The Spring Data team is preparing a new generation of the Data framework for 2025 and beyond: Spring Data 4 will ship with Jakarta EE 11 and Kotlin 2 baseline upgrades, introducing major new themes of which AOT Repositories, JSpecify annotations, Vector store support, and a major Spring Data JPA revision are only a selection. This talk gives an outlook on what’s to come and focuses on key design decisions and roadmap considerations.

Avatar for Mark Paluch

Mark Paluch

May 23, 2025
Tweet

More Decks by Mark Paluch

Other Decks in Programming

Transcript

  1. Agenda • Spring Data 3.5 (May Release Train 2025.0) •

    Spring Data 4.0 (November Release Train 2025.1)
  2. Spring Data 3.5 (May) • Vector Search: MongoDB, Cassandra •

    MongoDB: Queryable Encryption • Cassandra: Baseline upgrade, SAI Indexes • JPA: DTO Projections • JDBC & R2DBC: @Sequence support
  3. Spring Data 3.5 (May or: 2025.0.0) • Vector Search: MongoDB,

    Cassandra • MongoDB: Queryable Encryption • Cassandra: Baseline upgrade, SAI Indexes • JPA: DTO Projections • JDBC & R2DBC: @Sequence support
  4. Spring Data MongoDB 4.5 • Vector Search: Aggregation Stage •

    Queryable Encryption: Schema support, Experimental converter
  5. MongoDB Vector Search VectorSearchOperation $vectorSearch = VectorSearchOperation.search("cosine-index") .path("embedding") .vector(Vector.of(…)) .limit(Limit.of(10))

    .numCandidates(20) .searchType(SearchType.ANN) .withSearchScore(); AggregationResults<Document> results = template.aggregate(newAggregation($vectorSearch), Comment.class, Document.class);
  6. MongoDB Encryption • CSFLE: Client-Side Field Encryption (since 3.3) •

    Queryable Encryption: Range and Equality Queries (4.5) • Schema Support (Collection Creation) ⚠ Experimental Converters • Strict and Rigid
  7. MongoDB Queryable Encryption class Person { String id; // @ValueConverter(MongoEncryptionConverter.class)

    @Encrypted(algorithm = "Indexed") @Queryable(queryType = "equality", contentionFactor = 0) String ssn; // @ValueConverter(MongoEncryptionConverter.class) @Encrypted(algorithm = "Range") @Queryable(queryType = "range", contentionFactor = 0L, queryAttributes = "{ 'sparsity': 0 }") Integer age; // @ValueConverter(MongoEncryptionConverter.class) // @Encrypted + @Queryable @RangeEncrypted(contentionFactor = 0L, rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") Integer income; }
  8. Spring Data for Apache Cassandra 4.5 • Baseline Upgrade: Apache

    Cassandra 5 • Storage-Attached Indexes (SAI) • Vector Indexing via SAI • Vector Search: CQL support
  9. Cassandra Vector Search Vector vector = Vector.of(…); Columns columns =

    Columns.from("comment") .select("vector", it -> it.similarity(vector).cosine().as("similarity")); Query query = Query.select(columns).limit(3).sort(VectorSort.ann("vector", vector)); List<CommentSearch> result = template.query(Comments.class) .as(CommentSearch.class) .matching(query) .all(); @Table class Comments { @Id UUID id; String comment; @VectorType(dimensions = 5) @SaiIndexed Vector vector; } class CommentSearch { }
  10. Cassandra Vector Search Vector vector = Vector.of(…); Columns columns =

    Columns.from("comment") .select("vector", it -> it.similarity(vector).cosine().as("similarity")); Query query = Query.select(columns).limit(3).sort(VectorSort.ann("vector", vector)); List<CommentSearch> result = template.query(Comments.class) .as(CommentSearch.class) .matching(query) .all(); @Table class Comments { @Id UUID id; String comment; @VectorType(dimensions = 5) @SaiIndexed Vector vector; } class CommentSearch { String comment; float similarity; }
  11. Cassandra Vector Search Vector vector = Vector.of(…); Columns columns =

    Columns.from("comment") .select("vector", it -> it.similarity(vector).cosine().as("similarity")); @Table class Comments { } class CommentSearch { } Query query = Query.select(columns).limit(3).sort(VectorSort.ann("vector", vector)); List<CommentSearch> result = template.query(Comments.class) .as(CommentSearch.class) .matching(query) .all();
  12. Cassandra Vector Search Vector vector = Vector.of(…); Columns columns =

    Columns.from("comment") .select("vector", it -> it.similarity(vector).cosine().as("similarity")); @Table class Comments { } class CommentSearch { } Query query = Query.select(columns).limit(3).sort(VectorSort.ann("vector", vector)); List<CommentSearch> result = template.query(Comments.class) .as(CommentSearch.class) .matching(query) .all();
  13. Cassandra Vector Search Vector vector = Vector.of(…); Columns columns =

    Columns.from("comment") .select("vector", it -> it.similarity(vector).cosine().as("similarity")); Query query = Query.select(columns).limit(3).sort(VectorSort.ann("vector", vector)); List<CommentSearch> result = template.query(Comments.class) .as(CommentSearch.class) .matching(query) .all(); @Table class Comments { @Id UUID id; String comment; @VectorType(dimensions = 5) @SaiIndexed Vector vector; } class CommentSearch { String comment; float similarity; }
  14. Spring Data JPA 3.5 • JPQL Parsers: Refinements and bugfixes

    • Detail revisions: Fluent API Projections, Nulls Precedence in String queries • Projections: DTO Constructor Expressions
  15. JPA Constructor Expression for DTO Projections public interface UserRepository extends

    Repository<User, Integer> { @Query("select new com.acme.myapp.UserExcerpt(u.firstname, u.lastname) from User u") List<UserExcerpt> findRecordProjection(); } record UserExcerpt(String firstname, String lastname) { }
  16. Constructor Expression Derivation public interface UserRepository extends Repository<User, Integer> {

    @Query("select u.firstname, u.lastname from User u") List<UserExcerpt> findRecordProjection(); } record UserExcerpt(String firstname, String lastname) { }
  17. Even Better Constructor Expression Derivation public interface UserRepository extends Repository<User,

    Integer> { @Query("select u from User u") List<UserExcerpt> findRecordProjection(); } record UserExcerpt(String firstname, String lastname) { }
  18. Spring Data 4.0 (November 2025.1) • Baseline Upgrade: Spring Framework

    7, JSpecify 1.0, Kotlin 2.1, Jakarta EE 11 • Ahead-of-Time Repositories: JPA, MongoDB • JPA: JPA 3.2, JPQL Derived Queries, Revised Query Parsing & Enhancing • JDBC: Composite Id’s • Fluent API refinements: MongoDB, Cassandra
  19. • Introspection: What Query is actually being executed? • Debugging:

    Where is the Query executed? Repository Query Methods
  20. public Long countByLastname(String lastname) { String queryString = "SELECT COUNT(u)

    FROM User u WHERE u.lastname = :lastname"; Query query = this.entityManager.createQuery(queryString); query.setParameter("lastname", lastname); return (Long) query.getSingleResultOrNull(); }
  21. @Generated public class UserRepositoryImpl__Aot extends AotRepositoryFragmentSupport { private final EntityManager

    entityManager; public UserRepositoryImpl__Aot(EntityManager em, FragmentCreationContext ctx) { … } public Long countByLastname(String lastname) { String queryString = "SELECT COUNT(u) FROM User u WHERE u.lastname = :lastname"; Query query = this.entityManager.createQuery(queryString); query.setParameter("lastname", lastname); return (Long) query.getSingleResultOrNull(); } // … }
  22. • Opt-in: spring.aot.repositories.enabled=true • Generated Query Method Bodies • Only

    code required to run the query • Debuggable Query Methods 🤩 • Tooling Support: Repository.json AOT Repositories
  23. JPA Repositories Startup Performance Memory 0 85 170 255 340

    Regular AOT AOT Repositories Bootstrap First Request
  24. JPA Repositories Startup Performance Memory 0 85 170 255 340

    Regular AOT AOT Repositories Bootstrap First Request -11 % -4 %
  25. { "name": "com.acme.UserRepository", "module": "JPA", "type": "IMPERATIVE", "methods": [ {

    "name": "countUsersByLastname", "signature": "public abstract java.lang.Long com.acme.UserRepository.countUsersByLastname(java.lang.String)", "query": { "query": "SELECT COUNT(u) FROM com.acme.User u WHERE u.lastname = :lastname" } }, { "name": "findPagedWithNamedCountByEmailAddress", "signature": "public abstract Page<com.acme.User> com.acme.UserRepository.findPagedWithNamedCountByEmailAddress(Pageable,java.lang.String)", "query": { "name": "User.findByEmailAddress", "query": "SELECT u FROM User u WHERE u.emailAddress = ?1", "count-name": "User.findByEmailAddress.count-provided", "count-query": "SELECT count(u) FROM User u WHERE u.emailAddress = ?1" } }, { "name": "saveAll", "signature": "public abstract <S extends T> java.lang.Iterable<S> org.springframework.data.repository.CrudRepository.saveAll(java.lang.Iterable<S>)", "fragment": { "interface": "org.springframework.data.jpa.repository.support.SimpleJpaRepository", "fragment": "org.springframework.data.jpa.repository.support.SimpleJpaRepository" } } ] }
  26. • Generated Bean Definition wiring • Application must run in

    AOT mode (Native Image or -Dspring.aot.enabled=true) • Requires AOT processing • Can get out of sync • Requires AOT re-processing AOT Repositories (cont’d) mvn clean package gradle processAot
  27. • Not every Method should be generated • Caching of

    rewrite inputs • Queries can become dynamic: Range<T> Queries, IS NULL • JPA (Hibernate only) and MongoDB available now, more modules to follow AOT Repository Method Limitations
  28. • Baseline Upgrade: JPA 3.2, Hibernate 7, Eclipselink 5 •

    Query Derivation: JPQL instead of Criteria API • Specifications: Revised Specification design • Query Enhancer Selectors: Configuration of Query Enhancers (e.g. JSqlParser) • JPQL Parsers: Limited JpaSort.unsafe(…) parsing for Specifications Spring Data JPA 4.0
  29. Spring Data JPA 3.5 with Hibernate Queries per Second 0

    100 200 300 400 500 600 700 JPQL Criteria Queries In thousands, H2 In-Memory Database via EntityManager
  30. Spring Data JPA 4.0 with Hibernate Queries per Second 0

    100 200 300 400 500 600 700 Derived Queries @Query EntityManager In thousands, H2 In-Memory Database, JPQL Queries
  31. • Missing Update support, broken Delete support (nullability) • PredicateSpecification:

    Composition of Predicates • New variants: UpdateSpecification, DeleteSpecification Specification Revision
  32. UpdateSpecification in Action PredicateSpecification<User> predicate = userHasFirstname("Oliver") .and(userHasLastname("Gierke")); UpdateSpecification<User> updateLastname

    = UpdateSpecification .<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Drotbohm")) .where(predicate); long updated = repository.update(updateLastname);
  33. UpdateSpecification in Action PredicateSpecification<User> predicate = userHasFirstname("Oliver") .and(userHasLastname("Gierke")); UpdateSpecification<User> updateLastname

    = UpdateSpecification .<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Drotbohm")) .where(predicate); long updated = repository.update(updateLastname);
  34. UpdateSpecification in Action PredicateSpecification<User> predicate = userHasFirstname("Oliver") .and(userHasLastname("Gierke")); UpdateSpecification<User> updateLastname

    = UpdateSpecification .<User> update((root, update, criteriaBuilder) -> update.set("lastname", "Drotbohm")) .where(predicate); long updated = repository.update(updateLastname);
  35. • Type-based: Entity, Projection, or Converter<Document, Person> • EntityCallbacks: Application-wide

    or per Type Result Mapping Variants List<PersonProjection> result = template.query(Person.class) .as(PersonProjection.class) .matching(query) .all();
  36. • Query Result Converter: Contextual conversion during query result reading

    • Actual result: Raw query result • Contextual Converter: Wrapping, custom mapping Fluent API Refinement List<PersonProjection> result = template.query(Person.class) .as(PersonProjection.class) .matching(query) .map((document, reader) -> ) .all();
  37. interface VectorSearchRepository extends JpaRepository<Comment, Integer> { SearchResults<Comment> searchTop5ByEmbeddingWithin(Vector embedding, Score

    distance); } Vector vector = Vector.of(…); SearchResults<Comment> results = repository.searchTop5ByEmbeddingWithin(vector,
  38. interface VectorSearchRepository extends JpaRepository<Comment, Integer> { SearchResults<Comment> searchTop5ByEmbeddingWithin(Vector embedding, Score

    distance); } Vector vector = Vector.of(…); SearchResults<Comment> results = repository.searchTop5ByEmbeddingWithin(vector, Similarity.of(0.4, ScoringFunction.cosine()));
  39. • Familiar Programming Model: Declarative Search Methods • Store Variance:

    Distance vs similarity, pre-filters, scoring functions in index • Similarity: Transparent normalization of Vector distance into similarity Repository Vector Search Methods
  40. • Available Today: JPA via Hibernate 7 (Oracle 23, pgvector),

    MongoDB, Cassandra • Spring Data 2025.1.0-M3 • Exploring: Neo4j, Elasticsearch, JDBC, and R2DBC • Later today 15:30, Room 6: Modular RAG Architectures with Java and Spring AI by Thomas Vitale Feedback welcome! Repository Vector Search Methods