Introduction
Spring Data JPA is the most popular abstraction layer for interacting with relational databases in Spring Boot. It minimizes boilerplate code for the data access layer and provides powerful query capabilities. This article will guide you from Entity mapping to advanced queries.
1. Database configuration
1.1 PostgreSQL with Docker Compose
# docker-compose.yaml
services:
postgres:
image: postgres:17
environment:
POSTGRES_DB: springboot_demo
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
docker compose up -d
1.2 Application Configuration
# application.yaml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/springboot_demo
username: admin
password: secret
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update # create, create-drop, update, validate, none
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
Note:
ddl-auto: updateFor development use only. Production should be usedvalidateand schema management using Flyway/Liquibase.
2. JPA Entity Mapping
2.1 Basic Entities
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(unique = true, nullable = false, length = 100)
private String slug;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "stock_quantity", nullable = false)
private Integer stockQuantity = 0;
@Column(nullable = false)
private Boolean active = true;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ProductStatus status = ProductStatus.DRAFT;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Constructors, Getters, Setters
protected Product() {} // JPA required
public Product(String name, String slug, BigDecimal price) {
this.name = name;
this.slug = slug;
this.price = price;
}
}
public enum ProductStatus {
DRAFT, ACTIVE, ARCHIVED
}
2.2 Base Entity (DRY)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Getters, Setters
}
// Enable JPA Auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig { }
// Entities kế thừa
@Entity
@Table(name = "products")
public class Product extends BaseEntity {
private String name;
private BigDecimal price;
// ...
}
@Entity
@Table(name = "categories")
public class Category extends BaseEntity {
private String name;
private String slug;
// ...
}
2.3 ID Generation Strategies
// IDENTITY - Database auto-increment (khuyến nghị cho PostgreSQL)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// UUID - Unique across distributed systems
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// SEQUENCE - Database sequence (tốt cho batch insert)
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "product_seq")
@SequenceGenerator(name = "product_seq",
sequenceName = "product_sequence",
allocationSize = 50)
private Long id;
3. Repository Interface
3.1 JpaRepository
public interface ProductRepository extends JpaRepository<Product, Long> {
// JpaRepository cung cấp sẵn:
// save(entity), saveAll(entities)
// findById(id), findAll(), findAllById(ids)
// count(), existsById(id)
// deleteById(id), delete(entity), deleteAll()
// flush(), saveAndFlush(entity)
}
3.2 Derived Query Methods
Spring Data automatically creates queries from method names:
public interface ProductRepository extends JpaRepository<Product, Long> {
// SELECT * FROM products WHERE name = ?
List<Product> findByName(String name);
// SELECT * FROM products WHERE slug = ?
Optional<Product> findBySlug(String slug);
// SELECT * FROM products WHERE price BETWEEN ? AND ?
List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
// SELECT * FROM products WHERE name LIKE '%keyword%'
List<Product> findByNameContainingIgnoreCase(String keyword);
// SELECT * FROM products WHERE active = true ORDER BY created_at DESC
List<Product> findByActiveTrueOrderByCreatedAtDesc();
// SELECT * FROM products WHERE status = ? AND price < ?
List<Product> findByStatusAndPriceLessThan(ProductStatus status, BigDecimal price);
// SELECT * FROM products WHERE category_id IN (?, ?, ?)
List<Product> findByCategoryIdIn(List<Long> categoryIds);
// SELECT COUNT(*) FROM products WHERE status = ?
long countByStatus(ProductStatus status);
// SELECT EXISTS(SELECT 1 FROM products WHERE slug = ?)
boolean existsBySlug(String slug);
// DELETE FROM products WHERE active = false
void deleteByActiveFalse();
// SELECT * FROM products WHERE name = ? LIMIT 1
Optional<Product> findFirstByName(String name);
// SELECT * FROM products ORDER BY price DESC LIMIT 5
List<Product> findTop5ByOrderByPriceDesc();
}
3.3 Query Method Keywords
| Keywords | SQL | Example |
|---|---|---|
And | AND | findByNameAndPrice |
Or | OR | findByNameOrSlug |
Between | BETWEEN | findByPriceBetween |
LessThan | < | findByPriceLessThan |
GreaterThan | > | findByPriceGreaterThan |
Like | LIKE | findByNameLike |
Containing | LIKE %x% | findByNameContaining |
StartingWith | LIKE x% | findByNameStartingWith |
In | findByStatusIn | |
OrderBy | ORDER BY | findByOrderByPriceAsc |
Not | <> | findByStatusNot |
IsNull | IS NULL | findByDeletedAtIsNull |
4. Custom Queries
4.1 @Query with JPQL
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE p.price > :minPrice AND p.status = :status")
List<Product> findExpensiveActiveProducts(
@Param("minPrice") BigDecimal minPrice,
@Param("status") ProductStatus status);
@Query("SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))")
List<Product> searchByKeyword(@Param("keyword") String keyword);
@Query("SELECT p.status, COUNT(p) FROM Product p GROUP BY p.status")
List<Object[]> countByStatusGrouped();
@Query("UPDATE Product p SET p.active = false WHERE p.id = :id")
@Modifying
@Transactional
int softDelete(@Param("id") Long id);
}
4.2 @Query with Native SQL
@Query(value = """
SELECT p.* FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.slug = :categorySlug
AND p.price BETWEEN :minPrice AND :maxPrice
ORDER BY p.created_at DESC
""", nativeQuery = true)
List<Product> findByCategoryAndPriceRange(
@Param("categorySlug") String categorySlug,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice);
4.3 Projections
// Interface-based projection
public interface ProductSummary {
Long getId();
String getName();
BigDecimal getPrice();
}
public interface ProductRepository extends JpaRepository<Product, Long> {
List<ProductSummary> findByActiveTrue();
}
// Record-based projection (Spring Boot 4.x)
public record ProductInfo(Long id, String name, BigDecimal price) {}
@Query("SELECT new com.example.dto.ProductInfo(p.id, p.name, p.price) FROM Product p WHERE p.active = true")
List<ProductInfo> findActiveProductInfo();
5. Auditing
5.1 Auditing configuration
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaAuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
// Lấy username từ Security Context
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName)
.or(() -> Optional.of("system"));
};
}
}
5.2 Auditable Entities
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity extends BaseEntity {
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;
}
Summary
- JPA Entity uses @Entity, @Table, @Column to map Java objects to database tables
- JpaRepository provides built-in CRUD operations, Derived Query Methods automatically create SQL from method name
- @Query supports JPQL and Native SQL for complex queries, Projections reducing data transfer overhead
- JPA Auditing automatically tracks createdAt, updatedAt, createdBy, updatedBy
Exercises
- Create Entity
Articlewith fields: id, title, slug (unique), content (TEXT), status (enum), viewCount, createdAt, updatedAt. Extend from BaseEntity - Create a repository with at least 5 derived query methods: search by status, search by keyword in title, count by status, find top 10 by viewCount
- Write 2 custom @Query: a JPQL query to find articles with viewCount > N, a Native SQL query to join the categories table