404 Not Found

404 Not Found


nginx

Spring Data JPA Data Persistence

JPA makes the mapping between objects and relational databases declarative—you write Entities, define Repositories, and SQL is automatically generated.

1. What You'll Learn


2. A True Story About Data Persistence

(1) Pain Point: In-Memory Data Is Lost Upon Restart

Alice previously used an in-memory map to store product and order data for OrderFlow. Every time the application restarted, all the data was lost, forcing her to manually recreate the products during testing. Even more troublesome was that the map couldn't support complex queries (such as searching for orders within a date range), so a real database had to be used in the production environment.

(2) The Solution Using Spring Data JPA

Spring Data JPA makes persistence a matter of "defining interfaces":

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
}

Without an implementation class, Spring Data automatically generates SQL.

(3) Revenue

After Alice introduced JPA, OrderFlow data is persisted to MySQL and is retained after a restart. Complex queries are implemented using method naming conventions or @Query, and the Repository interface contains no implementation code.


3. Defining JPA Entities

(1) Core Entity Annotations

Comment Purpose Example
@Entity Marked as a JPA entity @Entity public class Product
@Table Specify table name @Table(name = "products")
@Id Mark Primary Key @Id private Long id;
@GeneratedValue Primary Key Generation Strategy @GeneratedValue(strategy = IDENTITY)
@Column Column Mapping @Column(nullable = false, length = 200)
@CreationTimestamp Auto-fill creation time @CreationTimestamp private Instant createdAt;

(1) ▶ Example: Product Entity

JAVA
@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    // Default constructor required by JPA
    protected Product() {}

    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    // Getters and setters omitted for brevity
}

Output:

TEXT
// Execution Successful
ddl-auto Value Behavior Applicable Environment
create-drop Enable table creation, disable table deletion Test
create Rebuild table on every startup Development
update Add only, do not delete; modify columns Development
validate Verify only, do not modify Production
none Take no action Production (Manual Management)
⚠️ Note: In a production environment, you must use validate or none; using update may result in data loss.


4. Repository Interface

(1) Repository Inheritance Hierarchy

100%
graph LR
    A[Repository] --> B[CrudRepository]
    B --> C[ListCrudRepository]
    B --> D[PagingAndSortingRepository]
    D --> E[JpaRepository]
    C --> E
Interface Core Methods Use Cases
CrudRepository save / findById / findAll / delete Basic CRUD
ListCrudRepository findAll returns a List Avoid Iterable conversion
PagingAndSortingRepository findAll(Pageable) Pagination and Sorting
JpaRepository All of the above + flush / saveAndFlush Most commonly used, recommended

(1) ▶ Example: ProductRepository

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

    // Derived query by method name
    List<Product> findByNameContaining(String keyword);

    List<Product> findByStockLessThan(Integer threshold);

    List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);

    // Custom JPQL
    @Query("SELECT p FROM Product p WHERE p.stock = 0")
    List<Product> findOutOfStockProducts();

    // Native SQL
    @Query(value = "SELECT * FROM products WHERE price > :minPrice ORDER BY price DESC",
           nativeQuery = true)
    List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
}

Output:

TEXT
// Execution Successful

(2) Conventions for Method Names

Keyword Example Generated SQL
findBy findByName WHERE name = ?
Containing findByNameContaining WHERE name LIKE %?%
Between findByPriceBetween WHERE price BETWEEN ? AND ?
LessThan findByStockLessThan WHERE stock < ?
OrderBy findByPriceOrderByStockDesc ORDER BY stock DESC
And / Or findByNameAndStock WHERE name = ? AND stock = ?

5. Entity-Relationship Mapping

(1) Order ↔ OrderItem: One-to-Many Relationship

100%
erDiagram
    ORDER ||--o{ ORDER_ITEM : contains
    PRODUCT ||--o{ ORDER_ITEM : included_in
    ORDER {
        bigint id PK
        varchar status
        decimal total_amount
        timestamp created_at
    }
    ORDER_ITEM {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        decimal unit_price
    }
    PRODUCT {
        bigint id PK
        varchar name
        decimal price
        int stock
    }

(1) ▶ Example: Order and OrderItem Entities

JAVA
@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String status;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }
}

@Entity
@Table(name = "order_items")
public class OrderItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
}

Output:

TEXT
// Execution Successful
Cascade Type Meaning Recommended Usage
ALL All operations are cascaded Used only for strong dependencies
PERSIST persist cascade Common
MERGE merge cascade Common
REMOVE delete cascade Use with caution
Fetch Type Behavior Use Cases
LAZY Query only on access Default recommendation to avoid N+1
EAGER Load Now @ManyToOne Default Value

