为什么我需要运行postgresql的nextval函数?怎么防止它?

3 投票
2 回答
1695 浏览
提问于 2025-04-18 03:17

我刚遇到一个关于Django和PostgreSQL的问题,搞不太懂。

我有一个简单的模型,定义如下:

class MyModel(models.Model):
    my_field = models.IntegerField()
    my_other_field = models.TextField()

在我的视图里,有类似这样的代码:

my_object = MyModel(my_field=1, my_other_field='blah')
my_object.save()

一切都运行得很好,直到今天早上。我遇到了这个错误:

 IntegrityError at /my_url/

duplicate key value violates unique constraint "my_model_pkey"
DETAIL:  Key (id)=(3) already exists.
CONTEXT:  Remote SQL command: INSERT INTO public.my_model(id, my_field, my_other_field) VALUES ($1, $2, $3) RETURNING id

我之前也遇到过这个错误,我知道它和PostgreSQL如何同步与我的模型相关的顺序表的id列有关。我必须在PostgreSQL中运行这个函数,直到返回的id值大于当前最大的id值。

select nextval('my_model_id_seq'::regclass);

我的问题是:为什么会发生这种情况?将来怎么避免呢?

顺便说一下,我插入数据到表里的唯一方式就是通过代码,从来没有手动插入过数据。

希望我的问题足够清楚。

2 个回答

1

问题的根源在这里(来自你错误信息的SQL命令):

INSERT INTO public.my_model(id, my_field, my_other_field)
VALUES ($1, $2, $3)
RETURNING id

因为你的id列似乎是一个serial类型,所以不要手动插入值。让系统自动从序列中获取默认值。应该是:

INSERT INTO public.my_model(my_field, my_other_field)
VALUES ($1, $2)
RETURNING id;

这就是一开始添加RETURNING id的目的:返回新生成的id。如果你自己传入一个值,就不需要返回它了。

解决办法

如果序列因为某种原因不同步了,比如手动输入的值和nextval()生成的数字冲突,运行这个查询一次

SELECT setval('my_model_id_seq', max(id)) FROM my_model;

这会把序列设置为当前的最大值。下一个调用就是下一个数字,不会出现错位的问题。

2

我觉得问题不是“为什么我的序列搞乱了”,而是“为什么Django在插入新行时要给id列提供一个值,而不是让数据库自动插入下一个序列值”。

Django的文档描述了它用来决定在你调用save()时是进行更新(UPDATE)还是插入(INSERT)的算法。

这个算法会检查对象的'id'字段是否已经设置了某个值。如果没有设置,它就会进行插入(通常不会指定'id'字段的值)。如果已经设置了,它会先尝试更新(UPDATE);如果更新没有成功,它就会进行插入(这次通常会指定'id'字段的值)。

正如Erwin的回答中提到的,您看到的错误信息表明它在尝试插入一行数据,同时指定'id'字段的值。

我注意到在Django的1.6版本中,这个算法似乎发生了变化。之前它会先用SELECT查询一下记录是否存在,如果存在就更新,如果不存在就插入。如果你的问题是在升级后开始出现的,那可能就是原因。文档中提到:

在一些罕见的情况下,即使数据库中存在该对象的主键值,数据库也不会报告行已被更新。例如,PostgreSQL的ON UPDATE触发器返回NULL。在这种情况下,可以通过将select_on_save选项设置为True来恢复旧的算法。

如果这对你来说是个问题,那就能解释你的症状:错误实际上是在尝试更新数据库中的值时发生的,而Django错误地认为该行不存在,然后尝试创建它。

你可以通过将'select_on_save'设置为true来检查这个问题,以恢复到旧的行为。

另一个可能的原因是,如果你的代码不小心将对象的'id'属性设置为某个值,然后调用save()。这可能会导致各种问题,具体取决于这个值在数据库中是否已经存在。特别是,这可能会导致创建一个'id'值超出当前序列范围的行,之后你在插入时就会遇到错误。

还有一个可能的原因是,在对之前从数据库加载的行使用save()时,使用了'force_insert'参数(这实际上是一个你应该更新的现有行)。

撰写回答