iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Software Development

spring boot 3 學習筆記系列 第 24

Day24 - Spring Data JPA 實戰:從 0 到 1 打造 Entity 與 Primary key 設計

  • 分享至 

  • xImage
  •  

Day23 內容裡,我們學會了如何設定 H2 和 PostgreSQL 資料庫連線。現在要來學習最重要的一步:定義一個 Entity (實體)

Entity 是一個特殊的 Java 物件,它會被 JPA「對映 (Mapping)」到資料庫中的一張資料表。學會定義 Entity,就等於學會了如何設計你的資料庫結構。今天將從頭開始,一步步建立一個功能完整的 Customer (客戶) Entity。

學習目標

完成今日學習後,你將能夠:

  1. 瞭解如何使用 JPA 定義 Entity 資料表:熟練運用 @Entity@Table@Column 來建立你的資料模型。
  2. 學會使用欄位限制條件與驗證:掌握如何透過 @Column 的屬性及 @Size 等 Annotation (註解) 來確保資料的完整性與正確性。
  3. 熟悉 PostgreSQL UUID 當作主鍵 (Primary key) 的最佳實務:學會使用 @GeneratedValue 搭配 UUID,這是現代化應用程式中非常推薦的主鍵 (Primary Key) 策略。
  4. 設計 ENUM (列舉) 欄位並正確存取:了解如何優雅地將程式中的狀態 (例如:啟用/停用) 對應到資料庫欄位。

Entity 的基本骨架 (@Entity & @Table)

首先,我們需要一個普通的 Java 類別 (POJO),然後透過加上 Annotation 把它變成 JPA 管理的 Entity。

  • @Entity:這是最重要的 Annotation,它告訴 JPA:「嘿!這個類別不是普通的類別,請幫我管理它,並將它對應到資料庫的一張資料表。」
  • @Table(name = "customers"):這個 Annotation 是選用的,但強烈建議使用。它能讓我們明確指定這個 Entity 對應的資料表名稱。如果不寫,JPA 預設會使用類別名稱 (例如 Customer 會對應到 customer 表)。明確指定 customers (複數) 是業界常見的命名慣例。
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
public class Customer {
    // ... 接下來我們會在這裡加入欄位
}

主鍵的最佳選擇 - UUID (@Id & @GeneratedValue)

每一張資料表都需要一個主鍵 (Primary key) 來唯一識別每一筆紀錄。傳統上我們常用自動遞增的數字 (Auto-increment Integer),但在分散式系統或需要更高安全性的現代應用中,UUID (Universally Unique Identifier) 是更佳的選擇。

  • 為什麼用 UUID? 幾乎保證全球唯一,避免了連續數字可能洩漏業務資訊 (例如訂單量) 的問題,也方便在不同資料庫之間遷移或合併資料。
  • @Id:用來標示哪個欄位是主鍵 (Primary key)。
  • @GeneratedValue(strategy = GenerationType.UUID):這行是關鍵!它告訴 JPA,當我們新增一筆資料時,請自動生成一個 UUID 作為主鍵 (Primary key) 的值。這是 JPA 2.1 / 3.1 之後的標準作法,Hibernate 會聰明地將它對應到 PostgreSQL 原生的 uuid 資料類型,效能非常好。
import java.util.UUID;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
public class Customer {

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

    // ... 其他欄位
}

定義欄位、長度與唯一約束 (@Column & @Size)

現在我們來定義 nameemail 兩個欄位,並加上一些規則。

區分「資料庫約束」與「應用程式驗證」

這是一個非常重要的觀念:

  1. 資料庫約束 (Database Constraint):由 @Column Annotation 設定,它會影響 DDL (Data Definition Language) 指令,直接在資料庫層級建立規則。例如,VARCHAR(50)UNIQUE
  2. 應用程式驗證 (Application Validation):由 @Size@NotNull 等 Bean Validation Annotation 設定。它在 Java 應用程式層級運作,在執行 SQL 語句之前就檢查資料是否合法。這能提供更即時、更友好的錯誤回饋。

最佳實務是兩者都用! 這樣可以做到雙重保險。

  • name 欄位:我們希望它在資料庫中是 VARCHAR(50),並且在應用程式中驗證長度介於 2 到 50 之間。
  • email 欄位:我們希望它在資料庫中是唯一的 (UNIQUE 約束),避免重複註冊。
// ... import ...
import jakarta.persistence.Column;
import jakarta.validation.constraints.Size;

@Entity
@Table(name = "customers")
public class Customer {

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

    // @Column 定義了資料庫層級的屬性
    // length=50 會在 DDL 中產生 VARCHAR(50)
    // nullable=false 會產生 NOT NULL 約束
    @Column(name = "name", length = 50, nullable = false)
    // @Size 則是在應用程式層級進行驗證
    @Size(min = 2, max = 50)
    private String name;

