配置 JPA 让 PostgreSQL 生成主键值

Posted

技术标签:

【中文标题】配置 JPA 让 PostgreSQL 生成主键值【英文标题】:Configure JPA to let PostgreSQL generate the primary key value 【发布时间】:2012-08-03 06:26:27 【问题描述】:

所以我们的项目使用 PostgreSQL 数据库,我们使用 JPA 来操作数据库。 我们已经在 Netbeans 7.1.2 中使用自动创建者从数据库中创建了实体。

经过小的改动,我们的主键值被描述为:

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Basic(optional = false)
@NotNull
@Column(name = "idwebuser", nullable = false)
private Integer idwebuser;

问题是现在应用程序不灵活,因为当我们直接修改数据库(使用 SQL 或其他工具)而不是通过 Java 应用程序时 - 生成的值低于实际的数据库 ID 值 - 所以我们在创建新实体时出错。

JPA 是否有可能让数据库自动生成 ID,然后在创建过程后获取它? 或者有什么更好的解决方案? 谢谢。

编辑 进一步来说: 我们有一个用户表,我的问题是使用任何类型的策略生成类型,JPA 正在插入一个由其生成器 ID 指定的新实体。这对我来说是错误的,因为如果我自己更改表,通过添加新条目,应用程序的 GeneratedValue 低于当前 ID - 这导致我们出现重复 ID 的异常。 我们可以修复它;)?

关于答案的简短说明 我有一个小谎言,因为我们使用了 PG Admin -> 查看前 100 行并从那里编辑行,而不是使用 select。无论如何,事实证明这个编辑器以某种方式跳过了更新 ID 的过程,因此即使在 DB 中,当我们编写正确的 INSERT 时,它也会以不正确的 ID 执行!所以基本上是我们使用的编辑器的问题,而不是数据库和应用的问题……

现在它甚至可以使用 @GeneratedValue(strategy=GenerationType.IDENTITY)

【问题讨论】:

这是否意味着 JPA 没有使用 PostgreSQL 序列?表中该列的定义是什么?那是serial 还是只是一个“整数” 我们使用的是串行字段类型。我将在主要问题@a_horse_with_no_name 中更多地描述这个问题 所以这确实意味着 JPA 使用关联的序列?这很奇怪。 在测试期间,我们现在大约有 17 行,我们使用:@SequenceGenerator(name="pk_sequence",sequenceName="entity_id_seq") @GeneratedValue(strategy=GenerationType.SEQUENCE,generator="pk_sequence") 我们刚刚收到错误 Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.2.0.v20110202-r8913): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "webuser_idwebuser_pk" Detail: Key (idwebuser)=(14) already exists. 所以看起来还是有问题。 序列使用有冲突。显然,并非所有插入都使用相同的策略来生成 PK。 【参考方案1】:

给定表定义:

CREATE TABLE webuser(
    idwebuser SERIAL PRIMARY KEY,
    ...
)

使用映射:

@Entity
@Table(name="webuser")
class Webuser 

    @Id
    @SequenceGenerator(name="webuser_idwebuser_seq",
                       sequenceName="webuser_idwebuser_seq",
                       allocationSize=1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator="webuser_idwebuser_seq")
    @Column(name = "idwebuser", updatable=false)
    private Integer id;

    // ....


tablename_columname_seq的命名是SERIAL的PostgreSQL默认序列命名,我建议你坚持下去。

如果您需要 Hibernate 与数据库的其他客户端合作,allocationSize=1 很重要。

请注意,如果事务回滚,此序列中会有“间隙”。交易可以出于各种原因回滚。您的应用程序应设计为应对此问题。

永远不要假设任何 id n 都有一个 id n-1n+1 永远不要假设在小于n 的id 之前或大于n 的id 之后添加或提交了id n。如果你真的很小心如何使用序列,你可以这样做,但你永远不应该尝试;而是在您的表中记录一个时间戳。 切勿添加或减去 ID。比较它们是否相等。

请参阅PostgreSQL documentation for sequences 和the serial data types。

他们解释说,上面的表定义基本上是一个快捷方式:

CREATE SEQUENCE idwebuser_id_seq;
CREATE TABLE webuser(
    idwebuser integer primary key default nextval('idwebuser_id_seq'),
    ...
)
ALTER SEQUENCE idwebuser_id_seq OWNED BY webuser.idwebuser;

...这应该有助于解释为什么我们添加了@SequenceGenerator 注释来描述序列。


如果您确实必须有一个无间隙序列(例如,支票或发票编号),请参阅gapless sequences,但请认真避免这种设计,并且永远不要将其用作主键。


