រៀនពីការប្រើ Spring Data JPA សម្រាប់ការគ្រប់គ្រង Database ក្នុង Java Spring Boot — ជាមួយ Entity, Repository, Query Methods, Relations, Pagination និង Transactions ។
Spring Data JPA គឺជា module មួយក្នុង Spring ecosystem ដែលជួយ simplify ការងារជាមួយ Database ។ វាប្រើ JPA (Java Persistence API) ដែលត្រូវបាន implement ដោយ Hibernate ។
លែងត្រូវសរសេរ SQL ច្រើន — Spring Data JPA generate query ដោយស្វ័យប្រវត្តិ
Built-in methods: save(), findById(), delete(), findAll()
Query ដោយ method name, @Query, JPQL, ឬ Native SQL
Pagination & Sorting built-in — ងាយស្រួល implement
បន្ថែម dependencies ខាងក្រោមទៅក្នុង pom.xml
<!-- Spring Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MySQL Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok (optional - ជួយកាត់បន្ថយ boilerplate) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
# Database Connection spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=secret spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA / Hibernate Settings spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
| ddl-auto Value | ន័យ | ប្រើនៅពេល |
|---|---|---|
create |
Drop & Create table ថ្មីរាល់ start | Development / Testing |
create-drop |
Create ពេល start, Drop ពេល stop | Unit Testing |
update |
Update schema ដោយមិន Delete data | Development |
validate |
Validate schema មិន change | Production |
none |
មិន make changes | Production |
ddl-auto=validate ឬ none ហើយប្រើ Flyway/Liquibase សម្រាប់ migration ។Entity class គឺជា Java class ដែល map ទៅ database table ។
import jakarta.persistence.*; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; @Entity // บอกว่าclass นี้เป็น Entity @Table(name = "products") // ชื่อ table ใน DB @Data // Lombok: getter, setter, toString @NoArgsConstructor @AllArgsConstructor @Builder public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", nullable = false, length = 100) private String name; @Column(columnDefinition = "TEXT") private String description; @Column(nullable = false, precision = 10, scale = 2) private BigDecimal price; @Column(name = "stock_quantity") private Integer stockQuantity; @Enumerated(EnumType.STRING) private ProductStatus status; @Column(name = "created_at", updatable = false) @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; } public enum ProductStatus { ACTIVE, INACTIVE, OUT_OF_STOCK }
Mark class ថា JPA Entity — ត្រូវ map ទៅ database table
កំណត់ table name, schema, indexes នៅ database
Primary key field — ត្រូវតែ specify នៅ Entity ម្ដង
IDENTITY (auto-increment), SEQUENCE, UUID
Customize column: name, nullable, length, unique, precision
STRING store ជា text, ORDINAL store ជា integer
Spring Data JPA មាន Repository interfaces ជាច្រើន — ជ្រើសរើសត្រឹមត្រូវ
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface ProductRepository extends JpaRepository<Product, Long> { // JpaRepository ផ្ដល់ built-in methods ជាច្រើន // Custom methods អាចបន្ថែមបាន — មើលផ្នែកបន្ទាប់ }
| Method | ន័យ |
|---|---|
save(entity) | Insert ឬ Update (merge if ID exists) |
saveAll(list) | Save multiple entities ក្នុងពេលតែមួយ |
findById(id) | Find by primary key → Optional<T> |
findAll() | Get all records |
findAllById(ids) | Get multiple by IDs |
existsById(id) | Check if exists → boolean |
count() | Count total records |
deleteById(id) | Delete by ID |
deleteAll() | Delete all records |
flush() | Sync persistence context ទៅ DB |
@Service @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; // CREATE public Product createProduct(Product product) { return productRepository.save(product); } // READ - find by ID public Product getProductById(Long id) { return productRepository.findById(id) .orElseThrow(() -> new RuntimeException("Product not found: " + id)); } // READ - all public List<Product> getAllProducts() { return productRepository.findAll(); } // UPDATE public Product updateProduct(Long id, Product updated) { Product existing = getProductById(id); existing.setName(updated.getName()); existing.setPrice(updated.getPrice()); return productRepository.save(existing); } // DELETE public void deleteProduct(Long id) { productRepository.deleteById(id); } }
Spring Data JPA generate SQL ដោយស្វ័យប្រវត្តិ ពី method name — គ្រាន់តែ follow naming convention ត្រូវ ។
@Repository public interface ProductRepository extends JpaRepository<Product, Long> { // === FIND BY SINGLE FIELD === Optional<Product> findByName(String name); List<Product> findByStatus(ProductStatus status); // === LIKE (contains, starts, ends) === List<Product> findByNameContaining(String keyword); List<Product> findByNameStartingWith(String prefix); List<Product> findByNameEndingWith(String suffix); List<Product> findByNameContainingIgnoreCase(String keyword); // === COMPARISON === List<Product> findByPriceGreaterThan(BigDecimal price); List<Product> findByPriceLessThanEqual(BigDecimal price); List<Product> findByPriceBetween(BigDecimal min, BigDecimal max); List<Product> findByStockQuantityGreaterThan(Integer qty); // === MULTIPLE CONDITIONS === List<Product> findByNameContainingAndStatus(String name, ProductStatus status); List<Product> findByStatusOrStockQuantityGreaterThan(ProductStatus s, Integer qty); // === NULL / NOT NULL === List<Product> findByDescriptionIsNull(); List<Product> findByDescriptionIsNotNull(); // === ORDERING === List<Product> findByStatusOrderByPriceAsc(ProductStatus status); List<Product> findAllByOrderByCreatedAtDesc(); // === COUNT / EXISTS / DELETE === long countByStatus(ProductStatus status); boolean existsByName(String name); void deleteByStatus(ProductStatus status); // === TOP / FIRST === Optional<Product> findFirstByOrderByCreatedAtDesc(); List<Product> findTop5ByStatusOrderByPriceAsc(ProductStatus status); }
| Keyword | SQL Equivalent | ឧទាហរណ៍ |
|---|---|---|
And | AND | findByNameAndStatus |
Or | OR | findByNameOrEmail |
Between | BETWEEN x AND y | findByPriceBetween |
LessThan | < | findByAgeLessThan |
GreaterThan | > | findByAgeGreaterThan |
Like | LIKE | findByNameLike |
Containing | LIKE %x% | findByNameContaining |
StartingWith | LIKE x% | findByNameStartingWith |
In | IN (list) | findByStatusIn |
IsNull | IS NULL | findByEmailIsNull |
OrderBy | ORDER BY | findByStatusOrderByName |
Top/First | LIMIT | findTop10By |
ពេល query methods មិន flexible គ្រប់គ្រាន់ — ប្រើ @Query annotation ។
// === JPQL (ប្រើ Entity/Field names មិនមែន table/column) === @Query("SELECT p FROM Product p WHERE p.price > :minPrice AND p.status = :status") List<Product> findByPriceAndStatus(@Param("minPrice") BigDecimal minPrice, @Param("status") ProductStatus status); // === JPQL with LIKE === @Query("SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))") List<Product> searchByKeyword(@Param("keyword") String keyword); // === JPQL with JOIN === @Query("SELECT p FROM Product p JOIN p.category c WHERE c.name = :categoryName") List<Product> findByCategoryName(@Param("categoryName") String name); // === JPQL - Custom DTO Projection === @Query("SELECT new com.example.dto.ProductSummary(p.id, p.name, p.price) FROM Product p") List<ProductSummary> findProductSummaries(); // === NATIVE SQL Query === @Query(value = "SELECT * FROM products WHERE stock_quantity > :qty", nativeQuery = true) List<Product> findInStockProducts(@Param("qty") Integer qty); // === UPDATE Query (ต้องมี @Modifying + @Transactional) === @Modifying @Transactional @Query("UPDATE Product p SET p.status = :status WHERE p.id = :id") int updateProductStatus(@Param("id") Long id, @Param("status") ProductStatus status); // === DELETE Query === @Modifying @Transactional @Query("DELETE FROM Product p WHERE p.status = :status") void deleteByStatus(@Param("status") ProductStatus status);
ឧទាហរណ៍: Category មួយ → Products ច្រើន
@Entity @Table(name = "categories") @Data public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JsonIgnore // ជៀសវាង infinite loop private List<Product> products = new ArrayList<>(); }
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id", nullable = false) private Category category;
ឧទាហរណ៍: Product ↔ Tag (product មួយ ⟷ tags ច្រើន)
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable( name = "product_tags", // join table name joinColumns = @JoinColumn(name = "product_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Set<Tag> tags = new HashSet<>();
ឧទាហរណ៍: User ↔ UserProfile
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "profile_id", referencedColumnName = "id") private UserProfile profile;
| FetchType | ន័យ | Default |
|---|---|---|
LAZY |
Load related data ពេល access ប៉ុណ្ណោះ (better performance) | @OneToMany, @ManyToMany |
EAGER |
Load related data ភ្លាមៗ ពេល load entity (N+1 problem) | @ManyToOne, @OneToOne |
@EntityGraph ឬ JOIN FETCH ក្នុង JPQL ដើម្បីជៀសវាង N+1 query problem ជាមួយ LAZY loading ។// Method 1: @EntityGraph @EntityGraph(attributePaths = {"category", "tags"}) List<Product> findByStatus(ProductStatus status); // Method 2: JPQL JOIN FETCH @Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.status = :status") List<Product> findWithCategory(@Param("status") ProductStatus status);
// Pageable នៅ Repository Page<Product> findAll(Pageable pageable); Page<Product> findByStatus(ProductStatus status, Pageable pageable); Page<Product> findByNameContaining(String keyword, Pageable pageable);
public Page<Product> getProducts(int page, int size, String sortBy, String dir) { // Sort direction Sort.Direction direction = dir.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC; // Create Pageable object Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy)); return productRepository.findAll(pageable); } // Multiple sort fields Pageable pageable = PageRequest.of(0, 10, Sort.by("status").and(Sort.by("price").descending())); // Page response info Page<Product> result = productRepository.findAll(pageable); result.getTotalElements(); // total records result.getTotalPages(); // total pages result.getNumber(); // current page number result.getContent(); // List of items result.hasNext(); // is there next page? result.hasPrevious(); // is there previous page?
@GetMapping public ResponseEntity<Page<Product>> getProducts( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "id") String sortBy, @RequestParam(defaultValue = "asc") String direction) { Page<Product> products = productService.getProducts(page, size, sortBy, direction); return ResponseEntity.ok(products); } // GET /api/products?page=0&size=10&sortBy=price&direction=desc
Dynamic query ស្វ័យប្រវត្តិ ដោយផ្អែកលើ filter conditions
// Repository extends JpaSpecificationExecutor public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {} // Specification class public class ProductSpec { public static Specification<Product> hasStatus(ProductStatus status) { return (root, query, cb) -> status == null ? null : cb.equal(root.get("status"), status); } public static Specification<Product> priceBetween(BigDecimal min, BigDecimal max) { return (root, query, cb) -> cb.between(root.get("price"), min, max); } public static Specification<Product> nameLike(String keyword) { return (root, query, cb) -> cb.like(cb.lower(root.get("name")), "%" + keyword.toLowerCase() + "%"); } } // ប្រើ Specification ក្នុង Service Specification<Product> spec = Specification .where(ProductSpec.hasStatus(ProductStatus.ACTIVE)) .and(ProductSpec.priceBetween(new BigDecimal("10"), new BigDecimal("100"))) .and(ProductSpec.nameLike("phone")); List<Product> products = productRepository.findAll(spec);
Auto track: createdBy, createdDate, lastModifiedBy, lastModifiedDate
// Enable Auditing ក្នុង Main class @EnableJpaAuditing @SpringBootApplication public class Application { ... } // Base Entity @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Data public abstract class BaseEntity { @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt; @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedBy private String updatedBy; } // Product extends BaseEntity @Entity public class Product extends BaseEntity { // fields... }
Ensure data consistency — rollback ពេល error
@Service public class OrderService { // Read-only transaction (better performance) @Transactional(readOnly = true) public List<Order> getAllOrders() { return orderRepository.findAll(); } // Write transaction with rollback @Transactional(rollbackFor = Exception.class) public Order placeOrder(OrderRequest request) { // 1. Create order Order order = orderRepository.save(new Order(request)); // 2. Deduct stock Product product = productRepository.findById(request.getProductId()) .orElseThrow(); product.setStockQuantity(product.getStockQuantity() - request.getQuantity()); productRepository.save(product); // 3. Send notification (if this fails → rollback all) notificationService.sendOrderConfirmation(order); return order; } }
@Transactional(readOnly=true) សម្រាប់ read operationsddl-auto=validate ជាមួយ Flyway