本章打通「Java 物件」與「資料庫資料」之間的橋樑: 理解 ORM 的概念、設定資料庫連線、定義 Entity 與 Repository, 並學會觀察 Hibernate 在背後產生的 SQL。

ORM、JPA、Hibernate、Spring Data JPA 的關係

寫過 JDBC 的人都知道痛苦所在:手寫 SQL 字串、手動把 一欄一欄搬進物件、自己管理連線與交易。 ORM(Object-Relational Mapping,物件關聯對映)就是把這些苦工自動化:

Java 世界 資料庫世界
類別 資料表
物件(一個 實例) 資料列(一筆記錄)
欄位 欄位
呼叫 執行
呼叫 執行 或

三個常被混用的名詞,其實是三個層次:

我們寫程式時面對的是 Spring Data JPA(宣告 Repository 介面) 與 JPA 註解(、...); 執行期真正做事的是 Hibernate。 這就是為什麼設定檔中會看到 開頭的設定項。

準備資料庫

如果採用第 1 章「方式 C」的 Docker 指令,容器啟動時就自動建立了 資料庫,本節可以跳過。

若是自行安裝的 MySQL,可以手動建立資料庫:

-- 登入 MySQL 後執行:
-- utf8mb4 是 MySQL 的完整 UTF-8 實作(支援表情符號等 4-byte 字元)
CREATE DATABASE bookstore_db
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

-- 確認建立成功
SHOW DATABASES;

其實本專案的連線字串加了 , 資料庫不存在時會自動建立,手動建立主要是為了讓你熟悉 SQL 操作。

資料庫連線設定:application.yml

Spring Boot 的主設定檔位於 。 先看完整內容,重點在 (連線資訊)與 (Hibernate 行為)兩段:

JDBC 連線字串解析

lP7.8cm 組成/參數 & 說明
& 協定:使用 MySQL 的 JDBC 驅動
& 資料庫主機與連接埠
& 要使用的資料庫名稱
& 資料庫不存在時自動建立(教學方便用)
& 本機開發不使用 SSL 加密
& 允許取得伺服器公鑰(MySQL 8 驗證機制需要)
& 明確指定時區,避免時間欄位差 8 小時
& 連線編碼,避免中文變亂碼

的意思是:優先讀取環境變數 , 讀不到才用預設值 。 好處:本機開發直接用預設值;到了 Docker 或正式環境, 只要設定環境變數就能改變連線目標,完全不用改程式與設定檔。 第 7 章的 docker-compose 就是靠這招把 改成 。

ddl-auto:Hibernate 如何管理資料表結構

lP8.6cm 選項 & 行為
& 完全不動資料表結構
& 只驗證 Entity 與資料表是否一致,不一致就啟動失敗
& 比對差異,自動「新增」缺少的表與欄位(不會刪除舊欄位)——本教材採用
& 每次啟動都先刪表再重建(資料會全部消失)
& 啟動時重建、程式關閉時刪除(適合自動化測試)

很適合教學與快速原型,但正式環境不應讓程式隨意改資料表。 業界標準做法是 搭配 Flyway 或 Liquibase 這類「資料庫遷移(migration)工具」,讓每一次結構變更都有版本、可追蹤、可重播。

Entity 對映:Book

Entity 是 ORM 的核心——一個標註了 JPA 註解的 Java 類別, 描述「物件如何對映到資料表」。先看本專案的主角 :

常用 JPA 註解整理:

lP8.2cm 註解 & 用途
& 宣告為 JPA Entity,交由 Hibernate 管理
& 指定資料表名稱與約束(不寫則用類別名稱)
& 標示主鍵欄位
& 主鍵產生策略; = 資料庫自動流水號
& 欄位屬性:名稱、長度、NOT NULL、精度等
& enum 的儲存方式;一律建議
& INSERT 前的回呼(如自動填建立時間)
& UPDATE 前的回呼(如自動更新修改時間)

啟動程式後,Hibernate()會自動產生對應的資料表:

create table books (
    id bigint not null auto_increment,
    title varchar(200) not null,
    author varchar(100) not null,
    isbn varchar(20) not null,
    price decimal(10,2) not null,
    stock integer not null,
    description TEXT,
    created_at datetime(6) not null,
    updated_at datetime(6),
    primary key (id),
    constraint uk_books_isbn unique (isbn)
) engine=InnoDB;

Entity 對映:User 與 Role

使用者資料同樣用 Entity 描述。注意兩個教學重點: 資料表刻意取名 ( 是 MySQL 保留字), 以及角色 enum 用 儲存。

Repository:宣告介面就完成資料存取層

Spring Data JPA 最神奇的地方:只要宣告介面、繼承 , 啟動時就會自動產生實作,完全不用寫 SQL 或實作類別。

衍生查詢:用方法名稱寫查詢

Spring Data JPA 會解析方法名稱、自動產生查詢,常用關鍵字:

P2.2cmP5.6cmP4.6cm 關鍵字 & 方法名稱範例 & 產生的查詢條件
& &
& &
& &
& & 不分大小寫比對
/ & &
& &
& &

方法名稱寫不出來的複雜查詢,就用 自己寫 JPQL (如 )。 JPQL 長得像 SQL,但操作對象是 Entity 與屬性(、), 不是資料表與欄位,由 Hibernate 負責翻譯成真正的 SQL。

眼見為憑:觀察 Hibernate 的 SQL

設定檔開啟了 與參數綁定日誌, 呼叫 API 時主控台會即時印出 Hibernate 產生的 SQL,例如查詢單本書籍:

Hibernate:
    select
        b1_0.id, b1_0.author, b1_0.created_at, b1_0.description,
        b1_0.isbn, b1_0.price, b1_0.stock, b1_0.title, b1_0.updated_at
    from
        books b1_0
    where
        b1_0.id=?
-- 綁定參數日誌:binding parameter [1] as [BIGINT] - [1]

接下來測試每一個 API 時,都回頭看一眼主控台的 SQL—— 「我呼叫的方法」與「實際執行的 SQL」對照久了,ORM 就再也不是黑魔法。

交易管理:@Transactional

一個交易內的多個資料庫操作,要嘛全部成功(commit), 要嘛全部取消(rollback),不會停在「做一半」的狀態。 經典例子:轉帳=「A 扣款 + B 入帳」必須綁在同一個交易, 否則扣了款卻入帳失敗,錢就憑空消失了。

在 Spring 中只要在 Service 方法標上 :

  • 方法開始時開啟交易,正常結束時 commit。

  • 方法拋出 時自動 rollback。

  • 查詢類方法標 , 告訴 Hibernate 與資料庫「這是唯讀操作」,可以做效能最佳化。

實際用法會在下一章的 中看到。