配置JPA使PostgreSQL生成主键值

49

我们的项目使用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,然后在创建过程之后获取它? 或者还有更好的解决方案吗? 谢谢。
编辑: 更具体地说: 我们有一个用户表,我的问题是使用任何类型的策略generationtype时,JPA都使用由其生成器指定的ID插入新实体。这对我来说是错误的,因为如果我自己对表进行更改,通过添加新条目,应用程序的GeneratedValue低于当前ID - 这导致我们出现重复ID的异常。 我们能不能修复它?;)
对答案的简短说明: 我这边有点撒谎,因为我们使用了PG Admin->查看前100行并从中编辑行,而不是使用SELECT。无论如何,结果发现此编辑器以某种方式跳过更新ID的过程,因此即使在DB中,当我们编写正确的INSERT时,它也会使用不正确的ID执行!所以基本上这更多是我们使用的编辑器的问题,而不是数据库和应用程序...现在甚至可以使用@GeneratedValue(strategy=GenerationType.IDENTITY)。

这是否意味着JPA没有使用PostgreSQL序列?该表中该列的定义是什么?它是一个serial还是只是一个integer - user330315
我们正在使用串行字段类型。我会在主问题中更详细地描述问题,@a_horse_with_no_name - Atais
那么这意味着JPA 没有使用相关的序列吗?这很奇怪。 - user330315
当然可以,你只需要告诉JPA/Hibernate/...使用相同的策略。我相信Craig的回答已经给出了你所需的一切。 - user330315
有时,如果您导入数据或以某种方式手动输入ID值,则您的数据库序列可能不是最新的。这将生成一个已存在的ID。在这种情况下,请考虑更新表序列:SELECT setval('users_id_seq',(select(max(id)+1)from users)); - Bhdr
显示剩余3条评论
5个回答

97

给定表的定义:

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是PostgreSQL中SERIAL的默认序列命名方式,我建议您遵循这种方式。如果需要Hibernate与其他客户端协同工作,则allocationSize=1很重要。请注意,如果事务回滚,则此序列将具有“间隙”。事务可能因各种原因而回滚,您的应用程序应设计为能够处理这种情况。
  • 永远不要假设对于任何id n 都存在一个id n-1n+1
  • 永远不要假设id n 在小于n的id或大于n的id之前添加或提交。如果你非常小心地使用序列,可以这样做,但你永远不应该尝试;在表中记录时间戳。
  • 永远不要从ID中添加或减去。只比较它们是否相等,不要进行其他操作。
请参见PostgreSQL文档中关于序列的说明serial数据类型。它们解释了上面的表定义基本上是一个快捷方式。
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中搜索“无间隔序列 postgresql”)。如果有多个连接在数据库上工作,则以上两种方法都会出现问题。

@Atais 很高兴知道问题已经解决。希望你也学到了一些调试技巧,以备下次使用JPA时遇到莫名其妙的“发生了什么”情况。相信我,下次肯定会有的... - Craig Ringer
12
仅供未来需要谷歌的人参考:现在甚至可以使用@GeneratedValue(strategy=GenerationType.IDENTITY) - Atais
1
@Atais 它适用于EclipseLink,但是我上次检查(承认是一段时间以前)它在Hibernate上不起作用,我不得不使用显式序列。 JPA 2.1规范有帮助地指出:“此规范未定义这些策略的确切行为” :-( - Craig Ringer
@Atais非常感谢您抽出时间发布这行代码,使我们节省了很多时间。谢谢! - Pavel_K
@Bhdr 但是你不应该需要这样做,因为这意味着你一开始就做错了什么。这是一个恢复步骤,但也很重要的是要明白如果有并发插入时结果仍然是错误的。最好的做法是 BEGIN; LOCK TABLE mytable; SELECT setval(...); COMMIT; - Craig Ringer
显示剩余6条评论

2

它对我有效

  1. 创建这样的表,使用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,感谢您的建议,我已经修改了答案。 - hang gao

2

看起来你需要使用类似于序列生成器的东西:

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

我能否让Sequence返回当前select count(*) from webuser的值,而不是应用程序递增的值,@a_horse_with_no_name? - Atais
1
@Atais:绝对不要使用count()来生成你的ID。永远不要这样做。它根本行不通。使用序列。如果你担心数字中的间隔,那么你的PK“设计”有问题。序列是生成唯一ID最有效、高效、可扩展和稳健的解决方案。 - user330315
@Atais 不是的,如果你想要这样做,你应该编辑你的答案来说明,因为那不是一个数据库序列。听起来像是你正在尝试获得一个无间隙的序列。 - Craig Ringer
@a_horse_with_no_name,我刚刚重新考虑了count()的想法,当然是错误的。也许它应该返回最后一个id +1,或者其他什么?请查看问题的编辑以获得澄清。 - Atais
@Atais:不要使用last_id + 1(这也会是错误的)。序列是生成唯一ID最有效和可扩展的解决方案。 - user330315
1
@Atais 这也行不通。想象一下如果两个事务同时发生会发生什么。你需要通过 SequenceGenerator 在数据库中使用 SEQUENCE,并接受 ID 可能相差超过一个的事实,可能会有两个或三个或更多 ID 的间隔。如果你绝对必须没有间隔,请参见 https://dev59.com/ZUvSa4cB1Zd3GeqPgrYW#11752742 但是你不应该依赖这个 - Craig Ringer

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

我实际上想使用DMBS,但似乎它正在使用Hibernate的。 - Atais
1
GenerationType.TABLE 在几乎所有方面都不如使用带有 GenerationType.SEQUENCESequenceGenerator。只有在必须拥有可移植到每个愚蠢的数据库的映射时,我才会考虑使用它。 - Craig Ringer
@CraigRinger,嗯,这只是一个与数据库连接的简单应用程序,一个数据库对应一个应用程序,没有更多的东西。所以我想这可能是一个过于复杂的解决方案。如果您可以看一下,我还编辑了主要问题。 - Atais
@Atais 是的,我看到了。如果你想让 Hibernate 编辑和在 Hibernate 之外进行的编辑保持同步,请使用序列生成器,因为这是 PostgreSQL 用于你(希望)用于生成主键的 SERIAL 列的方式。 - Craig Ringer
我认为只有在您想要支持多个数据库时,这才是有用的。 - fatihpense
显示剩余2条评论

1
您还可以编写一个脚本来执行批量转换通用的 GenerationType.IDENTITY 到所选答案提出的解决方案,以减轻工作量。下面的脚本在某种程度上依赖于 Java 源文件的格式,并且会在没有备份的情况下进行修改。请自行权衡风险!
运行脚本后:
  1. 搜索并替换 import javax.persistence.Table;import javax.persistence.Table; import javax.persistence.SequenceGenerator;
  2. 在 NetBeans 中按照以下方式重新格式化源代码:
    1. 选择要格式化的所有源文件。
    2. 按下 Alt+Shift+F
    3. 确认重新格式化。
将以下脚本保存为 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属性不会包括可编辑的输入字段。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接