跳转到内容

Java 持久性/标识和排序

来自 Wikibooks,开放世界中的开放书籍

对象 ID (OID) 是唯一标识对象的东西。在 JVM 中,这通常是对象的指针。在关系数据库表中,一行通过其 主键 在其表中唯一标识。当将对象持久化到数据库时,您需要一个对象的唯一标识符,这使您能够查询对象、定义与对象的关联关系,以及更新和删除对象。在 JPA 中,对象 ID 通过 @Id 注解或 <id> 元素定义,并且应对应于对象表的 主键


示例 id 注解

[编辑 | 编辑源代码]
...
@Entity
public class Employee {
    @Id
    private long id
    ...
}

示例 id XML

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <generator class="assigned" />
    </attributes>
<entity/>

常见问题

[编辑 | 编辑源代码]
奇怪的行为,唯一约束冲突。
[编辑 | 编辑源代码]
您绝不应更改对象的 id。这样做会导致错误,或者根据您的 JPA 提供程序而产生奇怪的行为。也不要创建具有相同 id 的两个对象,或尝试持久化与现有对象具有相同 id 的对象。如果您有一个可能存在的对象,请使用 EntityManagermerge() API,不要对现有对象使用 persist(),并避免将非托管的现有对象与其他托管对象相关联。
没有主键。
[编辑 | 编辑源代码]
参见 没有主键

对象 ID 可以是自然 ID 或生成 ID。自然 ID 是在对象中出现并在应用程序中具有一定含义的 ID。自然 ID 的示例包括电子邮件地址、电话号码和社会保险号码。生成 ID(也称为代理 ID)是由系统生成的 ID。JPA 中的排序号是 JPA 实现生成的顺序 ID,并自动分配给新对象。使用排序号的好处是它们保证是唯一的,允许对象的所有其他数据发生变化,是用于查询和索引的有效值,并且可以有效地分配。自然 ID 的主要问题是所有事物最终都会发生变化;即使是一个人的社会保险号码也可能发生变化。自然 ID 还会使数据库中的查询、外键和索引效率降低。

在 JPA 中,@Id 可以通过 @GeneratedValue 注解或 <generated-value> 元素轻松分配一个生成的排序号。

示例生成 id 注解

[编辑 | 编辑源代码]
...
@Entity
public class Employee {
    @Id
    @GeneratedValue
    private long id
    ...
}

示例生成的id XML

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value/>
        </id>
    </attributes>
<entity/>

序列策略

[编辑 | 编辑源代码]

有几种生成唯一id的策略。一些策略与数据库无关,而另一些则利用了内置的数据库支持。

JPA 通过 GenerationType 枚举值定义了几个 id 生成策略,这些策略提供支持:TABLE、SEQUENCE 和 IDENTITY。

选择哪种序列策略很重要,因为它会影响性能、并发性和可移植性。

表排序

[编辑 | 编辑源代码]

表排序使用数据库中的一个表来生成唯一id。该表有两个列,一个存储序列的名称,另一个存储最后分配的id值。序列表中每个序列对象都有一行。每次需要新的id时,该序列的行都会递增,并将新的id值传递回应用程序以分配给对象。这只是序列表架构的一个例子,有关其他表排序架构,请参阅自定义

表排序是最便携的解决方案,因为它只使用一个普通的数据库表,因此与序列和标识不同,它可以在任何数据库上使用。表排序还提供了良好的性能,因为它允许序列预分配,这对插入性能非常重要,但可能存在潜在的并发问题

在 JPA 中,@TableGenerator 注解或 <table-generator> 元素用于定义一个序列表。TableGenerator 定义了一个 pkColumnName 用于存储序列名称的列,valueColumnName 用于存储最后分配的 id 的列,以及 pkColumnValue 用于存储名称列中的值(通常是序列名称)。

示例序列表

[编辑 | 编辑源代码]
SEQUENCE_TABLE
  SEQ_NAME     SEQ_COUNT   
