配置 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
很重要。
请注意,如果事务回滚,此序列中会有“间隙”。交易可以出于各种原因回滚。您的应用程序应设计为应对此问题。
永远不要假设任何 idn
都有一个 id n-1
或 n+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
在几乎所有方面都不如使用SequenceGenerator
和GenerationType.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 生成主键值的主要内容,如果未能解决你的问题,请参考以下文章