Java 持久化/基本屬性
基本屬性是指屬性類為簡單型別,如 String、Number、Date 或基本型別。基本屬性的值可以直接對映到資料庫中的列值。下表總結了基本型別及其對映到的資料庫型別。
| Java 型別 | 資料庫型別 |
| String (char, char[]) | VARCHAR (CHAR, VARCHAR2, CLOB, TEXT) |
| Number (BigDecimal, BigInteger, Integer, Double, Long, Float, Short, Byte) | NUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE) |
| int, long, float, double, short, byte | NUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE) |
| byte[] | VARBINARY (BINARY, BLOB) |
| boolean (Boolean) | BOOLEAN (BIT, SMALLINT, INT, NUMBER) |
| java.util.Date | TIMESTAMP (DATE, DATETIME) |
| java.sql.Date | DATE (TIMESTAMP, DATETIME) |
| java.sql.Time | TIME (TIMESTAMP, DATETIME) |
| java.sql.Timestamp | TIMESTAMP (DATETIME, DATE) |
| java.util.Calendar | TIMESTAMP (DATETIME, DATE) |
| java.lang.Enum | NUMERIC (VARCHAR, CHAR) |
| java.util.Serializable | VARBINARY (BINARY, BLOB) |
在 JPA 中,基本屬性透過 @Basic 註解或 <basic> 元素進行對映。支援的型別和轉換取決於 JPA 實現和資料庫平臺。某些 JPA 實現可能支援不同資料型別之間的轉換或其他型別,或具有擴充套件的型別轉換支援,有關更多詳細資訊,請參閱 高階 部分。任何使用不直接對映到資料庫型別的型別的基本屬性都可以序列化為二進位制資料庫型別。
在 JPA 中對映基本屬性的最簡單方法是不做任何操作。任何沒有其他註解並且不引用其他實體的屬性將自動對映為基本屬性,即使不是基本型別,也會被序列化。屬性的列名將預設為與屬性名相同,並轉換為大寫。如果您的類中存在一個您不打算持久化的屬性,自動對映有時會產生意外結果。您必須使用 @Transient 註解或 <transient> 元素標記任何此類非持久化欄位。
雖然自動對映使快速原型設計變得容易,但您通常會達到需要控制資料庫模式的階段。要為基本屬性指定列名,請使用 @Column 註解或 <column> 元素。列註解還允許您指定其他資訊,例如資料庫型別、大小和某些約束。
@Entity
public class Employee {
// Id mappings are also basic mappings.
@Id
@Column(name="ID")
private long id;
@Basic
@Column(name="F_NAME")
private String firstName;
// The @Basic is not required in general because it is the default.
@Column(name="L_NAME")
private String lastName;
// Any un-mapped field will be automatically mapped as basic and column name defaulted.
private BigDecimal salary;
// Non-persistent fields must be marked as transient.
@Transient
private EmployeeService service;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id">
<column name="ID"/>
</id>
<basic name="firstName">
<column name="F_NAME"/>
</basic>
<basic name="lastName">
<column name="L_NAME"/>
</basic>
<transient name="service"/>
</attributes>
</entity>
- 參見 轉換
- 一個常見的問題是,從物件寫入的資料(例如字串)在從資料庫讀取時被截斷。這通常是由於列長度不足以處理物件的資料。在 Java 中,字串沒有最大大小限制,但在資料庫中,
VARCHAR欄位有最大大小限制。您必須確保在建立表時設定的列長度足夠大,以處理任何物件值。對於非常大的字串,可以使用CLOB,但通常情況下,不要過度使用CLOB,因為它們效率低於VARCHAR。
- 如果使用 JPA 生成資料庫模式,可以透過
Column註解或元素設定列長度,請參閱 列定義和模式生成.
- 參見 時區
- 參見 自定義型別
- 參見 自定義型別
- 參見 自定義型別
日期、時間和時間戳是資料庫和 Java 中常見的型別,因此理論上對映這些型別應該很簡單,對吧? 好吧,有時情況就是這樣,只需要一個普通的 Basic 對映即可,但有時會變得更加複雜。
一些資料庫沒有 DATE 和 TIME 型別,只有 TIMESTAMP 欄位,但有些資料庫有單獨的型別,有些資料庫只有 DATE 和 TIMESTAMP。 最初在 Java 1.0 中,Java 只有 java.util.Date 型別,它既是日期、時間,又是毫秒。 在 Java 1.1 中,這擴充套件到支援使用 java.sql.Date、java.sql.Time 和 java.sql.Timestamp 的常用資料庫型別,然後為了支援國際化,Java 建立了 java.util.Calendar 型別,實際上棄用了(幾乎所有方法)舊的日期型別(JDBC 仍然使用)。
如果將 Java java.sql.Date 型別對映到資料庫 DATE,這只是一個基本的對映,您應該不會遇到任何問題(暫時忽略 Oracle 的 DATE 型別,它曾經是時間戳)。 您還可以將 java.sql.Time 對映到 TIME,將 java.sql.Timestamp 對映到 TIMESTAMP。 但是,如果您在 Java 中有 java.util.Date 或 java.util.Calendar,並且希望將其對映到 DATE 或 TIME,您可能需要指示 JPA 提供程式執行某種轉換。 在 JPA 中,@Temporal 註釋或 <temporal> 元素用於對映此操作。 您可以指示僅將日期/時間值的 DATE 或 TIME 部分儲存到資料庫中。 您也可以使用 Temporal 將 java.sql.Date 對映到 TIMESTAMP 欄位,或任何其他此類轉換。
@Entity
public class Employee {
...
@Basic
@Temporal(DATE)
private Calendar startDate;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
...
<basic name="startDate">
<temporal>DATE</temporal>
</basic>
</attributes>
</entity>
不同時間類和資料庫型別以及不同資料庫上的毫秒精度不同。 java.util.Date 和 Calendar 類支援毫秒。 java.sql.Date 和 java.sql.Time 類不支援毫秒。 java.sql.Timestamp 類支援納秒。
在許多資料庫中,TIMESTAMP 型別支援毫秒。 在 Oracle 9 之前,只有一種 DATE 型別,它是日期和時間,但沒有毫秒。 Oracle 9 添加了一個具有毫秒(和納秒)的 TIMESTAMP 型別,現在將舊的 DATE 型別視為僅日期,因此在將其用作時間戳時要小心。 MySQL 有 DATE、TIME 和 DATETIME 型別。 DB2 有 DATE、TIME 和 TIMESTAMP 型別,TIMESTAMP 支援微秒。 Sybase 和 SQL Server 只有 DATETIME 型別,它有毫秒,但在某些版本上似乎存在精度問題,它似乎儲存了毫秒的估計值,而不是精確值。
如果使用時間戳版本鎖定,則需要非常小心毫秒精度。 確保您的資料庫精確支援毫秒,否則您可能會遇到問題,尤其是如果該值是在 Java 中分配的,然後與資料庫中儲存的值不同,這將導致對同一物件的下次更新失敗。
一般來說,我不建議使用時間戳作為主鍵或版本鎖定。 存在太多資料庫相容性問題,以及在同一毫秒內不支援兩個操作的明顯問題。
當您開始考慮時區、國際化、時代、本地、夏令時等時,時間變得更加複雜。 在 Java 中,只有 Calendar 支援時區。 通常假定 Calendar 處於本地時區,並以該假設儲存和檢索到資料庫中。 如果然後在另一個時區的另一臺計算機上讀取相同的 Calendar,問題是您將獲得相同的 Calendar 還是您將獲得原始時間在新的時區中的 Calendar? 這取決於 Calendar 是以 GMT 時間儲存的,還是以本地時間儲存的,以及是否在資料庫中儲存了時區。
一些資料庫支援時區,但大多數資料庫型別不儲存時區。 Oracle 有兩種用於具有時區的時間戳的特殊型別,TIMESTAMPTZ(儲存時區)和 TIMESTAMPLTZ(使用本地時區)。 一些 JPA 提供程式可能擴充套件支援儲存 Calendar 物件和時區。
- TopLink、EclipseLink :使用
@TypeConverter註釋和 XML 支援 OracleTIMESTAMPTZ和TIMESTAMPLTZ型別。
論壇帖子
Joda-Time 是 Java 中常用的日期/時間使用框架。 它取代了 Java 日曆,許多人發現 Java 日曆難以使用且效能較差。 JPA 中沒有標準的 Joda-Time 支援,但可以使用 Converter 將 Joda-Time 類和資料庫型別進行轉換。
- TopLink、EclipseLink :基本產品不提供任何特定的 Joda-Time 支援,但第三方庫提供了一個自定義轉換器,joda-time-eclipselink-integration。
Java Enums 通常用作物件模型中的常量。 例如,Employee 可能具有 gender 列舉型別 Gender(MALE、FEMALE)。
預設情況下,在 JPA 中,型別為 Enum 的屬性將作為 Basic 儲存到資料庫中,使用整型列舉值作為程式碼(即 0、1)。 JPA 還定義了 @Enumerated 註釋和 <enumerated> 元素(在 <basic> 上)來定義 Enum 屬性。 這可用於將 Enum 儲存為其名稱的 STRING 值(即 "MALE"、"FEMALE")。
有關將 Enum 型別轉換為除整型或字串名稱之外的值(例如字元常量)的說明,請參閱 轉換值。
public enum Gender {
MALE,
FEMALE
}
@Entity
public class Employee {
...
@Basic
@Enumerated(EnumType.STRING)
private Gender gender;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
...
<basic name="gender">
<enumerated>STRING</enumerated>
</basic>
</attributes>
</entity>
LOB 是大型物件,例如 BLOB(二進位制 LOB)或 CLOB(字元 LOB)。 它是一種資料庫型別,可以儲存大型二進位制或字串值,因為通常 VARCHAR 或 VARBINARY 型別的大小有限制。 LOB 通常作為定位器儲存在資料庫表中,實際資料儲存在表之外。 在 Java 中,CLOB 通常對映到 String,而 BLOB 通常對映到 byte[],儘管 BLOB 也可能代表某個序列化物件。
預設情況下,在 JPA 中,任何不是關係或基本型別(String、Number、時間、基本型別)的 Serializable 屬性都將序列化為 BLOB 欄位。
JPA 定義了 @Lob 註解和 <lob> 元素(在 <basic> 上),以定義屬性對映到資料庫中的 LOB 型別。該註解只是對 JPA 實現的一個提示,表明該屬性將儲存在 LOB 中,因為 LOB 可能需要以特殊的方式持久化。有時,將 LOB 對映為普通的 Basic 也能正常工作。
不同的資料庫和 JDBC 驅動程式對 LOB 大小有不同的限制。一些 JDBC 驅動程式在超過 4k、32k 或 1meg 時會出現問題。Oracle thin JDBC 驅動程式在某些版本中對繫結 LOB 資料存在 4k 限制。Oracle 為此限制提供瞭解決方法,一些 JPA 提供程式支援此方法。對於讀取 LOB,一些 JDBC 驅動程式更喜歡使用流,一些 JPA 提供程式也支援此選項。
通常,屬性的整個 LOB 將被讀取和寫入。對於非常大的 LOB,始終讀取值,或者讀取整個值可能並不理想。Basic 的獲取型別可以設定為 LAZY,以避免在訪問 LOB 之前讀取它。JPA 中對 Basic 上 LAZY 獲取的支援是可選的,因此一些 JPA 提供程式可能不支援它。一個解決方法是將 LOB 儲存在單獨的表和類中,並定義一個 OneToOne 到 LOB 物件,而不是 Basic,這在一般情況下通常是一個好主意,因為 LOB 的效能成本很高。如果永遠不需要讀取整個 LOB,則不應將其對映。在這種情況下,最好使用直接 JDBC 訪問和流式傳輸 LOB。可以將 LOB 對映到你的物件中的 java.sql.Blob/java.sql.Clob 以避免讀取整個 LOB,但這些需要活動連線,因此可能在分離物件時存在問題。
lob 註解示例
[edit | edit source]@Entity
public class Employee {
...
@Basic(fetch=FetchType.LAZY)
@Lob
private byte[] picture;
...
}
lob XML 示例
[edit | edit source]<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
...
<basic name="picture" fetch="LAZY">
<lob/>
</basic>
</attributes>
</entity>
延遲獲取
[edit | edit source]可以在 Basic 對映上設定 fetch 屬性來使用 LAZY 獲取。預設情況下,所有 Basic 對映都是 EAGER,這意味著只要選擇物件,就會選擇列。透過將 fetch 設定為 LAZY,列不會與物件一起選擇。如果訪問屬性,則屬性值將在單獨的資料庫選擇中被選擇。對 LAZY 的支援是 JPA 的一個可選特性,因此一些 JPA 提供程式可能不支援它。通常,對基本型別的延遲支援需要某種形式的位元組碼編織或動態位元組碼生成,這在某些環境或 JVM 中可能存在問題,或者可能需要預處理應用程式的持久化單元 jar 檔案。
只有很少訪問的屬性應該標記為延遲,因為訪問屬性會導致單獨的資料庫選擇,這會影響效能。如果查詢大量物件,情況尤其如此。原始查詢將需要一個數據庫選擇,但如果訪問每個物件的延遲屬性,這將需要 n 個數據庫選擇,這可能會成為一個主要的效能問題。
在基本型別上使用延遲獲取類似於獲取組的概念。延遲基本型別基本上是單個預設獲取組的支援。一些 JPA 提供程式通常支援獲取組,這允許更精細地控制每個查詢中獲取哪些屬性。
- TopLink、EclipseLink:支援延遲基本型別和獲取組。可以使用
FetchGroup類透過 EclipseLink API 配置獲取組。
可選
[edit | edit source]如果允許 Basic 屬性的值為 null,則該屬性可以是 optional。預設情況下,所有內容都被假定為可選,除了 Id,它不能是可選的。可選基本上只是一個提示,它適用於資料庫模式生成,如果持久化提供程式被配置為生成模式。如果為 false,它會向列新增 NOT NULL 約束。一些 JPA 提供程式還執行對可選屬性的物件驗證,並在寫入資料庫之前丟擲驗證錯誤,但這不是 JPA 規範所要求的。可選透過 Basic 註解或元素的 optional 屬性定義。
列定義和模式生成
[edit | edit source]Column 註解和元素上有一些用於資料庫模式生成的屬性。如果你不使用 JPA 來生成你的模式,你可以忽略這些。許多 JPA 提供程式確實提供了自動生成資料庫模式的功能。預設情況下,物件的屬性的 Java 型別將對映到你在使用的資料庫平臺的相應資料庫型別。你可能需要使用你的提供程式配置你的資料庫平臺(例如 persistence.xml 屬性)以允許為你的資料庫生成模式,因為許多資料庫使用不同的型別名稱。
Column 的 columnDefinition 屬性可用於覆蓋使用的預設資料庫型別,或使用約束或其他 DDL 增強型別定義。length、scale 和 precision 也可被設定為覆蓋預設值。由於 length 的預設值只是預設值,通常最好將它們設定為適合資料模型的預期資料的正確值,以避免資料截斷。unique 屬性可用於在列上定義唯一約束,大多數 JPA 提供程式將根據 Id 和關係對映自動定義主鍵和外部索引鍵約束。
JPA 沒有定義任何選項來定義索引。一些 JPA 提供程式可能為此提供擴充套件。你也可以透過本機查詢建立自己的索引。
列註解示例
[edit | edit source]@Entity
public class Employee {
@Id
@Column(name="ID")
private long id;
@Column(name="SSN", unique=true, nullable=false, description="description")
private long ssn;
@Column(name="F_NAME", length=100)
private String firstName;
@Column(name="L_NAME", length=200)
private String lastName;
@Column(name="SALARY", scale=10, precision=2)
private BigDecimal salary;
@Column(name="S_TIME", columnDefinition="TIMESTAMPTZ")
private Calendar startTime;
@Column(name="E_TIME", columnDefinition ="TIMESTAMPTZ")
private Calendar endTime;
...
}
列 XML 示例
[edit | edit source]<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id">
<column name="ID"/>
</id>
<basic name="ssn">
<column name="SSN" unique="true" optional="false"/>
</basic>
<basic name="firstName">
<column name="F_NAME" length="100"/>
</basic>
<basic name="lastName">
<column name="L_NAME" length="200"/>
</basic>
<basic name="startTime">
<column name="S_TIME" columnDefinition="TIMESTAMPTZ"/>
</basic>
<basic name="endTime">
<column name="E_TIME" columnDefinition="TIMESTAMPTZ"/>
</basic>
</attributes>
</entity>
如果使用 BigDecimal 與 Postgresql,JPA 將 salary 對映到型別為 NUMERIC(38,0) 的表列。你可以在 @Column 註解中調整 BigDecimal 的 scale 和 precision。
@Column(precision=8, scale=2)
private BigDecimal salary;
可插入、可更新 / 只讀欄位 / 返回
[edit | edit source]Column 註解和 XML 元素定義了 insertable 和 updatable 選項。這些允許在 SQL INSERT 或 UPDATE 語句中省略該列或外部索引鍵欄位。如果表的約束阻止插入或更新操作,則可以使用它們。如果多個屬性對映到同一個資料庫列,例如透過 ManyToOne 和 Id 或 Basic 對映對映的外部索引鍵欄位,則也可以使用它們。將 insertable 和 updatable 都設定為 false,實際上將該屬性標記為只讀。
insertable 和 updatable 也可用於資料庫表預設值,或在插入或更新時自動將值分配給列。但請謹慎操作,因為這意味著除非重新整理物件,否則物件的値將與資料庫不同步。對於 IDENTITY 或自動分配的 id 列,通常應該使用 GeneratedValue,而不是將 insertable 設定為 false。一些 JPA 提供程式還支援在插入或更新操作後從資料庫返回自動分配的欄位値。重新整理或將欄位返回到物件中的成本會影響效能,因此通常最好在物件模型中初始化欄位値,而不是在資料庫中。
- TopLink、EclipseLink:支援使用
ReturnInsert和ReturnUpdate註解和 XML 元素將插入和更新値返回到物件中。
轉換器 (JPA 2.1)
[edit | edit source]將値儲存到資料庫中的一個常見問題是,在 Java 中所需的値與資料庫中使用的値不同。常見的示例包括在 Java 中使用 boolean,在資料庫中使用 0、1 或 'T'、'F'。其他示例包括在 Java 中使用 String,在資料庫中使用 DATE,或對映自定義 Java 型別,如 Joda-Time 型別或 Money 型別。
JPA 2.1 定義了@Converter、@Convert 註解和<converter>、<convert> XML 元素。Converter 是一個使用者定義的類,它在 Java 程式碼中提供自定義轉換例程。它必須實現AttributeConverter 介面並用@Converter 註解(或在 XML 中指定)。Converter 可以透過兩種方式使用。通常它是在對映上使用@Convert 註解或<convert> XML 元素指定的。另一個選擇是,如果轉換自定義型別,則將Converter 應用於具有該型別的任何對映屬性。為了定義這樣的全域性轉換器,autoApply 標誌被新增到@Converter 註解中。@Convert 的disableConversion 標誌可以用來停用全域性轉換器的應用。@Convert 的attributeName 選項可以用來覆蓋繼承或嵌入式轉換。
@Entity
public class Employee {
...
@Convert(converter=BooleanTFConverter.class)
private Boolean isActive;
...
}
@Converter
public class BooleanTFConverter implements AttributeConverter<Boolean, String>{
@Override
public String convertToDatabaseColumn(Boolean value) {
if (Boolean.TRUE.equals(value)) {
return "T";
} else {
return "F";
}
}
@Override
public Boolean convertToEntityAttribute(String value) {
return "T".equals(value);
}
}
@Entity
public class Employee {
...
private Boolean isActive;
...
}
@Converter(autoApply=true)
public class BooleanTFConverter implements AttributeConverter<Boolean, String>{
@Override
public String convertToDatabaseColumn(Boolean value) {
if (Boolean.TRUE.equals(value)) {
return "T";
} else {
return "F";
}
}
@Override
public Boolean convertToEntityAttribute(String value) {
return "T".equals(value);
}
}
在 JPA 2.1 之前,沒有標準的方法來在資料型別和物件型別之間進行轉換。一種實現方法是透過屬性 get/set 方法來轉換資料。
@Entity
public class Employee {
...
private boolean isActive;
...
@Transient
public boolean getIsActive() {
return isActive;
}
public void setIsActive(boolean isActive) {
this.isActive = isActive;
}
@Basic
private String getIsActiveValue() {
if (isActive) {
return "T";
} else {
return "F";
}
}
private void setIsActiveValue(String isActive) {
this.isActive = "T".equals(isActive);
}
}
同樣,要轉換日期/時間,請參見Temporals。
此外,一些 JPA 提供者擁有特殊的轉換支援。
- TopLink,EclipseLink : 使用
@Convert、@Converter、@ObjectTypeConverter和@TypeConverter註解以及 XML 支援轉換。
JPA 定義了對大多數常見資料庫型別的支援,但是一些資料庫和 JDBC 驅動程式具有可能需要額外支援的額外型別。
一些自定義資料庫型別包括
- TIMESTAMPTZ、TIMESTAMPLTZ (Oracle)
- TIMESTAMP WITH TIMEZONE (Postgres)
- XMLTYPE (Oracle)
- XML (DB2)
- NCHAR、NVARCHAR、NCLOB (Oracle)
- Struct (STRUCT Oracle)
- Array (VARRAY Oracle)
- BINARY_INTEGER、DEC、INT、NATURAL、NATURALN、BOOLEAN (Oracle)
- POSITIVE、POSITIVEN、SIGNTYPE、PLS_INTEGER (Oracle)
- RECORD、TABLE (Oracle)
- SDO_GEOMETRY (Oracle)
- LOBs (Oracle thin driver)
為了處理對自定義資料庫型別的持久化,您可以使用Converter 或 JPA 提供者的特殊功能。否則,您可能需要將原始 JDBC 程式碼與 JPA 物件混合使用。一些 JPA 提供者為許多自定義資料庫型別提供自定義支援,有些還提供自定義鉤子,以便新增您自己的 JDBC 程式碼來支援自定義資料庫型別。
- TopLink,EclipseLink : 支援幾種自定義資料庫型別,包括 TIMESTAMPTZ、TIMESTAMPLTZ、XMLTYPE、NCHAR、NVARCHAR、NCLOB、物件關係型 Struct 和 Array 型別、PLSQL 型別、SDO_GEOMETRY 和 LOBs。