Skip to main content

Spring JPA Repositories

In modern Java applications, database access is a critical component that shouldn't be overly complex. Spring Data JPA addresses this challenge with repositories - one of the most powerful features in the Spring ecosystem that drastically reduces the code required to implement data access layers.

What are Spring JPA Repositories?

Spring JPA Repositories are interfaces that provide a higher-level abstraction over JPA (Java Persistence API), allowing developers to interact with databases using simple method declarations rather than complex implementations. They eliminate boilerplate code by providing pre-implemented CRUD (Create, Read, Update, Delete) operations and query methods.

Key Benefits

  • Less boilerplate code - no need to write standard CRUD operations
  • Consistent data access patterns across your application
  • Query methods by convention - create queries by method name
  • Pagination and sorting support built-in
  • Custom query implementation when needed

Setting Up Spring Data JPA

Before diving into repositories, let's ensure we have the necessary dependencies:

xml
<!-- For Maven projects -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

For Gradle:

groovy
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'

Creating Your First JPA Repository

Let's start with a simple entity representing a Product:

java
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private double price;
private String category;

// Constructors
public Product() {}

public Product(String name, double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}

// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }

public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }

@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
", category='" + category + '\'' +
'}';
}
}

Now, creating a repository for this entity is remarkably simple:

java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// That's it! No implementation needed.
}

This small interface declaration gives you:

  • CRUD operations for Product entities
  • Pagination and sorting support
  • Batch operations

Using the Repository

Let's see how to use our repository in a service class:

java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

private final ProductRepository productRepository;

@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

// Create a product
public Product saveProduct(Product product) {
return productRepository.save(product);
}

// Retrieve all products
public List<Product> findAllProducts() {
return productRepository.findAll();
}

// Retrieve a product by ID
public Product findProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found: " + id));
}

// Update a product
public Product updateProduct(Product product) {
if (productRepository.existsById(product.getId())) {
return productRepository.save(product);
}
throw new RuntimeException("Product not found: " + product.getId());
}

// Delete a product
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}

Testing Your Repository

Here's a simple way to test your repository using Spring Boot's integration testing support:

java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
public class ProductRepositoryTest {

@Autowired
private ProductRepository productRepository;

@Test
public void shouldSaveAndRetrieveProduct() {
// Given
Product product = new Product("Laptop", 1200.0, "Electronics");

// When
Product savedProduct = productRepository.save(product);
Optional<Product> retrievedProduct = productRepository.findById(savedProduct.getId());

// Then
assertThat(retrievedProduct).isPresent();
assertThat(retrievedProduct.get().getName()).isEqualTo("Laptop");
assertThat(retrievedProduct.get().getPrice()).isEqualTo(1200.0);
}
}

Query Methods by Convention

One of the most powerful features of Spring Data JPA is the ability to define queries simply by creating method names that follow specific conventions:

java
public interface ProductRepository extends JpaRepository<Product, Long> {

// Find products by category
List<Product> findByCategory(String category);

// Find products by name containing certain text (case insensitive)
List<Product> findByNameContainingIgnoreCase(String name);

// Find products by price range
List<Product> findByPriceBetween(double minPrice, double maxPrice);

// Find products by category and order by price
List<Product> findByCategoryOrderByPriceDesc(String category);

// Find top 3 expensive products
List<Product> findTop3ByOrderByPriceDesc();

// Check if product exists by name
boolean existsByName(String name);

// Count products in a category
long countByCategory(String category);
}

Example Usage:

java
@Service
public class ProductService {
// Previous methods...

public List<Product> findExpensiveElectronics(double minPrice) {
return productRepository.findByCategoryAndPriceGreaterThanEqual("Electronics", minPrice);
}

public List<Product> searchProductsByName(String keyword) {
return productRepository.findByNameContainingIgnoreCase(keyword);
}

public List<Product> findTopProducts() {
return productRepository.findTop3ByOrderByPriceDesc();
}
}

Possible Input/Output:

java
// Usage in a controller or application
List<Product> phones = productService.searchProductsByName("phone");
// Output: [Product{id=2, name='Smartphone', price=699.99, category='Electronics'},
// Product{id=5, name='Phone Case', price=19.99, category='Accessories'}]

List<Product> expensiveElectronics = productService.findExpensiveElectronics(1000.0);
// Output: [Product{id=1, name='Laptop', price=1200.0, category='Electronics'},
// Product{id=3, name='4K TV', price=1499.99, category='Electronics'}]

List<Product> premium = productService.findTopProducts();
// Output: [Product{id=3, name='4K TV', price=1499.99, category='Electronics'},
// Product{id=1, name='Laptop', price=1200.0, category='Electronics'},
// Product{id=2, name='Smartphone', price=699.99, category='Electronics'}]

Custom Queries with @Query Annotation

For more complex queries, you can use the @Query annotation:

java
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends JpaRepository<Product, Long> {

// JPQL query
@Query("SELECT p FROM Product p WHERE p.category = :category AND p.price < :price")
List<Product> findByCategoryAndPriceLessThan(
@Param("category") String category,
@Param("price") double price
);

// Native SQL query
@Query(value = "SELECT * FROM product WHERE category = ?1 AND price > ?2",
nativeQuery = true)
List<Product> findExpensiveProductsInCategory(String category, double minPrice);

// Query with ordering
@Query("SELECT p FROM Product p WHERE p.price > :minPrice ORDER BY p.price DESC")
List<Product> findExpensiveProducts(@Param("minPrice") double minPrice);

// Query returning a projection (specific columns)
@Query("SELECT p.name, p.price FROM Product p WHERE p.category = :category")
List<Object[]> findNameAndPriceByCategory(@Param("category") String category);

// Update query
@Modifying
@Transactional
@Query("UPDATE Product p SET p.price = p.price * :multiplier WHERE p.category = :category")
int updatePriceForCategory(@Param("multiplier") double multiplier, @Param("category") String category);
}