    // unique=true 會在資料庫中為此欄位建立一個 UNIQUE 約束
    @Column(name = "email", unique = true, nullable = false)
    private String email;

    // ... 其他欄位
}

註:
@Size 註解來自 Jakarta Bean Validation API。因此需要在 pom.xml 中加入 Bean Validation 的相依套件。

<!-- Bean Validation 驗證相依性,提供 @Size, @NotNull 等驗證註解 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

加入這個相依套件後,您就可以使用各種驗證註解,如:

  • @Size - 驗證字串長度或集合大小
  • @NotNull - 驗證欄位不為 null
  • @NotEmpty - 驗證欄位不為空
  • @Email - 驗證電子郵件格式
  • @Pattern - 驗證正則表達式加入這個相依套件後,您就> 可以使用各種驗證註解,如:
  • @Size - 驗證字串長度或集合大小
  • @NotNull - 驗證欄位不為 null
  • @NotEmpty - 驗證欄位不為空
  • @Email - 驗證電子郵件格式
  • @Pattern - 驗證正則表達式

用 ENUM 處理狀態 (@Enumerated)

在應用程式中,我們常用 ENUM 來表示一組固定的狀態,例如 ACTIVE (啟用) 和 INACTIVE (停用)。JPA 提供了 @Enumerated 讓我們可以方便地將 ENUM 存入資料庫。

@Enumerated 有兩種儲存策略 (Strategy):

  1. EnumType.ORDINAL:儲存 ENUM 的順序 (從 0 開始的整數)。這是一個危險的選項! 如果未來你修改了 ENUM 的順序 (例如在中間插入一個新的狀態),所有舊資料的意義都會錯亂。強烈不建議使用
  2. EnumType.STRING:儲存 ENUM 的名稱 (例如 "ACTIVE", "INACTIVE")。這是推薦的作法,因為它非常直觀,且不受 ENUM 順序變更的影響。雖然會多佔用一些儲存空間,但換來的可讀性與安全性是值得的。

首先,定義我們的狀態 ENUM:

CustomerStatus.java

public enum CustomerStatus {
    ACTIVE,
    INACTIVE
}

然後,在 Customer Entity 中使用它:

// ... import ...
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumType;

@Entity
@Table(name = "customers")
public class Customer {
    // ... id, name, email 欄位 ...

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private CustomerStatus status;

    // ... Getters and Setters ...
}

完整範例Customer Entity

好了,讓我們把所有部分組合起來,看看完整的 Customer.javaCustomerStatus.java 檔案。

CustomerStatus.java

public enum CustomerStatus {
    ACTIVE,
    INACTIVE
}

Customer.java

import com.example.demo.enums.CustomerStatus;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;

import java.util.UUID;

/**
 * Customer Entity 代表 'customers' 資料表.
 */
@Entity
@Table(name = "customers")
public class Customer {

    /**
     * 主鍵 (Primary Key).
     * 使用 UUID 類型,並由 JPA 自動生成。
     * 對應到 PostgreSQL 的 'uuid' 類型。
     */
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = "id", updatable = false, nullable = false)
    private UUID id;

    /**
     * 客戶名稱.
     * 資料庫層級:VARCHAR(50), NOT NULL
     * 應用程式層級:長度必須在 2 到 50 之間
     */
    @Column(name = "name", length = 50, nullable = false)
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;

    /**
     * 客戶 Email.
     * 資料庫層級:UNIQUE, NOT NULL
     * uniqueness 會在資料庫層級強制執行。
     */
    @Column(name = "email", unique = true, nullable = false)
    private String email;

    /**
     * 客戶狀態.
     * 使用 ENUM 類型,並在資料庫中儲存為字串 ("ACTIVE", "INACTIVE")。
     */
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private CustomerStatus status;

    // --- Constructors, Getters, and Setters ---

    public Customer() {
    }

    public UUID getId() {
        return id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public CustomerStatus getStatus() {
        return status;
    }

    public void setStatus(CustomerStatus status) {
        this.status = status;
    }
}

總結

已經成功建立了一個結構良好、包含多種實用約束的 JPA Entity。

  • @Entity@Table 開始,定義了類別與資料表的對映。
  • 接著使用 @Id@GeneratedValue 實現了現代化的 UUID 主鍵策略。
  • 然後透過 @Column@Size 為欄位加上了資料庫層級和應用程式層級的雙重保障。
  • 最後,用 @Enumerated(EnumType.STRING) 優雅地處理了狀態欄位。

相關資料來源


上一篇
Day23 - Spring Data JPA 入門:從 H2 到 PostgreSQL
下一篇
Day25 - Spring Data JPA Repository 實戰:從 CRUD 到進階查詢
系列文
spring boot 3 學習筆記30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言