EMP_SEQ 13
PROJ_SEQ 570

示例表生成器注解

[编辑 | 编辑源代码]
...
@Entity
public class Employee {
    @Id
    @TableGenerator(name="TABLE_GEN", table="SEQUENCE_TABLE", pkColumnName="SEQ_NAME",
        valueColumnName="SEQ_COUNT", pkColumnValue="EMP_SEQ")
    @GeneratedValue(strategy=GenerationType.TABLE, generator="TABLE_GEN")
    private long id;
    ...
}

示例表生成器 XML

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value strategy="TABLE" generator="EMP_SEQ"/>
            <table-generator name="EMP_SEQ" table="SEQUENCE_TABLE" pk-column-name="SEQ_NAME"
                value-column-name="SEQ_COUNT" pk-column-value="EMP_SEQ"/>
        </id>
    </attributes>
<entity/>

常见问题

[编辑 | 编辑源代码]
分配序列号时出错。
[编辑 | 编辑源代码]
如果您在数据库中没有定义 SEQUENCE 表,或者其架构与您配置的架构不匹配,或者与您的 JPA 提供商默认情况下期望的架构不匹配,则可能会出现诸如“找不到表”,“无效列”之类的错误。确保您正确创建了序列表,或者将 @TableGenerator 配置为与您创建的表匹配,或者让您的 JPA 提供商为您创建表(大多数 JPA 提供商支持模式创建)。您也可能会收到诸如“找不到序列”之类的错误,这意味着您没有为您的序列在表中创建一行。您必须为您的序列在序列表中插入一行初始行,其中包含初始 id(例如 INSERT INTO SEQUENCE_TABLE (SEQ_NAME, SEQ_COUNT) VALUES ("EMP_SEQ", 0)),或者让您的 JPA 提供商为您创建模式。
序列表中的死锁或并发性差。
[编辑 | 编辑源代码]
参见并发问题

序列对象

[编辑 | 编辑源代码]

序列对象使用特殊的数据库对象来生成id。序列对象仅在某些数据库(如 Oracle、DB2 和 Postgres)中受支持。通常,SEQUENCE 对象具有名称、增量和其他数据库对象设置。每次选择 <sequence>.NEXTVAL 时,序列都会按增量递增。

序列对象提供了最佳的排序选项,因为它们效率最高并且并发性最好,但是它们的可移植性最差,因为大多数数据库不支持它们。序列对象通过在数据库序列对象上设置增量来支持序列预分配,增量的大小为序列预分配的大小。

在 JPA 中,@SequenceGenerator 注解或 <sequence-generator> 元素用于定义一个序列对象。SequenceGenerator 定义了一个 sequenceName 用于数据库序列对象的名称,以及一个 allocationSize 用于序列预分配大小或序列对象增量。

示例序列生成器注解

[编辑 | 编辑源代码]
...
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="EMP_SEQ")
    @SequenceGenerator(name="EMP_SEQ", sequenceName="EMP_SEQ", allocationSize=100)
    private long id;
   
}

示例序列生成器 XML

[编辑 | 编辑源代码]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value strategy="SEQUENCE" generator="EMP_SEQ"/>
            <sequence-generator name="EMP_SEQ" sequence-name="EMP_SEQ" allocation-size="100"/>
        </id>
    </attributes>
<entity/>

常见问题

[编辑 | 编辑源代码]
分配序列号时出错。
[编辑 | 编辑源代码]
如果您在数据库中没有定义 SEQUENCE 对象,则可能会出现诸如“找不到序列”之类的错误。确保您创建了序列对象,或者让您的 JPA 提供商为您创建模式(大多数 JPA 提供商支持模式创建)。在创建序列对象时,确保序列的 INCREMENTSequenceGeneratorallocationSize 相匹配。创建序列对象的 DDL 依赖于数据库,对于 Oracle,它是 CREATE SEQUENCE EMP_SEQ INCREMENT BY 100 START WITH 100
无效的、重复的或负的序列号。
[编辑 | 编辑源代码]
如果您的序列对象的 INCREMENT 与您的 allocationSize 不匹配,则可能发生这种情况。这会导致 JPA 提供商认为它获得了比实际更多的序列,并最终导致值重复或出现负数。如果您的序列对象的 STARTS WITH 是 0 而不是等于或大于 allocationSize 的值,这也会发生在某些 JPA 提供商上。

