【Spring Boot】第9.2課-使用 JPA 設計實體類別與 MySQL 資料表欄位

本文最後更新於:2025-03-22

在上一篇,我們了解可藉由定義實體類別,讓 Spring Data JPA 建立出資料表。而本文會介紹各種設定資料表欄位的方式,包含欄位名稱、長度與唯一性等。

此外也會重複運用具有相同設定的欄位,包含嵌入物件與繼承基底類別。最後說明如何在插入或更新資料時,自動在欄位填入日期時間與使用者資料。


一、實體類別介紹

Spring Data JPA 允許我們在程式中,透過定義實體類別的方式,來設計資料表(table)。所謂的實體類別,指的是要儲存到資料庫的資料類別。

以下建立一個實體類別,描述了學生資料。

import jakarta.persistence.*;

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int grade;
    private BloodType bloodType;
    private LocalDate birthday;

    // getter, setter ...
}

該類別叫做「Student」,目前包含 id、名字、年級、血型與生日,共 5 個欄位。

其中的「BloodType」是個「列舉」(enumeration)類別,包含 4 種血型。

public enum BloodType {
    A, B, O, AB
}

二、設計資料表欄位

實體類別會對應到資料庫的 table,因此維護該類別,就相當於維護 table。在實體類別設計欄位時,會搭配使用 @Column 注解,透過它的參數進行設定。

參數 設定項目 補充說明
name 欄位名稱 針對同一個欄位,我們可以在實體類別與 table,分別使用不同的名稱。
length 字串長度 超出的部份會被截斷(truncate)。
nullable 值是否可為 null Java 基本型態預設為 false;參考型態(如 String)預設為 true。
unique 值是否唯一 若為 true,會自動建立「唯一索引」。
precision 整數與小數的總位數 適用於 Java 的 BigDecimal 型態;MySQL 的 DECIMAL 型態。
scale 小數在 precision 所佔的位數 適用於 Java 的 BigDecimal 型態;MySQL 的 DECIMAL 型態。

以下是將 @Column 注解用在實體類別上。

@Entity
@Table(name = "student")
public class Student {

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

    @Column(name = "name", length = 30, unique = true, nullable = false)
    private String name;

    @Column(name = "grade")
    private int grade;

    @Column(name = "blood_type")
    @Enumerated(EnumType.STRING)
    private BloodType bloodType;

    @Column(name = "birthday", nullable = false)
    private LocalDate birthday;

    // getter, setter ...
}

另外,如果欄位型態是一個「列舉」(Enum),不妨冠上 @Enumerated 注解,並設定以 Enum 值的名字來儲存。否則預設會以序數(ordinal)來儲存,可讀性較差。

在網路上其他文章中,讀者可能會注意到那些範例程式,即便實體類別的欄位名與 table 欄位一致,依然會再度使用 @Column 注解來設定 table 欄位名稱。筆者認為這樣的好處,是能避免日後在實體類別修改欄位名稱,無意間影響與 table 的對應關係。

設定完成後,請讀者啟動程式,讓 Spring Data JPA 自動在資料庫產生 table。下圖是在 MySQL 使用 DESCRIBE 指令,確認 table 的定義。

在結果中,可看到欄位型態、是否為唯一欄位,或者是否允許 null 值等資訊。

三、嵌入物件欄位

我們也可透過「嵌入」(embed)的方式,將自定義的物件放入實體類別中。

以下建立叫做「Contact」的類別,描述了聯繫方式。

@Embeddable
public class Contact {
    private String email;
    private String phone;

    // getter, setter ...
}

此類別包含信箱與電話這 2 個欄位。為了將它嵌入到實體類別中,需冠上 @Embeddable 注解。這麼做的好處,是日後若有其他實體類別(如老師、員工、公司等),均可重複使用。

接著回到 Student 實體類別,使用 @Embedded 注解嵌入 Contact 類別。

@Entity
@Table(name = "student")
public class Student {
    // ...

    @Embedded
    @AttributeOverride(name = "email", column = @Column(name = "contact_email"))
    @AttributeOverride(name = "phone", column = @Column(name = "contact_phone"))
    private Contact contact;

    // getter, setter ...
}

為了在實體類別對嵌入的物件欄位進行設定,需使用 @AttributeOverride 注解。它具有兩個參數,name 是指向內部的欄位;而 column 則是傳入前面介紹過的 @Column 注解,進行設定。

四、自動填入日期時間與使用者

(一)前言

我們可能會想記錄每一筆資料的異動資訊。比方說文章是何時發表的、何時修改的、誰建立的、誰更新的。透過 Spring Data 提供的「JPA Auditing」功能,可以幫助我們做這件事。

請先在啟動類別冠上 @EnableJpaAuditing 注解,以啟用此功能。

@SpringBootApplication
@EnableJpaAuditing
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

接著在實體類別類別冠上 @EntityListeners 注解,註冊這個功能。

@Entity
@Table(name = "student")
@EntityListeners(AuditingEntityListener.class)
public class Student {
    // ...
}

(二)填入日期時間

以下在 Student 實體類別中,添加了 2 個欄位,分別代表資料的建立與更新時間。

