🏠 ណែនាំ ⚙️ ការដំឡើង 📦 Entity 🗃️ Repository 🔍 Query Methods 💬 JPQL & Native Query 🔗 Relations 📄 Pagination 🚀 Advanced
📚 មេរៀនពេញលេញ

Spring Data JPA
ពីដំបូងដល់ Advanced

រៀនពីការប្រើ Spring Data JPA សម្រាប់ការគ្រប់គ្រង Database ក្នុង Java Spring Boot — ជាមួយ Entity, Repository, Query Methods, Relations, Pagination និង Transactions ។

Spring Boot 3.x Java 17+ Hibernate 6 MySQL / PostgreSQL Maven

// មាតិកា

01

Spring Data JPA គឺជាអ្វី?

Spring Data JPA គឺជា module មួយក្នុង Spring ecosystem ដែលជួយ simplify ការងារជាមួយ Database ។ វាប្រើ JPA (Java Persistence API) ដែលត្រូវបាន implement ដោយ Hibernate

Architecture Overview
Controller
Service
Repository
JPA / Hibernate
Database
🔥 ចំនុចល្អ

លែងត្រូវសរសេរ SQL ច្រើន — Spring Data JPA generate query ដោយស្វ័យប្រវត្តិ

⚡ CRUD ងាយ

Built-in methods: save(), findById(), delete(), findAll()

🔍 Custom Query

Query ដោយ method name, @Query, JPQL, ឬ Native SQL

📄 Pagination

Pagination & Sorting built-in — ងាយស្រួល implement

02

ការដំឡើង Dependencies

បន្ថែម dependencies ខាងក្រោមទៅក្នុង pom.xml

xml 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>
03

Configuration

properties application.properties
# 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
⚠️
នៅ Production ត្រូវ set ddl-auto=validatenone ហើយប្រើ Flyway/Liquibase សម្រាប់ migration ។
04

Entity Class

Entity class គឺជា Java class ដែល map ទៅ database table ។

java Product.java
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
}
@Entity

Mark class ថា JPA Entity — ត្រូវ map ទៅ database table

@Table

កំណត់ table name, schema, indexes នៅ database

@Id

Primary key field — ត្រូវតែ specify នៅ Entity ម្ដង

@GeneratedValue

IDENTITY (auto-increment), SEQUENCE, UUID

@Column

Customize column: name, nullable, length, unique, precision

@Enumerated

STRING store ជា text, ORDINAL store ជា integer

05

Repository Interface

Spring Data JPA មាន Repository interfaces ជាច្រើន — ជ្រើសរើសត្រឹមត្រូវ

Repository Hierarchy
Repository<T,ID>
CrudRepository<T,ID>
PagingAndSortingRepository
JpaRepository<T,ID>
java ProductRepository.java
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
java ProductService.java — ឧទាហរណ៍ CRUD
@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);
    }
}
06

Query Methods

Spring Data JPA generate SQL ដោយស្វ័យប្រវត្តិ ពី method name — គ្រាន់តែ follow naming convention ត្រូវ ។

java ProductRepository.java — Query Methods
@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);
}
KeywordSQL Equivalentឧទាហរណ៍
AndANDfindByNameAndStatus
OrORfindByNameOrEmail
BetweenBETWEEN x AND yfindByPriceBetween
LessThan<findByAgeLessThan
GreaterThan>findByAgeGreaterThan
LikeLIKEfindByNameLike
ContainingLIKE %x%findByNameContaining
StartingWithLIKE x%findByNameStartingWith
InIN (list)findByStatusIn
IsNullIS NULLfindByEmailIsNull
OrderByORDER BYfindByStatusOrderByName
Top/FirstLIMITfindTop10By
07

JPQL & Native Query

ពេល query methods មិន flexible គ្រប់គ្រាន់ — ប្រើ @Query annotation ។

java ProductRepository.java — @Query
// === 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);
ℹ️
JPQL vs Native SQL: JPQL ប្រើ Entity class names ហើយ database-independent ។ Native SQL ប្រើ table/column names ពិតប្រាកដ ហើយ database-specific ។
08

Entity Relationships

🔗 One-to-Many / Many-to-One

ឧទាហរណ៍: Category មួយ → Products ច្រើន

java Category.java (One side)
@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<>();
}
java Product.java (Many side)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
🔀 Many-to-Many

ឧទាហរណ៍: Product ↔ Tag (product មួយ ⟷ tags ច្រើន)

java Product.java — Many-to-Many with Tag
@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<>();
⚡ One-to-One

ឧទាហរណ៍: User ↔ UserProfile

java User.java — One-to-One
@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
⚠️
N+1 Problem: ប្រើ @EntityGraph ឬ JOIN FETCH ក្នុង JPQL ដើម្បីជៀសវាង N+1 query problem ជាមួយ LAZY loading ។
java ជៀសវាង N+1 ជាមួយ EntityGraph
// 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);
09

Pagination & Sorting

java ProductRepository.java — Pagination
// Pageable នៅ Repository
Page<Product> findAll(Pageable pageable);
Page<Product> findByStatus(ProductStatus status, Pageable pageable);
Page<Product> findByNameContaining(String keyword, Pageable pageable);
java ProductService.java — ប្រើ Pagination
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?
java ProductController.java — Pagination API
@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
10

Advanced Features

🔧 Specifications (Dynamic Query)

Dynamic query ស្វ័យប្រវត្តិ ដោយផ្អែកលើ filter conditions

java ProductSpec.java + ProductRepository
// 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);
📋 JPA Auditing

Auto track: createdBy, createdDate, lastModifiedBy, lastModifiedDate

java BaseEntity.java + Configuration
// 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...
}
🔒 @Transactional

Ensure data consistency — rollback ពេល error

java OrderService.java — Transaction Example
@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;
    }
}
Best Practices:
  • ប្រើ @Transactional(readOnly=true) សម្រាប់ read operations
  • ជៀសវាង EAGER fetch — ប្រើ LAZY + JOIN FETCH
  • ប្រើ DTO / Projection ជំនួស Entity ក្នុង API response
  • Index columns ដែល query ញឹកញាប់
  • Production → ddl-auto=validate ជាមួយ Flyway