标识排序

[编辑 | 编辑源代码]

标识排序使用数据库中的特殊 IDENTITY 列,允许数据库在插入行的对象时自动分配一个 id。IDENTITY 列在许多数据库中受支持,例如 MySQL、DB2、SQL Server、Sybase 和 PostgreSQL。Oracle 从 Oracle 12c 开始支持 IDENTITY 列。如果使用的是旧版本,可以使用序列对象和触发器来模拟它们。

虽然标识符排序看起来是最简单的方法来分配 ID,但它们存在几个问题。其中之一是,由于 ID 直到行插入后才会由数据库分配,因此在提交或刷新调用之前无法在对象中获取 ID。标识符排序也不允许预分配序列,因此可能需要为插入的每个对象进行一次选择操作,这可能会造成重大的性能问题,因此一般不推荐使用。

在 JPA 中,没有用于标识符排序的注释或元素,因为没有额外的信息需要指定。只需要将 GeneratedValue 的策略设置为 IDENTITY

示例标识符注释

[edit | edit source]
...
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long id;
    ...
}

示例标识符 XML

[edit | edit source]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value strategy="IDENTITY"/>
        </id>
    </attributes>
<entity/>

PostgreSQL 序列列

[edit | edit source]

序列对象是数据库构造,可以根据需要实例化为不同的对象。当调用其 "next" 方法时,它们提供增量值。请注意,增量可以是大于 1 的值,例如以 2、3 等递增。可以为每个表构建一个 "on insert" 触发器,该触发器调用为该特定表主键创建的序列对象,请求下一个值并将其插入到具有新记录的表中。MySQL 或 SQL Server 等产品中的标识数据类型是封装类型,它们执行相同的操作,而无需设置触发器和序列(尽管 PostgreSQL 至少使用其 Serial 和 Big Serial *伪* 数据类型自动化了大部分操作(这些实际上会创建一个 int/bigint 列并运行一个 "宏" 在执行 CREATE TABLE 语句时创建序列和 "on insert" 触发器)。)。

常见问题

[edit | edit source]
将 null 插入数据库,或插入时出错。
[edit | edit source]
这通常是由于未将 @Id 配置为使用 @GeneratedValue(strategy=GenerationType.IDENTITY) 造成的。请确保正确配置。也可能是您的 JPA 提供程序不支持您正在使用的数据库平台上的标识符排序,或者您尚未配置数据库平台。大多数提供程序要求您通过 persistence.xml 属性设置数据库平台,大多数提供程序还允许您自定义自己的平台,如果它没有得到直接支持。也可能是您没有将表中的主键列设置为标识类型。
对象在持久化后没有分配 ID。
[edit | edit source]
标识符排序要求在分配 ID 之前发生插入操作,因此它不会像其他类型的排序那样在持久化时分配。您必须在当前事务上调用 commit(),或者在 EntityManager 上调用 flush()。也可能是您没有将表中的主键列设置为标识类型。
子对象的 ID 在持久化时未从父对象分配。
[edit | edit source]
一个常见问题是,生成的 Id 是通过 OneToOneManyToOne 映射成为子对象 Id 的一部分。在这种情况下,由于 JPA 要求子对象为 Id 定义重复的 Basic 映射,因此它的 Id 将插入为 null。对此的一种解决方案是将子对象中 Id 映射上的 Column 标记为 insertable=false, updateable=false,并使用正常的 JoinColumn 定义 OneToOneManyToOne,这将确保外键字段由 OneToOneManyToOne 填充,而不是 Basic。另一种选择是先持久化父对象,然后在持久化子对象之前调用 flush()
插入性能差。
[edit | edit source]
标识符排序不支持序列预分配,因此需要在每次插入后进行一次选择操作,在某些情况下会使插入成本增加一倍。请考虑使用序列表或序列对象来允许序列预分配。
丢失最新的可用 ID。
[edit | edit source]
MySQL 错误 199 会导致其自动递增计数器在重启时丢失。因此,如果删除了最后一个实体并重启了 MySQL 服务器,相同的 ID 将被重新使用,因此不再唯一。