@Entity
@Table(name = "student")
@EntityListeners(AuditingEntityListener.class)
public class Student {
    // ...
    
    @Column(name = "created_time", nullable = false)
    @CreatedDate
    private LocalDateTime createdTime;

    @Column(name = "updated_time", nullable = false)
    @LastModifiedDate
    private LocalDateTime updatedTime;

    // getter, setter ...
}

只要在欄位分別冠上 @CreatedDate@LastModifiedDate 注解即可。Spring Data JPA 會在插入或更新資料時,將日期時間的值賦予給這些欄位。

當插入資料時,具有 @CreatedDate@LastModifiedDate 注解的欄位,都會同時被賦予值。而後續更新資料時,只會刷新 @LastModifiedDate 的欄位。

(三)填入使用者

以下在 Student 實體類別中,添加了 2 個欄位,分別代表資料的建立者與更新者。

@Entity
@Table(name = "student")
@EntityListeners(AuditingEntityListener.class)
public class Student {
    // ...
    
    @Column(name = "created_by", nullable = false)
    @CreatedBy
    private String createdBy;

    @Column(name = "updated_by", nullable = false)
    @LastModifiedBy
    private String updatedBy;

    // getter, setter ...
}

接著在欄位分別冠上 @CreatedBy@LastModifiedBy 注解。Spring Data JPA 會在插入或更新資料時,將代表使用者的值賦予給這些欄位。

那麼使用者的資料從哪裡來呢?這時我們需要準備一個實作 AuditorAware 介面的元件,讓它告訴 Spring Data JPA。

@Component
public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        String randomId = UUID.randomUUID().toString();
        return Optional.of(randomId);
    }
}

這個 AuditorAware 介面接收一個泛型類別,它需要與那些冠上 @CreatedBy@LastModifiedBy 注解的欄位型態相同。由於實體類別中的 createdBy 和 updatedBy 欄位是字串,因此泛型類別應傳入 String。

接著覆寫 getCurrentAuditor 方法,它會在資料正要被儲存時,由 Spring Data JPA 觸發。其回傳值會被賦予給具有這兩種注解的欄位。此處回傳隨機字串,當作使用者的 id。

附帶一提,若讀者所開發的專案有引進 Spring Security,那就可以從「Security Context」獲取當前使用者的資訊。

五、共享相同的欄位設定

(一)用於嵌入欄位

在本文第三節,我們在 Student 實體類別嵌入自定義的 Contact 類別,代表聯繫方式。

Spring Data JPA 並不支援直接在 Contact 類別內部使用 @Column 注解來設定。因此若將 Contact 也嵌入到其他實體類別,且內部欄位的設定均相同,那麼就會持續使用 @AttributeOverride 注解,寫出重複的設定。

但重複利用欄位設定這個想法,還是可以實現的。我們首先需建立一個父類別,將要共享的欄位抽離過去。此外,為了強調該父類別是「基底類別」,不會被用來建立物件,故宣告為抽象。

@MappedSuperclass
public abstract class BaseContact {
    
    @Column(name = "contact_email", length = 50)
    private String email;

    @Column(name = "contact_phone", length = 50)
    private String phone;
    
    // getter, setter ...
}

該基底類別冠上了 @MappedSuperclass 注解,用途是讓子類別繼承後,能將父類別的欄位也連動到資料庫的 table 欄位上。

@Embeddable
public class Contact extends BaseContact {
}

如此一來,在 Student 實體類別就不必透過 @AttributeOverride 注解來設定欄位值了。除非有少數特例需個別設定,否則可將其移除。

(二)用於實體類別

這個 @MappedSuperclass 注解,亦可用於那些實體類別都具備的欄位,例如 id、建立與更新資料的時間、建立與更新資料的人。

以下針對實體類別建立了一個基底類別。

@MappedSuperclass
public abstract class BaseEntity {

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

    @Column(name = "created_time", nullable = false)
    @CreatedDate
    private LocalDateTime createdTime;

    @Column(name = "updated_time", nullable = false)
    @LastModifiedDate
    private LocalDateTime updatedTime;

    @Column(name = "created_by", nullable = false)
    @CreatedBy
    private String createdBy;

    @Column(name = "updated_by", nullable = false)
    @LastModifiedBy
    private String updatedBy;

    // getter, setter ...
}

而實體類別只要留下自身特有的欄位,並繼承該基底類別即可。一旦基底類別的設計有改動,則所有實體類別都會生效。

@Entity
@Table(name = "student")
@EntityListeners(AuditingEntityListener.class)
public class Student extends BaseEntity {
    // ...
}

本文介紹了如何透過實體類別,設計資料庫中 table 的欄位,讀者可透過重啟程式,印證 table 的變化。

我們尚未印證本文第三節的 AuditorAware 元件,在插入或更新資料時的效果。下一篇將使用 Spring Data JPA 提供的介面,實際進行 CRUD。


本文的完成後專案,請點我

上一課:【Spring Boot】第9.1課-準備 MySQL 資料庫與認識 Spring Data JPA

下一課:【Spring Boot】第9.3課-使用 JPA Repository 存取 MySQL 資料庫


張貼留言: