Slide 1

Slide 1 text

Mark Paluch @mp911de • https://bsky.app/profile/mp911.de Spring Data 4 Data Access Revisited

Slide 2

Slide 2 text

Agenda • Spring Data 3.5 (May Release Train 2025.0) • Spring Data 4.0 (November Release Train 2025.1)

Slide 3

Slide 3 text

Spring Data 3.5

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Vector Search • Types: Vector • Encapsulates accidental complexity • Modules: MongoDB and Apache Cassandra

Slide 7

Slide 7 text

MongoVector.of(BinaryVector.int8Vector(…)) MongoVector.ofFloat(…) vs.

Slide 8

Slide 8 text

Spring Data MongoDB 4.5 • Vector Search: Aggregation Stage • Queryable Encryption: Schema support, Experimental converter

Slide 9

Slide 9 text

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 results = template.aggregate(newAggregation($vectorSearch), Comment.class, Document.class);

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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; }

Slide 12

Slide 12 text

Spring Data for Apache Cassandra 4.5 • Baseline Upgrade: Apache Cassandra 5 • Storage-Attached Indexes (SAI) • Vector Indexing via SAI • Vector Search: CQL support

Slide 13

Slide 13 text

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 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 { }

Slide 14

Slide 14 text

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 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; }

Slide 15

Slide 15 text

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 result = template.query(Comments.class) .as(CommentSearch.class) .matching(query) .all();

Slide 16

Slide 16 text

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 result = template.query(Comments.class) .as(CommentSearch.class) .matching(query) .all();

Slide 17

Slide 17 text

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 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; }

Slide 18

Slide 18 text

Spring Data JPA 3.5 • JPQL Parsers: Refinements and bugfixes • Detail revisions: Fluent API Projections, Nulls Precedence in String queries • Projections: DTO Constructor Expressions

Slide 19

Slide 19 text

JPA Constructor Expression for DTO Projections public interface UserRepository extends Repository { @Query("select new com.acme.myapp.UserExcerpt(u.firstname, u.lastname) from User u") List findRecordProjection(); } record UserExcerpt(String firstname, String lastname) { }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Upcoming Spring Data 4.0

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Repository Query Methods repository.countByLastname("White")

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

• Introspection: What Query is actually being executed? • Debugging: Where is the Query executed? Repository Query Methods

Slide 27

Slide 27 text

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(); }

Slide 28

Slide 28 text

@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(); } // … }

Slide 29

Slide 29 text

• 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

Slide 30

Slide 30 text

JPA Repositories Startup Performance Milliseconds 0 1300 2600 3900 5200 Regular AOT AOT Repositories

Slide 31

Slide 31 text

JPA Repositories Startup Performance Milliseconds 0 1300 2600 3900 5200 Regular AOT AOT Repositories -11 % -9 %

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

JPA Repositories Startup Performance Memory 0 85 170 255 340 Regular AOT AOT Repositories Bootstrap First Request -11 % -4 %

Slide 34

Slide 34 text

{ "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.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 java.lang.Iterable org.springframework.data.repository.CrudRepository.saveAll(java.lang.Iterable)", "fragment": { "interface": "org.springframework.data.jpa.repository.support.SimpleJpaRepository", "fragment": "org.springframework.data.jpa.repository.support.SimpleJpaRepository" } } ] }

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

• 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

Slide 38

Slide 38 text

• Not every Method should be generated • Caching of rewrite inputs • Queries can become dynamic: Range Queries, IS NULL • JPA (Hibernate only) and MongoDB available now, more modules to follow AOT Repository Method Limitations

Slide 39

Slide 39 text

Spring Data JPA 4.0

Slide 40

Slide 40 text

• 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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

• Missing Update support, broken Delete support (nullability) • PredicateSpecification: Composition of Predicates • New variants: UpdateSpecification, DeleteSpecification Specification Revision

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Spring Data MongoDB 5.0 Spring Data Cassandra 5.0

Slide 48

Slide 48 text

• Type-based: Entity, Projection, or Converter • EntityCallbacks: Application-wide or per Type Result Mapping Variants List result = template.query(Person.class) .as(PersonProjection.class) .matching(query) .all();

Slide 49

Slide 49 text

• Query Result Converter: Contextual conversion during query result reading • Actual result: Raw query result • Contextual Converter: Wrapping, custom mapping Fluent API Refinement List result = template.query(Person.class) .as(PersonProjection.class) .matching(query) .map((document, reader) -> ) .all();

Slide 50

Slide 50 text

But there is one more thing…

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

interface VectorSearchRepository extends JpaRepository { SearchResults searchTop5ByEmbeddingWithin(Vector embedding, Score distance); } Vector vector = Vector.of(…); SearchResults results = repository.searchTop5ByEmbeddingWithin(vector, Similarity.of(0.4, ScoringFunction.cosine()));

Slide 53

Slide 53 text

• 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

Slide 54

Slide 54 text

• 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

Slide 55

Slide 55 text

Mark Paluch @mp911de • https://bsky.app/profile/mp911.de Thanks for attending