簡介
Spring Data JPA 是 Spring Boot 中與關聯式資料庫互動的最受歡迎的抽象層。它最大限度地減少了資料存取層的樣板程式碼,並提供了強大的查詢功能。本文將指導您從實體映射到高階查詢。
1.資料庫配置
1.1 PostgreSQL 與 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.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
注意:
ddl-auto: update僅供開發使用。生產時應使用validate以及使用 Flyway/Liquibase 進行模式管理。
2.JPA實體映射
2.1 基本實體
@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 基礎實體(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產生策略
// 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. 儲存庫接口
3.1 Jpa儲存庫
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 派生查詢方法
Spring Data 會自動根據方法名稱建立查詢:
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 查詢方法關鍵字
| 關鍵字 | SQL | 範例 |
|---|---|---|
And | 和 | findByNameAndPrice |
Or | 或 | findByNameOrSlug |
Between | 之間 | findByPriceBetween |
LessThan | < | findByPriceLessThan |
GreaterThan | > | findByPriceGreaterThan |
Like | 喜歡 | findByNameLike |
Containing | 喜歡 %x% | findByNameContaining |
StartingWith | 喜歡 x% | findByNameStartingWith |
In | 列印 | findByStatusIn |
OrderBy | 訂購方式 | findByOrderByPriceAsc |
Not | <> | findByStatusNot |
IsNull | 為空 | findByDeletedAtIsNull |
4. 自訂查詢
4.1 使用 JPQL 進行@Query
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 使用原生 SQL 進行@Query
@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 預測
// 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. 審計
5.1 審計配置
@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 可審計實體
@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;
}
總結
- JPA Entity 使用@Entity、@Table、@Column 將Java 物件對應到資料庫表
- JpaRepository提供內建CRUD操作,衍生查詢方法自動從方法名稱建立SQL
- @Query 支援 JPQL 和 Native SQL 進行複雜查詢,Projections 減少資料傳輸開銷
- JPA審計自動追蹤createdAt、updatedAt、createdBy、updatedBy
練習
- 建立實體
Article包含欄位:id、標題、slug(唯一)、內容(文字)、狀態(枚舉)、viewCount、createdAt、updatedAt。從 BaseEntity 擴展 - 建立一個至少有 5 種派生查詢方法的儲存庫:按狀態搜尋、按標題中的關鍵字搜尋、按狀態計數、按 viewCount 尋找前 10 名 3.寫2個自訂@Query:一個JPQL查詢來找viewCount>N的文章,一個Native SQL查詢來連接類別表