JPA实体中数据库生成ID的最终指南1

根据JPA规范,Entity是满足以下要求的Java类:

  1. 带注释@Entity注记
  2. 没有args构造函数。
  3. 不是最终的
  4. 具有带注释的ID字段(或多个字段)@Id

如您所见,需要ID。那是为什么?

我们为什么要在JPA实体中使用ID呢?

JDBC和关系数据库不需要表的主键或唯一键。在使用JDBC时,我们使用自己的语言--原生SQL查询与数据库进行通信。若要获取数据集,开发人员将运行SELECT语句,该语句返回相应的元组。要保存或更新数据,我们需要编写另一个INSERTUPDATE声明。在应用程序到数据库级的通信中,应用程序中的对象与存储在数据库中的记录之间没有直接联系。通常,这种映射是作为业务逻辑的一部分手动管理的。

JPA采取了不同的方法。它引入了实体-Java对象,这些对象严格地与数据库中的记录绑定在一起。因此,JPA规范要求开发人员定义字段或一组字段,以在实体实例和特定DB记录之间建立一对一的关联。这样,开发人员就可以从数据库中获取JPA实体,使用它们并在以后保存它们,而无需调用任何JPA实体。INSERTUPDATE陈述。这是允许开发人员主要关注业务逻辑的关键概念之一,而大多数样板操作是由JPA实现本身处理的,ID是此过程的重要部分。

注: ID不必映射到定义为表主键的列。我们需要将ID映射到唯一标识每一行的列。但是对于本文,我们将继续交替使用术语ID和主键。

ID类型:我们有什么

我们需要在实体中定义ID。我们有什么选择?

首先,我们可以定义一个“简单”或“复合”结构的ID。“简单”ID由实体中的单个字段表示,复合字段由一个单独的类表示,该类包含一组标识实体的字段。

通常,我们对JPA实体使用简单的ID。可以自动生成简单ID(代理ID),这是处理ID值的最常用方法。生成可以发生在数据库端(服务器端生成)或应用程序中(客户端生成)。这两种方法各有优缺点。

在本文中,我们将重点讨论服务器端生成的ID。为了简单起见,我们将使用Hibernate ORM作为所有示例的默认JPA实现,除非我们明确提到另一个ORM。

生成的ID-为什么我们要关心?

ID生成事件通常只发生一次--当我们将新实体保存到数据库时。因此,假设我们有一个不经常创建多个实体的应用程序(经验法则--假设每秒不超过100个实体),并且不与其他应用程序共享数据库。在这种情况下,理论上,我们可以使用任何ID生成策略。管理国家清单的应用程序就是一个很好的例子--我们并不经常创建新的国家。但是电能计量呢?如果我们有100米,每小时发送数据,我们必须每小时保存100个测量数据。基本上,我们每36秒就可以节省一次测量。看起来不太像。几千米呢?数以万计?如果我们决定每10分钟做一次测量呢?一个企业要花费多少钱来停止信息系统来改变ID生成策略?

在实践中,应用程序和业务一样趋向于增长和变化,这就是为什么必须选择适当的ID生成策略,以避免将来发生痛苦的迁移。我们将在这篇文章中提到很多性能,甚至我们的应用程序也不是新的Facebook或Twitter,它们还没有每秒节省数百万实体,我们应该提前考虑最合适的ID生成策略,以避免将来出现问题。

默认情况下世代是如何工作的

最简单的方法是在JPA实体中定义生成的ID,用@Id@GeneratedValue注释。我们甚至不需要为@GeneratedValue。默认情况下,您将得到一个正确生成的ID字段。

@Table(name = "pet")
@Entity
public class Pet {
    @Id
    @GeneratedValue
    @Column(name = "id", nullable = false)
    private Long id;
}

有两种类型的默认值:从一开始就不应更改的默认值和应该更改的默认值。默认值不会破坏应用程序,但是在生成ID的情况下,它们工作得好吗?让我们看看@GeneratedValue默认参数值:

public @interface GeneratedValue {

    GenerationType strategy() default AUTO;

    String generator() default "";
}

正如我们所看到的,我们将生成策略参数设置为AUTO。这意味着JPA提供者决定如何为ID生成一个唯一的值。让我们从我们可以使用的策略列表开始。

JPA标准除了描述AUTO:

  • IDENTITY-特定于数据库的内置用途identity用于生成ID的列类型。
  • SEQUENCE-使用序列生成唯一的ID值。
  • TABLE-使用模拟序列的单独表。当应用程序需要ID时,JPA提供程序锁定表行,更新存储的ID值,并将其返回给应用程序。与前两种策略相比,这种策略提供了最差的性能,如果可能的话,应该避免。您可以阅读更多有关此策略的内容。在文件中.