高级

[edit | edit source]

复合主键

[edit | edit source]

复合主键是由表中多个列组成的主键。如果表中没有单个列是唯一的,可以使用复合主键。通常,拥有一个单列主键(如生成的序列号)更有效且更简单,但有时复合主键是可取的且不可避免的。

复合主键在遗留数据库模式中很常见,其中有时可以使用 *级联键*。这指的是一个模型,其中依赖对象的键定义包含其父对象的主键;例如,COMPANY 的主键是 COMPANY_IDDEPARTMENT 的主键由 COMPANY_IDDEP_ID 组成,EMPLOYEE 的主键由 COMPANY_IDDEP_IDEMP_ID 组成,等等。虽然这通常与面向对象的設計原則不符,但一些 DBA 更喜欢这种模型。该模型的难点包括限制员工无法更换部门、外键关系变得更加复杂以及所有主键操作(包括查询、更新和删除)效率较低。但是,每个部门都控制着自己员工的 ID,如果需要,数据库 EMPLOYEE 表可以根据 COMPANY_IDDEP_ID 进行分区,因为这些 ID 包含在每个查询中。

复合主键的其他常见用法包括多对多关系,其中连接表具有其他列,因此表本身映射到一个对象,其主键由一对外键列和依赖或聚合一对多关系组成,其中子对象的主键由其父对象的主键和一个本地唯一字段组成。

在 JPA 中,有两种声明复合主键的方法:IdClassEmbeddedId

Id 类

[edit | edit source]

IdClass 定义一个单独的 Java 类来表示主键。它是通过 @IdClass 注释或 <id-class> XML 元素定义的。IdClass 必须定义一个属性(字段/属性),该属性反映实体中的每个 Id 属性。它必须具有相同的属性名称和类型。使用 IdClass 时,您仍然需要使用 @Id 标记实体中的每个 Id 属性。

IdClass 的主要目的是用作传递给 EntityManager find()getReference() API 的结构。 一些 JPA 产品也会将 IdClass 用作缓存键来跟踪对象的标识。 因此,需要(取决于 JPA 产品)在 IdClass 上实现 equals()hashCode() 方法。 确保 equals() 方法检查主键的每个部分,并正确使用 equals 用于对象,== 用于基本类型。 确保 hashCode() 方法对两个相等的对象返回相同的值。

TopLink / EclipseLink : 不需要在 id 类中实现 equals()hashCode()

示例 id 类注释

[edit | edit source]
...
@Entity
@IdClass(EmployeePK.class)
public class Employee {
    @Id
    private long employeeId;

    @Id
    private long companyId;

    @Id
    private long departmentId;
    ...
}

示例 id 类 XML

[edit | edit source]
<entity class="org.acme.Employee">
    <id-class class="org.acme.EmployeePK"/>
    <attributes>
        <id name="employeeId"/>
        <id name="companyId"/>
        <id name="departmentId"/>
    </attributes>
<entity/>

示例 id 类

[edit | edit source]
...
public class EmployeePK implements Serializable {
    private long employeeId;

    private long companyId;

    private long departmentId;
    
    public EmployeePK(long employeeId, long companyId, long departmentId) {
        this.employeeId   = employeeId;
        this.companyId    = companyId;
        this.departmentId = departmentId;
    }