6. @Query JPQL vs. Native SQL

(1) ▶ Example: JPQL and Native SQL Queries

JAVA
public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL: find orders by status with item count
    @Query("SELECT o FROM Order o WHERE o.status = :status")
    List<Order> findByStatus(@Param("status") String status);

    // JPQL: join fetch to avoid N+1
    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    // Native SQL: order statistics
    @Query(value = """
        SELECT o.status, COUNT(*) as cnt, SUM(o.total_amount) as total
        FROM orders o
        GROUP BY o.status
        """, nativeQuery = true)
    List<Object[]> getOrderStatistics();
}

Output:

TEXT
// Execution Successful
Dimension JPQL Native SQL
Syntax Entity- and Field-Based Table- and Column-Based
Database Migration Portable Not Portable
Features Limited (does not support UNION, etc.) Full
Return Type Entity / DTO Object[] / DTO

7. Database Initialization

(1) ▶ Example: schema.sql and data.sql

SQL
-- src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Output:

TEXT
CREATE TABLE
SQL
-- src/main/resources/data.sql
INSERT INTO products (name, price, stock) VALUES
    ('Laptop Pro', 1299.99, 50),
    ('Wireless Mouse', 29.99, 200),
    ('USB-C Hub', 49.99, 100);
Initialization Method Configuration Use Cases
Hibernate ddl-auto spring.jpa.hibernate.ddl-auto=update Development
schema.sql + data.sql spring.sql.init.mode=always Requires precise control of DDL
Flyway spring.flyway.enabled=true Production Migration
Liquibase spring.liquibase.enabled=true Production Migration

8. Comprehensive Example: Complete Implementation of OrderFlow JPA Persistence

JAVA
// Product.java
@Entity
@Table(name = "products")
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    protected Product() {}
    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name; this.price = price; this.stock = stock;
    }
    // getters/setters
}

// Order.java
@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String status = "PENDING";

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount = BigDecimal.ZERO;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(this, product, quantity, product.getPrice());
        items.add(item);
        totalAmount = totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
    }
    // getters/setters
}

// OrderItem.java
@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
    public OrderItem(Order order, Product product, Integer quantity, BigDecimal unitPrice) {
        this.order = order; this.product = product;
        this.quantity = quantity; this.unitPrice = unitPrice;
    }
    // getters/setters
}

// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
    List<Product> findByStockLessThan(Integer threshold);
}

❓ FAQ

Q Why must a JPA entity have a no-arg constructor?
A JPA uses reflection to create entity instances, which requires a no-arg constructor. You can annotate it with protected to prevent accidental misuse from outside. Since records do not have a no-argument constructor, they cannot be used directly as JPA entities.
Q How do I resolve a LazyInitializationException?
A The cause is accessing a lazily loaded property after the session has been closed. Solutions: 1) @Transactional Keep the session open; 2) JOIN FETCH Load all at once; 3) @EntityGraph Define a loading schedule.
Q Which should I choose, CrudRepository or JpaRepository?
A We recommend JpaRepository. It inherits from all subinterfaces and provides the most comprehensive set of methods (including pagination, sorting, and bulk operations), making it the standard choice for actual development.
Q Is ddl-auto=update safe?
A No, it is not safe. The update option only inserts data and does not delete it, which may leave behind obsolete columns. Additionally, changing column types may result in data loss. In a production environment, you must use validate along with Flyway or Liquibase to manage database migrations.
Q How do you handle the N+1 query problem?
A 1) @Query("JOIN FETCH") Load related data all at once; 2) @EntityGraph Declarative load plan; 3) @BatchSize Batch loading. This is the most critical aspect of JPA performance optimization.
Q Can schema.sql and Hibernate's ddl-auto be used together?
A We do not recommend using them together, as they may conflict. If you use Hibernate to manage DDL, you do not need schema.sql; if you use schema.sql, set ddl-auto=none.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Configure an H2 data source for OrderFlow, create the Product Entity and ProductRepository, implement a CRUD REST API for products, and persist data to H2.

  2. Advanced Problem (Difficulty ⭐⭐): Implement the one-to-many relationship between Order and OrderItem, use JOIN FETCH to solve the N+1 problem, and write a Repository method to query orders by date range.

  3. Challenge (Difficulty: ⭐⭐⭐): Switch to the MySQL data source, use Flyway to manage database migration scripts, and create the migration files V1__init_schema.sql and V2__add_order_status_index.sql.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