依开发人员手册,如果我们使用与UUID不同的ID类型(如Long、Integer等)并将策略设置为AUTO,Hibernate将执行以下操作(自5.0版起):

  • 尝试使用SEQUENCEID生成策略
  • 如果序列不受支持(即我们使用MySQL),它将使用TABLE(或IDENTITY,在Hibernate 5.0之前)生成ID的策略

为什么Hibernate试图使用SEQUENCE作为默认策略?这里的关键指标是性能。这个TABLE就业绩而言,战略是最差的。在本文中,作者使用不同的策略进行了一些测试。通过更改ID生成策略,他能够将10K实体的时间从185秒减少到4.3秒。IDENTITYSEQUENCE并启用Hibernate的一些优化。所以,这两种退步策略(IDENTITYTABLE)不会破坏应用程序,但性能不会很好。

这里的问题是,即使是SEQUENCE表现不佳;表演将接近IDENTITY。之所以发生这种情况,是因为所有实体都使用单个数据库序列,而且序列参数不允许Hibernate应用ID池优化。我们将查看默认的SEQUENCE下一节将详细介绍行为。

结语::为ID生成策略保留默认值可能会对我们的应用程序性能造成负面影响。对于生产应用程序,我们需要将默认值更改为更合适的东西。

顺序:如何正确定义?

这个SEQUENCE策略使用一个单独的DB对象--序列--在将数据插入数据库之前获取和分配一个唯一的ID值。这提供了批处理。INSERT操作支持,因为JPA提供程序不需要在每个提供程序之后获取生成的ID。INSERT与标识列、触发器生成的ID等类似。

缺省值:它们足够好吗?

对象的默认定义。SEQUENCE策略,我们需要写下面的代码。实际上,这就是默认情况下的结果。AUTO如果我们的数据库支持序列,策略。

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id", nullable = false)
private Long id;

为该定义生成的序列(如果我们在Hibernate设置中启用自动数据库创建)的SQL如下所示:

create sequence hibernate_sequence start 1 increment 1;

JPA提供程序将只对所有用户使用此数据库序列。INSERT语句,如果我们为所有SEQUENCE我们应用的策略。这可能会引起一些问题。

首先,我们可能会耗尽序列。在大多数数据库中,最大序列值为2^63-1,因此很难达到这一极限。但是,产生大量新数据的应用程序仍然有可能,例如物联网系统或横幅网络,每天产生数十亿个事件。

2^63-1是一个很大的数字。如果我们每秒保存10.000个实体,我们需要大约2900万年来耗尽这个序列。这意味着,在大多数情况下,我们可能不会担心序列结束,但我们仍然需要意识到序列是有限的。

第二,演出会受到影响。默认的序列增量设置为1,这将禁用Hibernate对序列的ID池生成优化。JPA提供程序将从每个INSERT序列中的语句。例如,如果我们试图保存两个实体并查看Hibernate SQL日志,我们将看到如下内容:

select nextval ('hibernate_sequence')
insert into pet (name, id) values (?, ?)
select nextval ('hibernate_sequence')
insert into pet (name, id) values (?, ?)

因此,我们通过执行两个ID值来选择两个ID值。SELECT语句,将这些ID分配给实体,然后保存它们。这给了我们一个额外的开销SELECT每一个INSERT。这是对应用程序性能的负面影响。

结语::SEQUENCE对于非数据密集型应用程序,ID生成策略是一种很好的方法。如果我们计划做更大的事情,为了避免性能和顺序耗尽的问题,我们需要改变默认策略的设置。

序列:我们可以改变什么?

让我们首先指定一个实体ID生成的专用序列。

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pet_seq")
@Column(name = "id", nullable = false)
private Long id;

对于这个定义,我们将看到执行以下SQL:

create sequence pet_seq start 1 increment 50

Hibernate对非默认序列使用ID生成池优化。这样做的目的是为一个会话分配一系列的值,并将这些值用作ID。默认情况下,分配的ID数等于50个。

