Mapping Types and Identifiers#

JPA 透過將 Java 物件型別對應到資料庫結構來解決 Object/Relational mismatch。JPA 使用三種主要的 Object-Relational mapping 元素:typeembeddableentity。Entity 與 embeddable 都透過 Hibernate Type(s) 將資料庫欄位型別與 Java 值物件關聯起來,兩者的主要差異在於 entity 必須擁有 identifier(對應資料庫 primary key),而 embeddable 則不能有自己的 identity。

不一定要對應所有資料庫欄位。有時使用一個 root entity 搭配數個 sub-entity 更實用,讓每個 business case 僅 fetch 所需資訊,同時仍享有 entity state management 的好處。

Embeddable type 透過 composition 將多個屬性組合為可重用的元件。當 entity 包含 embeddable type 時,其所有屬性會成為 owner entity 的一部分。由於 type 和 identifier 的選擇會顯著影響整體應用效能,本章深入探討這兩個主題。

Types#

JDBC 為每種支援的資料庫型別定義了 java.sql.JDBCType 列舉。Hibernate 在 JDBC 之上建構,負責 JDBC type 與 Java 對應物(primitives 或 Objects)之間的對應。

Figure 9.1: Type Mapping

Primitive Types#

Hibernate 定義了多種 primitive type 對應,涵蓋 booleanbyteshortcharintlongfloatdouble 等。由於 boolean 在不同資料庫中可能以 BITBYTEBOOLEANCHAR 表示,Hibernate 提供了四種 Type(s) 來解析 boolean primitive type(BooleanTypeNumericBooleanTypeTrueFalseTypeYesNoType)。

只有 non-nullable 的資料庫欄位才能對應到 Java primitives(booleanbyteshortcharintlongfloatdouble)。對於 nullable 欄位,應使用 primitive wrapper(BooleanByteShortCharIntegerLongFloatDouble)。

String Types#

Java String 可消耗任意大小的記憶體,而資料庫則區分有限長度(VARCHARNVARCHAR)與無限長度(TEXTNTEXTBLOBNCLOB)的型別。Hibernate 定義了對應的 Type(s)

  • StringTypeVARCHAR / String
  • StringNVarcharTypeNVARCHAR / String
  • TextTypeLONGVARCHAR / String
  • MaterializedClobTypeCLOB / String
  • MaterializedNClobTypeNCLOB / String

Date/Time Types#

時間處理因時區、閏秒和日光節約時間而複雜,Hibernate 提供了大量 time-related Type(s) 來涵蓋多種 Java 與資料庫表示法:

  • DateType / TimeType / TimestampType – 對應 DATETIMETIMESTAMP
  • CalendarType / CalendarDateType / CalendarTimeType – 處理 CalendarGregorianCalendar
  • TimeZoneType – 將 TimeZone 儲存為 VARCHAR

建議將 timestamp 儲存為 UTC(Coordinated Universal Time),並在 data layer 進行時區轉換。這是處理時間問題的常見最佳實務。

Numeric Types#

  • BigIntegerTypeNUMERIC / BigInteger
  • BigDecimalTypeNUMERIC / BigDecimal

Oracle 可表示多達 38 位數字,超出 java.lang.Long(8 bytes)的範圍,此時需要使用 BigIntegerBigDecimal

Binary Types#

資料庫提供多種 binary 儲存方式(RAWVARBINARYBYTEABLOBCLOB)。Hibernate 的 binary type 對應包含:

  • BinaryType / MaterializedBlobTypebyte[]Byte[]
  • BlobType / ClobType / NClobType – JDBC BlobClob
  • SerializableType / SerializableToBlobType – Java Serializable 物件

UUID Types#

持久化 Java UUID 有多種方式,最有效率的是資料庫特定的 UUID column type:

  • UUIDBinaryTypeBINARY / UUID
  • UUIDCharTypeVARCHAR / UUID
  • PostgresUUIDTypeOTHER / UUID

當資料庫未原生支援 UUID 時,BINARY 型別比 VARCHAR 需要更少的 bytes,因此相關 index 的記憶體佔用也更小。

Other Types#

Hibernate 也能對應 Java Enum(s)ClassURLLocaleCurrency 等型別,均使用 VARCHAR 或其他適當的 JDBC type。

Custom Types#

開發者不限於使用內建 Type(s),可透過實作 UserType 介面來新增自訂型別。書中以 PostgreSQL 的 inet type 為例,說明如何建立自訂的 IPv4Type

  • 建立 immutableIPv4 wrapper class 封裝 IP 位址
  • 繼承 ImmutableType 基底類別,實作 get()set() 方法處理型別轉換
  • 使用 @Type annotation 指示 Hibernate 使用自訂型別
  • 搭配 PostgreSQL 的 GiST indexinet_ops operator class),可使用進階查詢功能(如子網路比對)