    public boolean equals(Object object) {
        if (object instanceof EmployeePK) {
            EmployeePK pk = (EmployeePK)object;
            return employeeId == pk.employeeId && companyId == pk.companyId && departmentId == pk.departmentId;
        } else {
            return false;
        }
    }

    public int hashCode() {
        return (int)(employeeId + companyId + departmentId);
    }
}

嵌入式 Id

[edit | edit source]

EmbeddedId 定义了一个单独的 Embeddable Java 类来包含实体的主键。 它通过 @EmbeddedId 注释或 <embedded-id> XML 元素定义。 EmbeddedIdEmbeddable 类必须使用 Basic 映射定义实体的每个 id 属性。 EmbeddedIdEmbeddable 中的所有属性都被认为是主键的一部分。

EmbeddedId 也用作传递给 EntityManager find()getReference() API 的结构。 一些 JPA 产品也会将 EmbeddedId 用作缓存键来跟踪对象的标识。 因此,需要(取决于 JPA 产品)在 EmbeddedId 上实现 equals()hashCode() 方法。 确保 equals() 方法检查主键的每个部分,并正确使用 equals 用于对象,== 用于基本类型。 确保 hashCode() 方法对两个相等的对象返回相同的值。

TopLink / EclipseLink : 不需要在 id 类中实现 equals()hashCode()

示例嵌入式 id 注释

[edit | edit source]
...
@Entity
public class Employee {
    @EmbeddedId
    private EmployeePK id
    ...
}

示例嵌入式 id XML

[edit | edit source]
<entity class="org.acme.Employee">
    <attributes>
        <embedded-id name="org.acme.EmployeePK"/>
    </attributes>
<entity/>
<embeddable class="org.acme.EmployeePK">
    <attributes>
        <basic name="employeeId"/>
        <basic name="companyId"/>
        <basic name="departmentId"/>
    </attributes>
<embeddable/>

示例嵌入式 id 类

[edit | edit source]
...
@Embeddable
public class EmployeePK {
    @Basic
    private long employeeId;

    @Basic
    private long companyId;

    @Basic
    private long departmentId;
        
    public EmployeePK(long employeeId, long companyId, long departmentId) {
        this.employeeId = employeeId;
        this.companyId = companyId;
        this.departmentId = departmentId;
    }

    public boolean equals(Object object) {
        if (object instanceof EmployeePK) {
            EmployeePK pk = (EmployeePK)object;
            return employeeId == pk.employeeId && companyId == pk.companyId && departmentId == pk.departmentId;
        } else {
            return false;
        }
    }

    public int hashCode() {
        return (int)(employeeId + companyId + departmentId);
    }
}

通过 OneToOne 和 ManyToOne 关系实现主键

[edit | edit source]

一个常见的模型是让一个依赖对象共享其父对象的主键。 在 OneToOne 的情况下,子对象的主键与父对象相同,而在 ManyToOne 的情况下,子对象的主键由父对象的主键和另一个本地唯一的字段组成。

JPA 1.0 不允许在 OneToOneManyToOne 上使用 @Id,但 JPA 2.0 允许。

在处理复合主键时,最大的陷阱之一是实现与表具有多列主键的实体类的关联(使用 @JoinColumns 注释)。 许多 JPA 实现可能会抛出看似不一致的异常,如果未为 *每个* @JoinColumns 注释指定 referencedColumnName,而 JPA 要求为复合外键(即使所有引用列名称都等于引用表中的列名称)。 请参阅 http://download.oracle.com/javaee/5/api/javax/persistence/JoinColumns.html

JPA 1.0

[edit | edit source]

不幸的是,JPA 1.0 处理此模型的效果不佳,并且情况变得复杂,因此为了让您的生活更轻松,您可以考虑为子对象定义一个生成的唯一 id。 JPA 1.0 要求所有 @Id 映射都为 Basic 映射,因此如果您的 Id 来自通过 OneToOneManyToOne 映射的外键列,您还必须为外键列定义一个 Basic @Id 映射。 这样做的部分原因是,Id 必须是标识和缓存目的的简单对象,以及用于 IdClassEntityManager find() API 中。

