Complete guide to Spring Data: the official Spring framework for simplifying data access across relational databases, NoSQL databases, and other data stores. Learn architecture, components, and build production-ready data access layers.
Table of Contents
1. What is Spring Data?
Spring Data is an umbrella project within the Spring ecosystem that provides a unified, consistent programming model for data access across different data stores. It simplifies data access by reducing boilerplate code and providing powerful abstractions for working with relational databases (JPA), NoSQL databases (MongoDB, Redis, Neo4j), search engines (Elasticsearch), and other data technologies.
Instead of writing repetitive CRUD operations, complex queries, and data access boilerplate, Spring Data allows you to define repository interfaces with method signatures. The framework automatically implements these methods at runtime, following Spring's convention-over-configuration philosophy. This makes data access as straightforward as declaring an interface and letting Spring handle the implementation.
Spring Data supports multiple data access technologies through specialized modules, each optimized for its specific data store while maintaining a consistent programming model across all of them.
2. Why Use Spring Data?
- Reduced boilerplate: eliminate repetitive CRUD code and focus on business logic instead of data access implementation details.
- Unified API: consistent programming model across different data stores (SQL, NoSQL, search engines) makes it easier to work with multiple technologies.
- Query method generation: automatically generate queries from method names, reducing the need for manual query writing.
- Spring Boot integration: auto-configuration, property-based setup, and seamless integration with the Spring ecosystem.
- Type-safe queries: leverage Java's type system to catch query errors at compile time rather than runtime.
- Testing support: easy mocking and testing of repositories using Spring's testing framework and in-memory databases.
- Pagination and sorting: built-in support for pagination, sorting, and dynamic queries without additional code.
- Transaction management: declarative transaction support integrated with Spring's transaction management.
3. Spring Data Architecture
Spring Data follows a layered architecture that abstracts data store specifics while providing a consistent interface:
3.1 Architecture Layers
- Application Layer: Your Spring Boot application code (controllers, services) that uses repositories.
- Repository Interface: Your custom repository interfaces extending Spring Data base interfaces.
- Spring Data Core: Core abstractions like
Repository,CrudRepository,PagingAndSortingRepository. - Module-Specific Implementations: Concrete implementations for different data stores (JPA, MongoDB, Redis, etc.).
- Data Store APIs: Native APIs of the underlying data stores (JDBC, MongoDB Driver, Redis Client, etc.).
This architecture allows you to:
- Write business logic against stable Spring Data interfaces
- Switch data stores (with some limitations) without changing repository interfaces
- Test with mock implementations or in-memory databases
- Combine multiple data stores in the same application
4. Core Components
The following diagram illustrates how Spring Data components interact:
4.1 Repository Interface
The Repository interface is the central abstraction in Spring Data. It's a marker interface with no methods, serving as a base for all Spring Data repositories. It provides type information and enables Spring Data to discover and create repository implementations.
4.1.1 Repository Hierarchy
Spring Data provides a hierarchy of repository interfaces, each adding more functionality:
- Repository<T, ID>: Base marker interface with no methods
- CrudRepository<T, ID>: Adds basic CRUD operations (save, findById, findAll, delete, etc.)
- PagingAndSortingRepository<T, ID>: Extends CrudRepository with pagination and sorting
- JpaRepository<T, ID>: JPA-specific extensions (flush, saveAndFlush, etc.)
Example:
// Base repository - marker interface
public interface Repository<T, ID> {}
// CRUD operations
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
Optional<T> findById(ID id);
Iterable<T> findAll();
void deleteById(ID id);
// ... more methods
}
// Pagination and sorting
public interface PagingAndSortingRepository<T, ID>
extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
// JPA-specific extensions
public interface JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID> {
void flush();
<S extends T> S saveAndFlush(S entity);
// ... more JPA-specific methods
}
4.2 Spring Data JPA
Spring Data JPA is the most popular Spring Data module, providing repository support for the Java Persistence API (JPA). It simplifies working with relational databases like PostgreSQL, MySQL, H2, and others.
4.2.1 Entity Mapping
Entities are Java classes mapped to database tables using JPA annotations:
package com.example.data.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
@Column(name = "created_at")
private LocalDateTime createdAt;
// Relationships
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders;
// Constructors, getters, setters
// ...
}
4.2.2 JPA Repository
Create a repository interface extending JpaRepository:
package com.example.data.repository;
import com.example.data.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Query methods are automatically implemented
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String name);
boolean existsByEmail(String email);
}
4.2.3 Key JPA Features
- Entity Relationships: @OneToMany, @ManyToOne, @ManyToMany, @OneToOne
- Cascading Operations: CascadeType for automatic persistence of related entities
- Lazy/Eager Loading: Control when related entities are loaded
- Transaction Management: Automatic transaction boundaries
- Entity Lifecycle: @PrePersist, @PostPersist, @PreUpdate, @PostUpdate callbacks
4.3 Spring Data MongoDB
Spring Data MongoDB provides repository support for MongoDB, a NoSQL document database. It maps Java objects to MongoDB documents and provides query methods.
4.3.1 Document Mapping
Documents are Java classes mapped to MongoDB collections:
package com.example.data.document;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.time.LocalDateTime;
import java.util.List;
@Document(collection = "products")
public class Product {
@Id
private String id;
private String name;
private Double price;
@Field("created_at")
private LocalDateTime createdAt;
private List<String> tags;
// Nested documents
private ProductDetails details;
// Constructors, getters, setters
// ...
}
4.3.2 MongoDB Repository
package com.example.data.repository;
import com.example.data.document.Product;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends MongoRepository<Product, String> {
List<Product> findByNameContaining(String name);
List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
List<Product> findByTagsIn(List<String> tags);
}
4.3.3 Key MongoDB Features
- Document Storage: Store complex nested objects as JSON documents
- Flexible Schema: No fixed schema, documents can have different structures
- Indexing: Support for MongoDB indexes for performance
- Aggregation: Complex queries using MongoDB aggregation framework
- GridFS: Store large files using GridFS
4.4 Spring Data Redis
Spring Data Redis provides repository support for Redis, an in-memory data structure store. It's commonly used for caching, session storage, and real-time applications.
4.4.1 Redis Hash Mapping
Redis entities are mapped using @RedisHash:
package com.example.data.redis;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
import java.time.LocalDateTime;
@RedisHash("sessions")
public class Session {
@Id
private String id;
@Indexed
private String userId;
private String token;
private LocalDateTime expiresAt;
// Constructors, getters, setters
// ...
}
4.4.2 Redis Repository
package com.example.data.repository;
import com.example.data.redis.Session;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface SessionRepository extends CrudRepository<Session, String> {
List<Session> findByUserId(String userId);
void deleteByUserId(String userId);
}
4.4.3 Key Redis Features
- In-Memory Storage: Extremely fast read/write operations
- Data Structures: Strings, Hashes, Lists, Sets, Sorted Sets
- Pub/Sub: Publish-subscribe messaging
- Expiration: Automatic key expiration (TTL)
- Caching: Perfect for caching frequently accessed data
4.5 Spring Data Elasticsearch
Spring Data Elasticsearch provides repository support for Elasticsearch, a distributed search and analytics engine. It's ideal for full-text search, log analysis, and real-time analytics.
4.5.1 Document Mapping
package com.example.data.elasticsearch;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "articles")
public class Article {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "standard")
private String title;
@Field(type = FieldType.Text, analyzer = "standard")
private String content;
@Field(type = FieldType.Keyword)
private String category;
// Constructors, getters, setters
// ...
}
4.5.2 Elasticsearch Repository
package com.example.data.repository;
import com.example.data.elasticsearch.Article;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ArticleRepository extends ElasticsearchRepository<Article, String> {
List<Article> findByTitleContaining(String title);
List<Article> findByCategory(String category);
}
4.6 Query Methods
Query Methods are one of Spring Data's most powerful features. By following naming conventions, Spring Data automatically generates queries from method names, eliminating the need to write SQL or query code manually.
4.6.1 Query Method Naming Conventions
Spring Data parses method names and generates queries based on keywords:
- find...By: Query method prefix
- read...By: Alternative prefix (same as find)
- get...By: Alternative prefix (same as find)
- query...By: Alternative prefix (same as find)
- count...By: Returns count
- exists...By: Returns boolean
- delete...By: Deletes entities
4.6.2 Query Keywords
Common keywords for building queries:
- And: Logical AND (e.g.,
findByFirstNameAndLastName) - Or: Logical OR (e.g.,
findByFirstNameOrLastName) - Is, Equals: Equality check (e.g.,
findByEmailIs,findByEmailEquals) - Between: Range query (e.g.,
findByAgeBetween) - LessThan, LessThanEqual: Less than comparison
- GreaterThan, GreaterThanEqual: Greater than comparison
- After, Before: Date comparisons
- IsNull, IsNotNull: Null checks
- Like, NotLike: Pattern matching
- StartingWith, EndingWith, Containing: String matching
- In, NotIn: In clause
- OrderBy: Sorting (e.g.,
findByAgeOrderByNameDesc)
4.6.3 Query Method Examples
public interface UserRepository extends JpaRepository<User, Long> {
// Simple queries
Optional<User> findByEmail(String email);
List<User> findByName(String name);
// Multiple conditions
List<User> findByFirstNameAndLastName(String firstName, String lastName);
List<User> findByAgeBetween(int minAge, int maxAge);
// String matching
List<User> findByNameContaining(String name);
List<User> findByNameStartingWith(String prefix);
List<User> findByNameEndingWith(String suffix);
// Comparisons
List<User> findByAgeGreaterThan(int age);
List<User> findByCreatedAtAfter(LocalDateTime date);
// Null checks
List<User> findByEmailIsNotNull();
// In clause
List<User> findByIdIn(List<Long> ids);
// Sorting
List<User> findByAgeOrderByNameAsc(int age);
// Count and exists
long countByEmail(String email);
boolean existsByEmail(String email);
// Delete
void deleteByEmail(String email);
// Limiting results
User findFirstByOrderByCreatedAtDesc();
List<User> findTop10ByOrderByAgeDesc();
}
4.6.4 Custom Queries with @Query
For complex queries, use @Query annotation:
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL query
@Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")
Optional<User> findActiveUserByEmail(@Param("email") String email);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE age > :age", nativeQuery = true)
List<User> findUsersOlderThan(@Param("age") int age);
// Update query
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginDate < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}
4.7 Specifications
Specifications provide a type-safe way to build dynamic queries. They're particularly useful for building complex, dynamic search filters where query methods become unwieldy.
4.7.1 What are Specifications?
Specifications use the Specification pattern to build queries programmatically. They're ideal when you need to:
- Build dynamic queries based on user input
- Combine multiple search criteria
- Create reusable query components
- Maintain type safety
4.7.2 Creating Specifications
package com.example.data.specification;
import com.example.data.entity.User;
import org.springframework.data.jpa.domain.Specification;
public class UserSpecifications {
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.equal(root.get("email"), email);
}
public static Specification<User> isActive() {
return (root, query, cb) ->
cb.equal(root.get("active"), true);
}
public static Specification<User> ageBetween(int minAge, int maxAge) {
return (root, query, cb) ->
cb.between(root.get("age"), minAge, maxAge);
}
public static Specification<User> nameContains(String name) {
return (root, query, cb) ->
name == null ? null :
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
}
4.7.3 Using Specifications
Extend JpaSpecificationExecutor to use specifications:
public interface UserRepository
extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
// Repository methods
}
// Usage in service
@Service
public class UserService {
private final UserRepository userRepository;
public List<User> searchUsers(String email, String name, Integer minAge, Integer maxAge) {
Specification<User> spec = Specification.where(null);
if (email != null) {
spec = spec.and(UserSpecifications.hasEmail(email));
}
if (name != null) {
spec = spec.and(UserSpecifications.nameContains(name));
}
if (minAge != null && maxAge != null) {
spec = spec.and(UserSpecifications.ageBetween(minAge, maxAge));
}
return userRepository.findAll(spec);
}
}
5. Project Setup
To get started with Spring Data, add the appropriate Spring Data module dependencies to your project. Each data store has its own Spring Data module.
5.1 Gradle Configuration
Create a build.gradle file with Spring Data JPA:
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
java {
sourceCompatibility = '17'
targetCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot starters
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Database driver (PostgreSQL example)
runtimeOnly 'org.postgresql:postgresql'
// For development/testing - H2 in-memory database
runtimeOnly 'com.h2database:h2'
// Optional: QueryDSL for type-safe queries
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
5.2 Maven Configuration (Alternative)
If you prefer Maven, use the following pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
5.3 Multiple Data Stores
To use multiple Spring Data modules (e.g., JPA + MongoDB + Redis):
dependencies {
// Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Spring Data MongoDB
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
// Spring Data Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Database drivers
runtimeOnly 'org.postgresql:postgresql'
}
6. Configuration
Configure Spring Data using application.properties or application.yml. Spring Boot's auto-configuration handles most of the setup.
6.1 Spring Data JPA Configuration
# application.properties
# Database connection
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=postgres
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA/Hibernate settings
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true
# Connection pool
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
6.2 Spring Data MongoDB Configuration
# application.properties
spring.data.mongodb.uri=mongodb://localhost:27017/mydb
spring.data.mongodb.auto-index-creation=true
6.3 Spring Data Redis Configuration
# application.properties
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.timeout=2000ms
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
6.4 Spring Data Elasticsearch Configuration
# application.properties
spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.connection-timeout=1s
spring.elasticsearch.socket-timeout=30s
6.5 Multiple Data Sources
When using multiple data stores, configure each separately:
# application.properties
# PostgreSQL (JPA)
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=postgres
spring.datasource.password=password
# MongoDB
spring.data.mongodb.uri=mongodb://localhost:27017/mydb
# Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
7. Real-World Examples
7.1 Example 1: User Management with JPA
A complete user management system using Spring Data JPA:
package com.example.data.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
private boolean active = true;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Constructors, getters, setters
// ...
}
package com.example.data.repository;
import com.example.data.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String name);
List<User> findByActiveTrue();
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDateTime date);
}
package com.example.data.service;
import com.example.data.entity.User;
import com.example.data.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(String email, String name) {
if (userRepository.existsByEmail(email)) {
throw new IllegalArgumentException("Email already exists");
}
User user = new User();
user.setEmail(email);
user.setName(name);
return userRepository.save(user);
}
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}
public List<User> searchUsers(String name) {
return userRepository.findByNameContaining(name);
}
public void deactivateUser(Long id) {
userRepository.findById(id).ifPresent(user -> {
user.setActive(false);
userRepository.save(user);
});
}
}
7.2 Example 2: REST Controller with Pagination
Expose user data via REST API with pagination support:
package com.example.data.controller;
import com.example.data.entity.User;
import com.example.data.service.UserService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<Page<User>> getAllUsers(
@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
Page<User> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
User user = userService.createUser(request.email(), request.name());
return ResponseEntity.ok(user);
}
public record CreateUserRequest(String email, String name) {}
}
7.3 Example 3: Product Catalog with MongoDB
Store and query products using Spring Data MongoDB:
package com.example.data.document;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.math.BigDecimal;
import java.util.List;
@Document(collection = "products")
public class Product {
@Id
private String id;
private String name;
private String description;
private BigDecimal price;
@Field("category_id")
private String categoryId;
private List<String> tags;
private ProductInventory inventory;
// Constructors, getters, setters
// ...
public static class ProductInventory {
private int stock;
private boolean inStock;
// ...
}
}
package com.example.data.repository;
import com.example.data.document.Product;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.math.BigDecimal;
import java.util.List;
public interface ProductRepository extends MongoRepository<Product, String> {
List<Product> findByNameContainingIgnoreCase(String name);
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
List<Product> findByCategoryId(String categoryId);
List<Product> findByTagsIn(List<String> tags);
@Query("{ 'inventory.stock': { $gt: 0 } }")
List<Product> findInStockProducts();
}
7.4 Example 4: Caching with Redis
Implement caching using Spring Data Redis:
package com.example.data.service;
import com.example.data.entity.User;
import com.example.data.repository.UserRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class CachedUserService {
private final UserRepository userRepository;
public CachedUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Cacheable(value = "users", key = "#id")
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
@Cacheable(value = "users", key = "#email")
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}
@CachePut(value = "users", key = "#user.id")
public User save(User user) {
return userRepository.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void deleteById(Long id) {
userRepository.deleteById(id);
}
}
7.5 Example 5: Complex Search with Specifications
Build dynamic search functionality using Specifications:
package com.example.data.specification;
import com.example.data.entity.User;
import org.springframework.data.jpa.domain.Specification;
public class UserSpecifications {
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.equal(root.get("email"), email);
}
public static Specification<User> nameContains(String name) {
return (root, query, cb) ->
name == null ? null :
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> isActive() {
return (root, query, cb) -> cb.equal(root.get("active"), true);
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
}
package com.example.data.service;
import com.example.data.entity.User;
import com.example.data.repository.UserRepository;
import com.example.data.specification.UserSpecifications;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class UserSearchService {
private final UserRepository userRepository;
public UserSearchService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Page<User> searchUsers(
String email,
String name,
Boolean active,
LocalDateTime createdAfter,
Pageable pageable) {
Specification<User> spec = Specification.where(null);
if (email != null && !email.isEmpty()) {
spec = spec.and(UserSpecifications.hasEmail(email));
}
if (name != null && !name.isEmpty()) {
spec = spec.and(UserSpecifications.nameContains(name));
}
if (active != null) {
spec = spec.and(UserSpecifications.isActive());
}
if (createdAfter != null) {
spec = spec.and(UserSpecifications.createdAfter(createdAfter));
}
return userRepository.findAll(spec, pageable);
}
}
7.6 Example 6: Transaction Management
Demonstrate proper transaction handling:
package com.example.data.service;
import com.example.data.entity.Order;
import com.example.data.entity.OrderItem;
import com.example.data.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public Order createOrder(Long userId, List<OrderItem> items) {
Order order = new Order();
order.setUserId(userId);
order.setItems(items);
order.calculateTotal();
// All operations in this method are in the same transaction
// If any operation fails, the entire transaction rolls back
return orderRepository.save(order);
}
@Transactional(readOnly = true)
public Optional<Order> findById(Long id) {
// Read-only transaction - optimized for queries
return orderRepository.findById(id);
}
@Transactional(rollbackFor = Exception.class)
public void processOrder(Long orderId) throws Exception {
// Transaction will rollback on any exception
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("Order not found"));
// Business logic that might throw exceptions
order.process();
orderRepository.save(order);
}
}
8. Best Practices
8.1 Repository Design
- Keep repositories focused: Each repository should handle one entity type
- Use query methods: Prefer query methods over custom queries when possible
- Return appropriate types: Use
Optionalfor single results,Listfor collections - Naming conventions: Follow Spring Data naming conventions for query methods
- Avoid business logic: Keep repositories focused on data access, move business logic to services
8.2 Entity Design
- Use JPA annotations correctly: @Entity, @Table, @Id, @GeneratedValue
- Define relationships carefully: Choose appropriate fetch types (LAZY vs EAGER)
- Avoid bidirectional relationships: Prefer unidirectional when possible
- Use @Version for optimistic locking: Prevent concurrent modification issues
- Implement equals() and hashCode(): Use business keys, not database IDs
8.3 Query Optimization
- Use pagination: Always paginate large result sets
- Fetch joins for relationships: Use JOIN FETCH to avoid N+1 queries
- Index frequently queried columns: Add database indexes for performance
- Use projections: Select only needed columns with DTO projections
- Monitor query performance: Enable SQL logging in development
8.4 Transaction Management
- Use @Transactional at service layer: Not at repository or controller level
- Mark read-only transactions: Use
@Transactional(readOnly = true)for queries - Handle exceptions properly: Understand which exceptions cause rollback
- Avoid long-running transactions: Keep transaction boundaries small
- Use transaction propagation: Understand REQUIRED, REQUIRES_NEW, etc.
8.5 Error Handling
@Service
public class UserService {
private final UserRepository userRepository;
public User findUserOrThrow(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found: " + id));
}
public User createUser(String email, String name) {
if (userRepository.existsByEmail(email)) {
throw new DuplicateEntityException("Email already exists: " + email);
}
// Create user...
}
}
8.6 Testing
- Use @DataJpaTest: For repository layer testing
- Use in-memory databases: H2 for JPA tests, Embedded MongoDB for MongoDB tests
- Test query methods: Verify query methods work correctly
- Test transactions: Verify transaction behavior
- Use TestContainers: For integration tests with real databases
9. Testing
Spring Data makes testing easy with specialized test annotations and in-memory databases. The following examples demonstrate testing with JUnit 5:
package com.example.data.repository;
import com.example.data.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void testSaveAndFindById() {
// Given
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
entityManager.persistAndFlush(user);
// When
Optional<User> found = userRepository.findById(user.getId());
// Then
assertThat(found).isPresent();
assertThat(found.get().getEmail()).isEqualTo("test@example.com");
}
@Test
void testFindByEmail() {
// Given
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
entityManager.persistAndFlush(user);
// When
Optional<User> found = userRepository.findByEmail("test@example.com");
// Then
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Test User");
}
@Test
void testExistsByEmail() {
// Given
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
entityManager.persistAndFlush(user);
// When
boolean exists = userRepository.existsByEmail("test@example.com");
// Then
assertThat(exists).isTrue();
}
}
package com.example.data.service;
import com.example.data.entity.User;
import com.example.data.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testCreateUser() {
// Given
User savedUser = new User();
savedUser.setId(1L);
savedUser.setEmail("test@example.com");
savedUser.setName("Test User");
when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
User result = userService.createUser("test@example.com", "Test User");
// Then
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getEmail()).isEqualTo("test@example.com");
}
}
Run tests using Gradle:
# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests UserRepositoryTest
# Run with coverage
./gradlew test jacocoTestReport
The component relationships in a typical Spring Data test setup:
10. Advanced Concepts
10.1 Custom Repository Implementations
Sometimes you need custom repository methods that can't be expressed as query methods. Spring Data allows you to provide custom implementations:
10.1.1 Custom Repository Interface
public interface UserRepositoryCustom {
List<User> findUsersWithComplexLogic(String criteria);
}
public interface UserRepository
extends JpaRepository<User, Long>, UserRepositoryCustom {
// Standard methods
}
10.1.2 Custom Implementation
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findUsersWithComplexLogic(String criteria) {
// Custom implementation using EntityManager
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
// Complex query logic
query.where(cb.like(root.get("name"), "%" + criteria + "%"));
return entityManager.createQuery(query).getResultList();
}
}
10.2 Projections
Projections allow you to select only specific fields from entities, improving performance by reducing data transfer:
10.2.1 Interface-based Projections
public interface UserSummary {
String getName();
String getEmail();
// Only these fields are selected
}
public interface UserRepository extends JpaRepository<User, Long> {
List<UserSummary> findByActiveTrue();
}
10.2.2 DTO Projections
public class UserDTO {
private String name;
private String email;
public UserDTO(String name, String email) {
this.name = name;
this.email = email;
}
// Getters
}
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.data.dto.UserDTO(u.name, u.email) FROM User u")
List<UserDTO> findAllUserDTOs();
}
10.3 Auditing
Auditing automatically tracks creation and modification dates and users:
10.3.1 Enable Auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue
private Long id;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
10.4 Multi-Tenancy
Support multiple tenants in a single database:
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(name = "tenant_id")
private String tenantId;
private String email;
// ...
}
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.tenantId = :tenantId")
List<User> findAllByTenant(@Param("tenantId") String tenantId);
}
10.5 Batch Operations
Optimize bulk operations with batch processing:
@Service
public class BatchUserService {
private final UserRepository userRepository;
@Transactional
public void saveAllInBatch(List<User> users) {
int batchSize = 50;
for (int i = 0; i < users.size(); i += batchSize) {
List<User> batch = users.subList(
i, Math.min(i + batchSize, users.size()));
userRepository.saveAll(batch);
userRepository.flush(); // Force flush to database
}
}
}
11. Production Considerations
11.1 Performance Optimization
- Connection pooling: Configure appropriate pool sizes (HikariCP for JPA)
- Query optimization: Use EXPLAIN ANALYZE to identify slow queries
- Indexing strategy: Create indexes for frequently queried columns
- Lazy loading: Use LAZY fetching for relationships, fetch joins when needed
- Caching: Implement second-level cache (Hibernate) or application-level cache (Redis)
- Batch operations: Use batch inserts/updates for bulk operations
11.2 Database Migrations
- Use Liquibase or Flyway: Version control your database schema
- Never use ddl-auto=create: Use validate or none in production
- Test migrations: Always test migrations in staging before production
- Backup before migration: Always backup before running migrations
11.3 Monitoring and Observability
- Enable query logging: Monitor slow queries in production
- Use connection pool monitoring: Track connection pool metrics
- Database metrics: Monitor database performance (CPU, memory, connections)
- Application metrics: Track repository method execution times
- Distributed tracing: Use tracing to debug complex queries
11.4 Security
- SQL injection prevention: Use parameterized queries (Spring Data does this automatically)
- Input validation: Validate all inputs before persisting
- Access control: Implement proper authorization checks
- Encrypt sensitive data: Encrypt PII and sensitive fields at rest
- Secure connections: Use SSL/TLS for database connections
- Credential management: Store database credentials securely (secrets manager)
11.5 Scalability
- Read replicas: Use read replicas for read-heavy workloads
- Sharding: Consider sharding for very large datasets
- Connection pooling: Configure pools based on expected load
- Async processing: Use async repositories for non-blocking operations
- Caching strategy: Implement multi-level caching (L1: Hibernate, L2: Redis)
11.6 Error Handling
@ControllerAdvice
public class DataAccessExceptionHandler {
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntegrityViolation(
DataIntegrityViolationException ex) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("Data integrity violation", ex.getMessage()));
}
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleEntityNotFound(
EntityNotFoundException ex) {
return ResponseEntity.notFound().build();
}
}
0 Comments