選擇適當的資料庫特定型別不僅能節省儲存空間,還能存取進階查詢功能。例如 PostgreSQL 的 inet type 每個 IPv4 位址只需 7 bytes,而 VARCHAR(18) 需要最多 18 bytes。

Identifiers#

所有資料庫表都必須有 primary key column,確保每筆資料列可被唯一識別。Primary key 可以是 natural key(具有現實世界意義)或 surrogate key(合成產生的識別碼)。

  • Natural key 的唯一性由現實世界的序列產生器保證,但可能在跨國系統中產生衝突,且 compound natural key 會因多欄位 join 而影響效能
  • Surrogate key 獨立於資料列內容,可由 numerical sequence 或 pseudorandom number generator(如 UUID)產生

Numerical sequence 比 UUID key 更受推薦,因為它佔用更少空間且對 index 更友善。UUID 佔 128 bits(INTEGER 的四倍、BIGINT 的兩倍),且非循序特性會造成 clustered index 的 fragmentation。

UUID Identifiers#

某些企業系統使用 UUID primary key。UUID key 可由應用程式透過 java.util.UUID 產生,或由資料庫系統指派。各資料庫的 UUID 支援情況不同:

  • Oracle – 無 UUID type,使用 RAW(16) 搭配 SYS_GUID() 函式
  • SQL Server – 使用 uniqueidentifier type,搭配 NEWID()NEWSEQUENTIALID()(pseudo-sequential,避免 fragmentation)
  • PostgreSQL – 原生支援 UUID type,但需由 data access layer 產生 identifier
  • MySQL – 使用 BINARY(16) 搭配 UUID() 函式

若資料庫無內建 UUID type,BINARY(16) column type 是首選。CHAR(32) 雖可儲存 UUID 文字表示,但額外的空間開銷使其不太理想。

The Assigned Generator#

省略 @GeneratedValue annotation 時,Hibernate 使用 assigned identifier,讓 data access layer 自行控制 identifier 產生流程。此方式可將 identifier 產生的負擔從資料庫端卸載到應用程式端。

Legacy UUID Generator(uuid)#

uuid generator 產生 hex 格式的 UUID 字串,使用 8-8-4-8-4 hex digit layout,不符合 RFC 4122 標準(標準格式為 8-4-4-4-12)。

Newer UUID Generator(uuid2)#

uuid2 generator 符合 RFC 4122 標準(variant 2),支援 java.lang.UUIDbyte[]String 等 Domain Model 物件型別。

由於 uuid2 符合 RFC 4122 且可使用 BINARY column type,UUIDGenerator 應優先於 legacy UUIDHexGenerator

Numerical Identifiers#

Numerical surrogate key 通常是較佳選擇,因為佔用空間少且 index 在 sequential identifier 下表現更好。JPA 定義了以下 GenerationType

  • IDENTITY – 對應資料庫 identity column(auto-increment)
  • SEQUENCE – 透過呼叫資料庫 sequence 配置 identifier
  • TABLE – 使用獨立表格模擬 database sequence(適用於不支援 sequence 的資料庫)
  • AUTO – 根據當前 database dialect 決定策略

Identity Generator#

Identity column(SQL:2003 標準)使用輕量級 locking 機制進行遞增,效率高。但 新指派的值只能在執行 insert statement 之後才能得知,這意味著:

  • Hibernate 在 persist() 呼叫時就必須發出 insert statement,打破了 transactional write-behind caching 的語意
  • 使用 identity generator 的 entity 無法參與 JDBC batch updates

Sequence Generator#

Database sequence(SQL:2003 標準)相較於 identity column 有以下優勢:

  • 同一個 sequence 可用於多個 column,甚至跨 table
  • 值可以被 preallocated 以提升效能
  • 支援 incremental steps,可搭配 application-level optimization
  • Sequence call 可與 insert statement 解耦,不會停用 JDBC batch updates

Database sequence 是最有效率的 Hibernate identifier 選擇,既允許 sequence call optimizer,又不影響 JDBC batching。

Table Generator#

當資料庫不支援 sequence 時,JPA 提供 table generator 作為替代方案。使用一張資料庫表儲存最新的 sequence value,並以 row-level locking 防止併發衝突。

與 identity column 和 sequence(單次 request 即可遞增)不同,table generator 每次更新需要三個步驟:

  1. 執行 lock statement 確保 sequence value 不被重複配置
  2. 在 data access layer 遞增 current value
  3. 將 new value 寫回資料庫,並 commit secondary transaction 以釋放 row-level lock