优化的工作方式如下:

  • 步骤1:Hibernate执行一个SELECT从序列中获取ID。
  • 步骤2如果所选值等于序列初始值,Hibernate将从序列中选择下一个ID作为高值,将初始值设置为范围低值。否则,它将进入第4步。
  • 步骤3:Hibernates插入数据分配IDlow到``high`‘范围。
  • 步骤4:一旦Hibernate需要下一个批处理,它就会从序列中选择下一个ID值(大于初始值)。Hibernate根据allocationSize参数。Low值=ID – allocationSize, high = ID。然后Hibernate进入步骤3。

我们只多表演两场SELECTS用于前50个保存的实体,用于默认设置。对于以下50个实体,我们只执行一个额外的选择。例如,如果启用Hibernate SQL日志,我们可以看到如下内容:

select nextval ('pet_seq'); //selects 1 – got initial value, need to select next value
select nextval ('pet_seq'); //selects 51 as range high value
insert into pet (name, id) values (?, ?);// id=1
insert into pet (name, id) values (?, ?);//id=2
//insert other 48 entities
select nextval ('pet_seq'); //selects 101 as range next high value, calculates 101 – 50 = 51 as the low
insert into pet (name, id) values (?, ?);//id=51
//etc. 

有一个缺点:如果关闭数据库会话(即重新启动应用程序或重新打开实体管理器),将丢失未使用的ID。这样一个短命的应用程序的一个很好的例子可能是一个无服务器的lambda函数。如果每个会话只保存一个实体,然后退出应用程序,我们将永远失去49个ID。这种行为可能导致序列耗尽,因此对于处理少量实例的短会话,我们需要设置更小的ID分配大小,以避免浪费大量ID。

要调整ID生成参数,即减少分配大小,可以使用@SequenceGenerator注释序列生成器允许我们使用现有序列或创建具有所需参数的新序列。例如,在下面的代码中,我们提供完整的序列定义,并将ID分配大小指定为20。

@Id
@SequenceGenerator(name = "pet_seq", 
        sequenceName = "pet_sequence", 
        initialValue = 1, allocationSize = 20)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pet_seq")
@Column(name = "id", nullable = false)
private Long id;

如果序列不存在,Hibernate将为该定义生成以下SQL:

create sequence pet_sequence start 1 increment 20

定义序列生成器时,需要记住以下内容:如果指定现有序列名称,并且启用Hibernate模式验证,则allocationSize参数必须与increment参数,否则应用程序将无法启动。

如果要更改Hibernate中的序列验证行为,可以禁用模式验证或设置参数的hibernate.id.sequence.increment_size_mismatch_strategy价值对价值LOGFIX.

LOG参数值时,Hibernate将忽略不匹配。这可能会导致PK唯一性冲突,因为ID分配范围计算将与实际序列不匹配。increment值,我们可以得到重复的ID值。例如,对于allocationSize等于20和序列increment是1,我们会得到这样的东西:

select nextval ('pet_seq'); // selects 1 initial value, need to select next value
select nextval ('pet_seq'); //selects 2 as range high value
insert into pet (name, id) values (?, ?);// id=1
insert into pet (name, id) values (?, ?);//id=2
//Now we’ve exceeded high value, need to select the next batch
select nextval ('pet_seq'); //selects 3 as range high value, calculates 3 – 20 = -17 as the low
insert into pet (name, id) values (?, ?);//id=-17
insert into pet (name, id) values (?, ?);//id=-16
//Restarting the application
select nextval ('pet_seq'); //selects 4 as range high value, calculates 4 – 20 = -16 as the low
insert into pet (name, id) values (?, ?);//id=-16 getting unique constraint violation

假设我们将参数设置为FIX。在这种情况下,allocationSize将自动调整jpa序列生成器中的参数以匹配DB序列。increment参数,例如,对于上述情况,参数为1。

的另一个特性@SequenceGenerator定义是,通过在不同的序列生成器中指定相同的“序列名称”,我们可以为不同的实体重用相同的序列。

//ID Definition for ‘Pet’ entity
@Id
@SequenceGenerator(name = "pet_seq", sequenceName = "common_sequence")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pet_seq")
@Column(name = "id", nullable = false)
private Long id;

//ID Definition for ‘Owner’ entity
@Id
@SequenceGenerator(name = "owner_seq", sequenceName = " common_sequence ")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "owner_seq")
@Column(name = "id", nullable = false)
private Long id;

结语*定义序列生成器使我们能够:

  1. 使用ID获取优化以获得更好的应用程序性能。
  2. 根据应用程序工作负载优化获取大小,以便在频繁获取ID和由于会话关闭而浪费一些ID之间保持平衡。
  3. 在不同实体之间共享相同的序列。

这使得SEQUENCEID生成几乎是一个理想的选择。在这个策略中有什么需要注意的地方吗?

DB的多个客户端:这里有什么问题吗?

即使SEQUENCE生成策略使用数据库中的序列,它在应用程序代码中分配ID值。这意味着使用相同数据库的其他应用程序可能不知道序列的存在,因此出现了ID生成策略。

在数据库中使用多个客户端可能导致其他DB客户端直接分配ID而不使用序列的情况。这些ID值可能与为应用程序中未保存的实体保留的ID值相同。当我们的应用程序开始保存实体时,它可能会导致PK唯一性冲突,并且数据不会被存储。

结语: SEQUENCE如果多个客户端更新数据库,ID的生成策略可能无法正常工作。在这种情况下,ID生成应该由数据库控制。IDENTITY这里的策略效果更好。

身份:利弊

IDENTITY是用于开发人员使用MySQL数据库生成ID的“默认”策略。由于许多RDBMS(除了MySQL)都支持用于列定义的标识数据类型,所以我们可以在许多应用程序中看到这种策略。有时开发人员选择它是因为“它在我以前的项目中奏效了”,而且没有人愿意改变这个习惯,如果它成功的话。通过指定如下代码中的策略,我们获得了一个可靠的ID生成过程,该流程管理在一个地方--数据库。

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

为每一个INSERT语句,数据库将自动为相应的@Id场。这与SEQUENCE策略行为,如果我们定义allocationSize等于“1”。对于这两种情况,我们都需要为每个INSERT声明。然而,有一个不同之处。重要的是要理解使用identity列意味着必须在已知标识符值之前物理插入实体行。由于数据库生成ID的值,JPA提供程序应该在插入数据后将其返回给应用程序。

问题是:JPA提供程序如何在插入记录后获取ID?如果数据库驱动程序支持JDBC3API(大多数现代数据库都支持),则会自动完成。JPA提供程序隐式调用Statement.getGeneratedValues()方法,该方法返回生成的值。在遮罩下,JPA提供程序生成如下所示的SQL语句:

insert into pet (name) values (‘Buddy’) RETURNING *

假设我们使用一个旧版本的数据库驱动程序。在这种情况下,将执行额外的SELECT(通常由JPA提供程序执行,但有时我们需要手动执行)来获取生成的值,类似于下面的代码。这是旧PostgreSQL版本的日志,它模拟IDENTITY使用DB序列的数据类型。对于其他RDBMS,SQL将类似。

insert into pet (name) values (?)
select currval('pet_id_seq')
insert into pet (name) values (?)
select currval('pet_id_seq')

此行为不允许JPA提供程序执行批处理插入。因为提供程序需要在每个INSERT,它将批处理操作拆分为单个操作。INSERT运算符,并在每次执行后获取生成的ID值。我们不能送一批INSERT语句并获得一批生成的ID,因为我们无法将生成的ID可靠地关联到JPA对象。原因是数据库不能保证生成的id的顺序与INSERT此外,INSERT语句不能以与批处理相同的顺序执行。因此,获得插入记录的ID的唯一可靠方法是拆分批处理。

结语: IDENTITY策略简单易用,保证了可靠的应用独立主键值的生成.

从另一方面来说,这种策略在规则中提供了次优的性能。INSERT操作和批处理INSERT根本不支持操作。因此,建议使用IDENTITY对于我们保存少量新数据或几个独立客户端应用程序更改数据库的情况。

结论:序列与序列的一致性

那么,我们应该为我们的JPA实体选择哪种ID生成策略呢?以下是一些建议:

  1. SEQUENCE与其他策略相比,它提供了更好的整体性能。此外,我们需要考虑以下几点:
    1. 为每个JPA实体定义一个单独的序列是一个很好的实践。避免默认的序列生成器参数。
    2. 我们应该用@SequenceGenerator对微调序列参数的注释。
    3. 我们需要根据应用程序工作负载模式来定义批处理大小。
  1. 我们可能更喜欢IDENTITY下列案件的战略:
    1. 如果数据库不支持序列。
    2. 用于不经常创建和保存的实体。
    3. 如果我们的数据库被其他应用程序修改。
  1. TABLEAUTO如果可能的话,生成策略。他们的表现最差。

ID列表不限于简单的服务器生成的ID。在下面的文章中,我们将讨论客户端生成的ID,特别是UUID。此外,尽管不太流行,复合ID也有一些需要学习的东西,所以我们也将讨论它们。

db-generated-ids-in-jpa.png

小伙伴们如果觉得我写的不错,不妨帮个忙,给我点个赞呗,可以让更多的人看到这篇文章

你可能感兴趣的:(JPA实体中数据库生成ID的最终指南1)