由于您现在对同一外键列有两个映射,您必须定义哪个映射将写入数据库(它必须是 Basic 映射),因此 OneToOneManyToOne 外键必须被定义为只读。 这是通过将 JoinColumn 属性 insertableupdatable 设置为 false 来完成的,或者通过使用 @PrimaryKeyJoinColumn 而不是 @JoinColumn 来完成的。

对同一列有两个映射的副作用是,您现在必须保持两者同步。 这通常通过让 OneToOne 属性的 set 方法也将其 Basic 属性值设置为目标对象的 id 来完成。 如果目标对象的主键是 GeneratedValue,这可能会变得非常复杂,在这种情况下,您必须确保在关联两个对象之前已分配目标对象的 id。

有时我认为,如果 JPA 主键只是使用 Column 集合在实体上定义,而不是将它们与属性映射混在一起,那将简单得多。 这将使您能够以您想要的任何方式映射主键字段。 可以使用通用 List 将主键传递给 find() 方法,并且 JPA 提供者将负责正确地散列和比较主键,而不是用户的 IdClass。 但也许对于简单单例主键模型,JPA 模型更直观。

TopLink / EclipseLink : 允许将主键指定为列列表,而不是使用 Id 映射。 这允许将 OneToOneManyToOne 映射外键用作主键,而无需重复映射。 它也允许通过任何其他映射类型定义主键。 这是通过使用 DescriptorCustomizerClassDescriptor addPrimaryKeyFieldName API 来完成的。
Hibernate / Open JPA / EclipseLink (截至 1.2): 允许在 OneToOneManyToOne 映射上使用 @Id 注释。

示例 OneToOne id 注释

[edit | edit source]
...
@Entity
public class Address {
    @Id
    @Column(name="OWNER_ID")
    private long ownerId;
    
    @OneToOne
    @PrimaryKeyJoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
    private Employee owner;
    ...
    
    public void setOwner(Employee owner) {
        this.owner = owner;
        this.ownerId = owner.getId();
    }
    ...
}

示例 OneToOne id XML

[edit | edit source]
<entity class="org.acme.Address">
    <attributes>
        <id name="ownerId">
            <column name="OWNER_ID"/>
        </id>
        <one-to-one name="owner">
            <primary-key-join-column name="OWNER_ID" referencedColumnName="EMP_ID"/>
        </one-to-one>
    </attributes>
<entity/>

示例 ManyToOne id 注释

[edit | edit source]
...
@Entity
@IdClass(PhonePK.class)
public class Phone {
    @Id
    @Column(name="OWNER_ID")
    private long ownerId;

    @Id
    private String type;
    
    @ManyToOne
    @PrimaryKeyJoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
    private Employee owner;
    ...
    
    public void setOwner(Employee owner) {
        this.owner = owner;
        this.ownerId = owner.getId();
    }
    ...
}

示例 ManyToOne id XML

[edit | edit source]
<entity class="org.acme.Phone">
    <id-class class="org.acme.PhonePK"/>
    <attributes>
        <id name="ownerId">
            <column name="OWNER_ID"/>
        </id>
        <id name="type"/>
        <many-to-one name="owner">
            <primary-key-join-column name="OWNER_ID" referencedColumnName="EMP_ID"/>
        </many-to-one>
    </attributes>
</entity>

JPA 2.0

[edit | edit source]

在 JPA 2.0 中,为 OneToOneManyToOne 定义一个 Id 要简单得多。可以将 @Id 注解或 id XML 属性添加到 OneToOneManyToOne 映射中。对象的 Id 将从目标对象的 Id 导出。如果 Id 是一个单一值,那么源对象的 Id 与目标对象的 Id 相同。如果它是一个复合 Id,那么 IdClass 将包含 BasicId 属性,以及目标对象的 Id 作为关系值。如果目标对象也具有复合 Id,那么源对象的 IdClass 将包含目标对象的 IdClass