Pagination and Sorting

Spring Data JPA makes pagination trivially easy:

java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

@Service
public class ProductService {
// Previous methods...

public Page<Product> findProductsByPage(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("price").descending());
return productRepository.findAll(pageable);
}

public List<Product> findProductsSorted() {
Sort sort = Sort.by("category").ascending()
.and(Sort.by("price").descending());
return productRepository.findAll(sort);
}

// Pagination with custom query
public Page<Product> findProductsByCategoryPaged(String category, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return productRepository.findByCategory(category, pageable);
}
}

// In the repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findByCategory(String category, Pageable pageable);
}

Real-world Example: Product Management API

Let's put everything together in a practical REST API example:

java
// Controller layer
@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductService productService;

@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}

@GetMapping
public ResponseEntity<Page<Product>> getAllProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(productService.findProductsByPage(page, size));
}

@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
try {
Product product = productService.findProductById(id);
return ResponseEntity.ok(product);
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}

@GetMapping("/search")
public ResponseEntity<List<Product>> searchProducts(
@RequestParam String keyword) {
return ResponseEntity.ok(productService.searchProductsByName(keyword));
}

@GetMapping("/category/{category}")
public ResponseEntity<List<Product>> getProductsByCategory(
@PathVariable String category,
@RequestParam(required = false) Double minPrice) {
if (minPrice != null) {
return ResponseEntity.ok(
productService.findExpensiveProductsInCategory(category, minPrice));
}
return ResponseEntity.ok(productService.findByCategory(category));
}

@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(productService.saveProduct(product));
}

@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(
@PathVariable Long id,
@RequestBody Product product) {
try {
product.setId(id);
return ResponseEntity.ok(productService.updateProduct(product));
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
try {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
}

Advanced Repository Features

Spring Data JPA repositories offer several advanced features:

1. Derived Delete Methods

java
public interface ProductRepository extends JpaRepository<Product, Long> {
// Delete by category
void deleteByCategory(String category);

// Delete products older than a certain date
@Transactional
long deleteByDateCreatedBefore(LocalDate date);
}

2. Custom Repository Implementation

For complex operations that can't be expressed through method names or queries:

java
public interface ProductRepositoryCustom {
List<Product> findProductsOnSale();
void updateInventory(Long productId, int quantity);
}

public class ProductRepositoryCustomImpl implements ProductRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;

@Override
public List<Product> findProductsOnSale() {
// Complex logic here with entityManager
return entityManager.createQuery(
"SELECT p FROM Product p WHERE p.onSale = true AND p.availableQuantity > 0",
Product.class)
.getResultList();
}

@Override
public void updateInventory(Long productId, int quantity) {
// Complex update logic
Product product = entityManager.find(Product.class, productId);
if (product != null) {
product.setAvailableQuantity(product.getAvailableQuantity() + quantity);
entityManager.merge(product);
}
}
}

public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
// Regular repository methods...
}

3. Projections

Projections let you return only the data you need, rather than entire entities:

java
// Interface-based projection
public interface ProductSummary {
String getName();
Double getPrice();
// Can also include calculated values
@Value("#{target.name + ' - $' + target.price}")
String getDisplayName();
}

public interface ProductRepository extends JpaRepository<Product, Long> {
List<ProductSummary> findByCategory(String category);
}

// Class-based projection
public class ProductView {
private String name;
private Double price;
private String category;

// Constructor, getters, setters
}

@Query("SELECT new com.example.demo.ProductView(p.name, p.price, p.category) FROM Product p")
List<ProductView> findAllProductViews();

Common Pitfalls and Best Practices

  1. N+1 Query Problem: Be careful with lazy fetching in relationships. Use @EntityGraph for efficient fetching:
java
public interface ProductRepository extends JpaRepository<Product, Long> {
@EntityGraph(attributePaths = {"reviews"})
Optional<Product> findWithReviewsById(Long id);
}
  1. Transaction Management: Ensure read-only operations are marked as such:
java
@Transactional(readOnly = true)
public interface ProductRepository extends JpaRepository<Product, Long> {
// Read operations
}
  1. Repository Composition: Instead of creating one large repository, split functionality into focused interfaces:
java
public interface ReadOnlyProductRepository extends Repository<Product, Long> {
Optional<Product> findById(Long id);
List<Product> findAll();
}

public interface WriteOnlyProductRepository extends Repository<Product, Long> {
Product save(Product product);
void delete(Product product);
}

Summary

Spring Data JPA repositories have revolutionized the way Java developers interact with databases. By drastically reducing boilerplate code and providing a consistent approach to data access, they allow you to focus on business logic instead of complex persistence layer implementations.

Key takeaways:

  • Spring JPA repositories eliminate the need for repetitive CRUD code
  • Query methods follow naming conventions to automatically generate the required SQL/JPQL
  • Custom queries can be added with @Query annotation
  • Advanced features like pagination, sorting, and projections are built-in
  • The implementation is handled by Spring behind the scenes

These repositories integrate seamlessly with the rest of the Spring ecosystem, making them an essential tool in any Spring developer's toolkit.

Additional Resources

Practice Exercises

  1. Create a User entity and a corresponding UserRepository with methods to find users by email, by age range, and by name containing a certain string.

  2. Implement a BookRepository with pagination support and methods to find books by author, by publication year, and by title keywords.

  3. Enhance a CustomerRepository with a custom implementation that performs complex business operations not directly mapped to CRUD functions.

  4. Create a repository that uses projections to return only specific fields from an Order entity.

  5. Implement a repository method using @Query to perform a join between two entities and return a custom result.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)