Table generator 引入 serializable execution(row-level lock),在高併發環境下會嚴重影響可擴展性。由於 row-level lock 是 transactional 的(只在 transaction 結束時釋放),需要使用獨立的 database transaction 來取得 sequence value,這會對 connection pool 產生額外壓力。Identity column 和 sequence 為高併發情境高度最佳化,應是首選。

Optimizers#

Sequence 和 table generator 都支援 optimizer 策略,可減少呼叫資料庫的次數以提升 identifier 產生效能。

Hibernate 5 預設使用增強版的 SequenceStyleGeneratorTableGenerator(取代 legacy 的 SequenceGeneratorSequenceHiLoGeneratorMultipleHiLoPerTableGenerator),且預設啟用 hibernate.id.new_generator_mappingstrue

內建 optimizer 包含:

  • noneNoopOptimizer)– 每個 identifier 都需要一次 database roundtrip
  • hi/loHiLoOptimizer)– 使用 legacy hi/lo algorithm 配置 identifier
  • pooledPooledOptimizer)– hi/lo 的增強版,可與不知道此 optimizer 的外部系統互通
  • pooled-loPooledLoOptimizer)– pooled 的變體,database sequence value 代表 lo value 而非 hi value

預設使用 pooled optimizer。若設定 hibernate.id.optimizer.pooled.prefer_lotrue,則預設使用 pooled-lo

Hi/Lo Algorithm#

Hi/lo algorithm 將 sequence domain 分割為 hi groups。每個 hi value 同步取得,每個 hi group 被分配固定數量的 lo entries,可在離線狀態下配置而不會衝突:

  • Hi token 由 database sequence 或 table generator 指派
  • Identifier range:Id = [n * (hi - 1) + 1, n * hi](n 為 increment size)
  • 當所有 lo values 用完,取得新的 hi value,循環繼續

Figure 9.2: The hi/lo algorithm

Hi/lo optimizer 的主要限制是:由於 database sequence 只指派 group values,所有資料庫客戶端都必須知道此 algorithm。DBA 手動插入資料時也必須遵循 hi/lo 規則。因此 Hibernate 提供了 pooled 和 pooled-lo 等與外部客戶端互通的 optimizer。

Pooled Optimizer#

Pooled optimizer 將 database sequence value 編碼到 identifier range 的邊界中,使實際的 sequence call 不會干擾 identifier 配置流程:

  • Identifier range:Id = [(hi - n) + 1, hi](n 為 increment size)
  • 第一次 sequence call 產生 lo value,第二次確定 hi value
  • 外部系統可直接使用 sequence call 取得值並插入資料,不會與 Hibernate 的 identifier 配置衝突

Figure 9.3: The pooled optimizer

Pooled-Lo Optimizer#

Pooled-lo 是 pooled 的變體,database sequence value 代表 lo value(range 的下界):

  • Identifier range:Id = [lo, (lo + n) - 1](n 為 increment size)
  • 第一次 sequence call 產生 lo value,後續以此為基礎配置 identifier
  • 同樣可與外部系統互通

Figure 9.4: The pooled-lo optimizer

Optimizer Gain#

使用 optimizer 可顯著減少 identifier 配置的時間:

  • Sequence generator + pooled optimizer – 即使 database sequence 本身已很快,pooled optimizer 仍能大幅降低 identifier 產生時間。對 write-intensive 應用,increment size 應根據每次 transaction 插入的列數調整

Figure 9.5: Sequence pooled optimizer gain

  • Table generator + pooled optimizer – 由於 row-level locking 和額外的 database connection switch 開銷,table generator 效率低於 database sequence,但 pooled optimizer 同樣能有效改善效能

Figure 9.6: Table pooled optimizer gain

Identifier Generator Performance#

併發環境下各 identifier generator 的效能比較:

  • Identity vs. Table – 即使 identity generator 無法使用 JDBC batching,仍然能勝過使用 pooled optimizer(increment size = 100)的 table generator。隨著 thread 數增加,table generator 的效能下降更為明顯

Figure 9.7: Identity vs. Table

  • Sequence vs. Table – Sequence generator 既能使用 pooled optimizer 又能搭配 JDBC batch insert,與 table generator 的差距更大。在高併發環境中,table generator 的 row-level locking 和 connection switch 會造成 serial execution

Figure 9.8: Sequence vs. Table

Database sequence 是最有效率的 Hibernate identifier 選擇。它使用輕量級的同步機制,比 row-level locking 的併發控制更具可擴展性,同時允許 sequence call optimizer 且不影響 JDBC batching。