JPA 2.0 ManyToOne id 注解示例

[edit | edit source]
...
@Entity
@IdClass(PhonePK.class)
public class Phone {

    @Id
    private String type;
    
    @ManyToOne
    @Id
    @JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
    private Employee owner;
    ...//getters and setters
}

JPA 2.0 ManyToOne id XML 示例

[edit | edit source]
<entity class="org.acme.Address">
    <id-class class="org.acme.PhonePK"/>
    <attributes>
        <id name="type"/>
        <many-to-one name="owner" id="true">
            <join-column name="OWNER_ID" referencedColumnName="EMP_ID"/>
        </many-to-one>
    </attributes>
<entity/>

JPA 2.0 id 类示例

[edit | edit source]
...
public class PhonePK implements Serializable {
    private String type;
    private long owner;
    
    public PhonePK() {}

    public PhonePK(String type, long owner) {
        this.type = type;
        this.owner = owner;
    }

    public boolean equals(Object object) {
        if (object instanceof PhonePK) {
            PhonePK pk = (PhonePK)object;
            return type.equals(pk.type) && owner == pk.owner;
        } else {
            return false;
        }
    }

    public int hashCode() {
        return (int)(type.hashCode() + owner);
    }
 
    //getter and setters with names matching the properties with many-to-one relationships
}

高级排序

[edit | edit source]

并发和死锁

[edit | edit source]

表排序的一个问题是,序列表可能会成为并发瓶颈,甚至会导致死锁。如果在与插入相同的事务中分配序列 ID,这会导致并发性差,因为序列行将被锁定在事务持续时间内,阻止任何需要分配序列 ID 的其他事务。在某些情况下,整个序列表或表页面可能会被锁定,导致即使分配其他序列的事务也会等待甚至死锁。如果使用大型序列预分配大小,这将不再是一个问题,因为序列表很少被访问。一些 JPA 提供程序使用单独的(非 JTA)连接来分配序列 ID,从而避免或限制此问题。在这种情况下,如果您使用 JTA 数据源连接,那么在您的 persistence.xml 中包含一个非 JTA 数据源连接也很重要。

保证顺序 ID

[edit | edit source]

表排序还允许分配真正的顺序 ID。序列和标识排序是非事务性的,通常在数据库上缓存值,导致分配的 ID 中出现较大差距。这通常不是问题,并且需要良好的性能,但是如果性能和并发性不太重要,并且需要真正的顺序 ID,那么可以使用表序列。通过将序列的 allocationSize 设置为 1 并确保序列 ID 在插入的同一事务中分配,您可以保证没有间隙的序列 ID(但通常最好忍受间隙并获得良好的性能)。

用完数字

[edit | edit source]

程序员经常有一种偏执的妄想恐惧,即用完序列号。由于大多数序列策略只是不断增加一个数字,因此不可避免地,你最终会用完。但是,只要使用足够大的数字精度来存储序列 ID,这就不成问题。例如,如果您将 ID 存储在 NUMBER(5) 列中,这将允许 99,999 个不同的 ID,这在大多数系统中最终会用完。但是,如果您将 ID 存储在 NUMBER(10) 列中,这更常见,这将存储 9,999,999,999 个 ID,或者每秒一个 ID,持续约 300 年(比大多数数据库存在的时间更长)。但也许您的系统会处理大量数据,并且(希望)会存在很长时间。如果您将 ID 存储在 NUMBER(20) 中,这将是 99,999,999,999,999,999,999 个 ID,或者每毫秒一个 ID,持续约 3,000,000,000 年,这很安全。

但是您还需要在 Java 中存储此 ID。如果您将 ID 存储在 Java int 中,这将是一个 32 位数字,即 4,294,967,296 个不同的 ID(实际上是 2,147,483,648 个正 ID),或者每秒一个 ID,持续约 100 年。如果您改为使用 long,这将是一个 64 位数字,即 9,223,372,036,854,775,808 个不同的 ID,或者每毫秒一个 ID,持续约 300,000,000 年,这很安全。我建议使用 long 而不是 int,因为我已经看到过在大型数据库中 int ID 用完的情况(发生的情况是它们变为负数,直到它们环绕到 0 并开始出现约束错误)。

