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。