注意:如果您的表定义如下所示:

CREATE TABLE webuser(
    idwebuser integer primary key,
    ...
)

并且您正在使用(不安全,请勿使用)插入其中:

INSERT INTO webuser(idwebuser, ...) VALUES ( 
    (SELECT max(idwebuser) FROM webuser)+1, ...
);

或(不安全,永远不要这样做):

INSERT INTO webuser(idwebuser, ...) VALUES ( 
    (SELECT count(idwebuser) FROM webuser), ...
);

然后你做错了,应该切换到一个序列(如上所示)或一个正确使用锁定计数器表的无间隙序列实现(再次,见上文并在 Google 中查看“gapless sequence postgresql”)。如果有多个连接在数据库上工作,则上述两种方法都会出错。

【讨论】:

我不知道为什么或如何,但它仍然无法正常工作;我们显然使用CREATE TABLE webuser( idwebuser SERIAL PRIMARY KEY, ... ) 在实体中,我已经使用您的示例给出的序列注释对其进行了编辑。仍然:当我启动应用程序并注册新用户时 - 它毫无问题地插入了他;然后我用 PGAdmin 添加了 3 个具有以下 id 的虚拟行,并尝试注册下一个用户,这导致错误:ERROR: duplicate key value violates unique constraint "webuser_idwebuser_pk" Detail: Key (idwebuser)=(20) already exists.@Craig,我也不太关心差距。 @Atais 我现在只能告诉你的是,它对我来说很好用,并且是规范和 PostgreSQL 文档所要求的。我们需要弄清楚您的设置有何不同。也许您应该编辑您的答案以显示您更新的映射并显示您在 PgAdmin-III 中使用的插入查询的确切文本。还可以考虑在postgresql.conf 中启用log_statement = 'all',重新启动Pg,并检查日志以查看Hibernate 实际运行的语句,或者在persistence.xml 中启用hibernate.show_sql=true 天啊,我刚刚弄明白了!我有一个小谎言,因为我们使用了 PG Admin -> 查看前 100 行并从那里编辑行,而不是使用 select。无论如何,事实证明这个编辑器以某种方式跳过了更新 ID 的过程,因此即使在 DB 中,当我们编写正确的 INSERT 时,它也会以不正确的 ID 执行!所以这基本上是我们使用的编辑器的问题,而不是数据库和应用程序......无论如何,谢谢! 仅供未来使用谷歌搜索的人使用:现在它甚至可以使用 @GeneratedValue(strategy=GenerationType.IDENTITY) @Atais 它适用于 EclipseLink,但最后我检查(诚然不久前)它不适用于 Hibernate,我不得不使用显式序列。 JPA 2.1 规范很有帮助地说“该规范没有定义这些策略的确切行为”:-(【参考方案2】:

看来你必须像这样使用序列生成器:

@GeneratedValue(generator="YOUR_SEQ",strategy=GenerationType.SEQUENCE)

【讨论】:

我可以让序列返回当前select count(*) from webuser 的值,而不是应用程序增加的值@a_horse_with_no_name? @Atais:不要使用 count() 来生成您的 ID。 从不。它根本行不通。使用序列。如果你担心数字的差距,那么你的PK“设计”有问题。序列是生成唯一 ID 的最高效、高性能、可扩展和稳健的解决方案 @Atais 不,如果这是你想要的,你应该编辑你的答案,因为那不是数据库序列。听起来您正在尝试获得无间隙序列。 @a_horse_with_no_name,我刚刚重新考虑了 count() 的想法,当然这是错误的。也许它应该返回last-id +1,还是什么?如需澄清,请查看问题的编辑。 @Atais 那也行不通。想象一下如果两个交易同时发生会发生什么。您需要通过SequenceGenerator 在数据库中使用SEQUENCE,并接受ID 可能不止一个,可能存在两个或三个或更多ID 的间隙。如果您绝对必须没有间隙,请参阅***.com/questions/2183932/…,但您不应依赖此【参考方案3】:

请尝试使用GenerationType.TABLE 而不是GenerationType.IDENTITY。数据库将创建单独的表,用于生成唯一的主键,它还将存储上次使用的 id 号。

【讨论】:

刚试过,没有效果。我们用应用程序创建了三个第一个 java 实体,然后用查询我又创建了四个。所以现在 JPA 的序列在(新)ID=4 上停止,但实际上它应该是(新)ID=8。表生成类型给了我们错误: 如果 DBMS 的内置生成器要好得多,为什么还要使用基于 Hibernate 的生成器? (顺便说一句。Hibernate 将创建该表,而不是 Database 我实际上想使用 DMBS,但它似乎使用的是 Hibernate。 GenerationType.TABLE 在几乎所有方面都不如使用SequenceGeneratorGenerationType.SEQUENCE。只有当我必须有一个可移植到每个脑死亡数据库的映射时,我才会考虑使用它。 @CraigRinger,这只是一个与数据库连接的简单应用程序,一个数据库一个应用程序,仅此而已。所以我想这是一个有点太复杂的解决方案。如果您可以查看,我还编辑了主要问题。【参考方案4】:

您还可以通过编写脚本将通用GenerationType.IDENTITY 批量转换为所选答案提出的解决方案,从而节省一些精力。下面的脚本对 Java 源文件的格式有一些轻微的依赖性,并且会在没有备份的情况下进行修改。警告购买者!

运行脚本后:

    搜索并将import javax.persistence.Table;替换为 import javax.persistence.Table; import javax.persistence.SequenceGenerator;。 在 NetBeans 中重新格式化源代码,如下所示:
      选择所有要格式化的源文件。 按 Alt+Shift+F 确认重新格式化。

将以下脚本另存为update-sequences.sh 或类似名称:

#!/bin/bash

# Change this to the directory name (package name) where the entities reside.
PACKAGE=com/domain/project/entities

# Change this to the path where the Java source files are located.
cd src/main/java

for i in $(find $PACKAGE/*.java -type f); do
  # Only process classes that have an IDENTITY sequence.
  if grep "GenerationType.IDENTITY" $i > /dev/null; then
    # Extract the table name line.
    LINE_TABLE_NAME=$(grep -m 1 @Table $i | awk 'print $4;')
    # Trim the quotes (if present).
    TABLE_NAME=$LINE_TABLE_NAME//\"
    # Trim the comma (if present).
    TABLE_NAME=$TABLE_NAME//,

    # Extract the column name line.
    LINE_COLUMN_NAME=$(grep -m 1 -C1 -A3 @Id $i | tail -1)
    COLUMN_NAME=$(echo $LINE_COLUMN_NAME | awk 'print $4;')
    COLUMN_NAME=$COLUMN_NAME//\"
    COLUMN_NAME=$COLUMN_NAME//,

    # PostgreSQL sequence name.
    SEQUENCE_NAME="$TABLE_NAME_$COLUMN_NAME_seq"

    LINE_SEQ_GENERATOR="@SequenceGenerator( name = \"$SEQUENCE_NAME\", sequenceName = \"$SEQUENCE_NAME\", allocationSize = 1 )"
    LINE_GENERATED_VAL="@GeneratedValue( strategy = GenerationType.SEQUENCE, generator = \"$SEQUENCE_NAME\" )"
    LINE_COLUMN="@Column( name = \"$COLUMN_NAME\", updatable = false )\n"

    # These will depend on source code formatting.
    DELIM_BEGIN="@GeneratedValue( strategy = GenerationType.IDENTITY )"
    # @Basic( optional = false ) is also replaced.
    DELIM_ENDED="@Column( name = \"$COLUMN_NAME\" )"

    # Replace these lines...
    #
    # $DELIM_BEGIN
    # $DELIM_ENDED
    #
    # With these lines...
    #
    # $LINE_SEQ_GENERATOR
    # $LINE_GENERATED_VAL
    # $LINE_COLUMN

    sed -i -n "/$DELIM_BEGIN/:a;N;/$DELIM_ENDED/!ba;N;s/.*\n/$LINE_SEQ_GENERATOR\n$LINE_GENERATED_VAL\n$LINE_COLUMN/;p" $i
  else
    echo "Skipping $i ..."
  fi
done

使用 NetBeans 生成 CRUD 应用程序时,ID 属性将不包括可编辑的输入字段。

【讨论】:

【参考方案5】:

对我有用

    像这样创建表,使用 SERIAL。
CREATE TABLE webuser(
    idwebuser SERIAL PRIMARY KEY,
    ...
)
    在 id 字段添加@GeneratedValue(strategy = GenerationType.IDENTITY)。
@Entity
@Table(name="webuser")
class Webuser 

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    // ....


【讨论】:

@LF,谢谢你的建议,我已经修改了答案。

以上是关于配置 JPA 让 PostgreSQL 生成主键值的主要内容,如果未能解决你的问题,请参考以下文章

求教!jpa主键自动生成策略的相关问题!

hibernate主键生成

使用jpa在postgresql数据库中创建主键自增表

无法访问 PostgreSQL 触发函数中的主键值

flea-db使用之主键生成器表介绍

jpa中Mysql数据库的主键自增怎么配置,pojo类该怎么写