自定义

[edit | edit source]

JPA 支持三种不同的生成 ID 策略,但还有许多其他方法。通常,JPA 策略就足够了,所以您只会在遗留情况下使用不同的方法。

有时应用程序具有特定于应用程序的生成 ID 策略,例如在 ID 前添加国家代码或分支编号。有几种方法可以集成自定义 ID 生成策略,最简单的方法就是将 ID 定义为普通 ID,并在创建对象时让应用程序分配 ID 值。

一些 JPA 产品提供了额外的排序和 ID 生成选项以及配置钩子。

TopLinkEclipseLink : 提供了一些额外的排序选项。UnaryTableSequence 允许使用单列表。QuerySequence 允许使用自定义 SQL 或存储过程。还存在一个 API,允许用户提供自己的代码来分配 ID。
Hibernate : 通过 @GenericGenerator 注解提供 GUID ID 生成选项。

通过触发器生成主键

[edit | edit source]

可以将数据库表定义为具有自动分配其主键的触发器。这通常不是一个好主意(尽管一些 DBA 可能会认为它是),最好使用 JPA 提供程序生成的序列 ID,或者在应用程序中分配 ID。通过触发器分配 ID 的主要问题是,应用程序和对象需要将此值返回。对于通过触发器分配的非主键值,可以在提交或刷新对象后刷新对象以获取这些值。但是,这对于 ID 是不可能的,因为 ID 是刷新对象所必需的。

如果您有另一种方法来选择触发器生成的 ID,例如使用另一个唯一字段选择对象的行,您可以在插入后发出此 SQL select 以获取 ID 并将其设置回对象。您可以在 JPA @PostPersist 事件中执行此 select。一些 JPA 提供程序可能不允许/不喜欢在事件期间执行查询,它们也可能不会在事件回调期间拾取对对象的更改,因此这样做可能会有问题。此外,一些 JPA 提供程序可能不允许在不使用 GeneratedValue 的情况下取消分配/为空主键,因此您可能会遇到问题。一些 JPA 提供程序内置支持将分配在触发器(或存储过程)中的值返回到对象中。

TopLink / EclipseLink : 提供 ReturningPolicy,允许从数据库插入或更新后返回任何字段值,包括主键。这通过 @ReturnInsert@ReturnUpdate 注解或 <return-insert><return-update> XML 元素在 eclipselink-orm.xml 中定义。

通过事件生成主键

[edit | edit source]

如果应用程序生成自己的 ID 而不是使用 JPA GeneratedValue,那么有时希望在 JPA 事件中执行此 ID 生成,而不是应用程序代码必须生成和设置 ID。在 JPA 中,这可以通过 @PrePersist 事件来实现。

没有主键

[编辑 | 编辑源代码]

有时您的对象或表没有主键。在这种情况下,最佳解决方案通常是向对象和表添加一个生成的 ID。如果您没有此选项,有时表中会有一列或一组列构成唯一值。您可以将这组唯一的列用作 JPA 中的 ID。JPA Id 不一定始终与数据库表主键约束匹配,也不需要主键或唯一约束。

如果您的表确实没有唯一列,那么将所有列用作 ID。通常情况下,当这种情况发生时,数据是只读的,因此即使表允许具有相同值的重复行,对象也将是相同的,因此 JPA 认为它们是同一个对象并不重要。允许更新和删除的问题是,没有办法唯一地标识对象的 row,因此所有匹配的 row 将被更新或删除。

如果您的对象没有 ID,但其表有,则可以。将对象设为 Embeddable 对象,可嵌入对象没有 ID。您需要一个包含此 EmbeddableEntity 来持久化和查询它。

华夏公益教科书