gpt4 book ai didi

python - Django 和 Postgres 中为 select_for_update 生成的查询顺序的差异

转载 作者:太空狗 更新时间:2023-10-29 17:20:33 27 4
gpt4 key购买 nike

我遇到了一个奇怪的情况,当在 transaction.atomic() block 中使用 select_for_update() 时,Django 和 Postgres 中记录的查询顺序不同。

基本上,我有一个 ModelForm,我在其中针对数据库验证 cleaned_data 是否存在重复请求。然后在创建 View 的 form_valid() 方法中,我正在保存实例。为了在同一个事务中执行这两个操作,我覆盖了 post() 方法,并将这两个方法调用包装在 transaction.atomic() 中。

这是我上面所说的代码:

# Form
class MenuForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user_id = kwargs.pop('user_id', None)
super(MenuForm, self).__init__(*args, **kwargs)

def clean(self):
cleaned_data = super(MenuForm, self).clean()
dish_name = cleaned_data.get('dish_name')
menus = Menu.objects.select_for_update().filter(user_id=self.user_id)

for menu in menus:
if menu.dish_name == dish_name:
self.add_error('dish_name', 'Dish already exists')
return cleaned_data
return cleaned_data

# CreateView
class MenuCreateView(CreateView):
form_class = MenuForm

def get_form_kwargs(self):
kwargs = super(MenuCreateView, self).get_form_kwargs()
kwargs.update({'user_id': self.request.session.get('user_id')})
return kwargs

def form_valid(self, form):
user = User.objects.get(id=self.request.session.get('user_id'))
form.instance.user = user
return super(MenuCreateView, self).form_valid(form)

def post(self, request, *args, **kwargs):
form = self.get_form()

with transaction.atomic():
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)

现在假设我同时发出两个请求,以创建包含同一道菜的菜单。我预计第二个请求会失败。但是,他们两个都在过去。看起来,第二个交易没有看到前一个交易中所做的更改。因此,在 select_for_update() 返回的两个事务中,总的 menus 保持不变。

鉴于 Postgres 默认隔离级别是 READ COMMITTED,我希望这些更改是可见的。因此,我尝试记录查询以查看 COMMIT;在正确的时间被解雇。这是 django 和 postgres 中的查询日志:

Django 日志:

SELECT "menu"."id", "menu"."dish_id", "menu"."dish_name" FROM "menu" WHERE ("menu"."dish_name" = "Test Dish") FOR UPDATE; args=("Test Dish")    
INSERT INTO "menu" ("dish_id", "dish_name") VALUES (2, "Test Dish") RETURNING "menu"."id"; args=(2, "Test Dish")
SELECT "menu"."id", "menu"."dish_id", "menu"."dish_name" FROM "menu" WHERE ("menu"."dish_name" = "Test Dish") FOR UPDATE; args=("Test Dish")
INSERT INTO "menu" ("dish_id", "dish_name") VALUES (2, "Test Dish") RETURNING "menu"."id"; args=(2, "Test Dish")

Postgres 日志:

<2016-03-18 17:55:46.176 IST  0  2/31  56ebf3ca.aac0>LOG:  statement: SHOW default_transaction_isolation
<2016-03-18 17:55:46.177 IST 0 2/32 56ebf3ca.aac0>LOG: statement: SET TIME ZONE 'UTC'
<2016-03-18 17:55:46.178 IST 0 2/33 56ebf3ca.aac0>LOG: statement: SELECT t.oid, typarray
FROM pg_type t JOIN pg_namespace ns
ON typnamespace = ns.oid
WHERE typname = 'hstore';

<2016-03-18 17:55:46.182 IST 0 2/34 56ebf3ca.aac0>LOG: statement: BEGIN
<2016-03-18 17:55:46.301 IST 0 3/2 56ebf3ca.aac1>LOG: statement: SHOW default_transaction_isolation
<2016-03-18 17:55:46.302 IST 0 3/3 56ebf3ca.aac1>LOG: statement: SET TIME ZONE 'UTC'
<2016-03-18 17:55:46.302 IST 0 3/4 56ebf3ca.aac1>LOG: statement: SELECT t.oid, typarray
FROM pg_type t JOIN pg_namespace ns
ON typnamespace = ns.oid
WHERE typname = 'hstore';

<2016-03-18 17:55:46.312 IST 0 3/5 56ebf3ca.aac1>LOG: statement: BEGIN
<2016-03-18 17:55:46.963 IST 0 3/5 56ebf3ca.aac1>LOG: statement: SELECT "menu"."id", "menu"."dish_id", "menu"."dish_name" FROM "menu"
WHERE ("menu"."dish_name" = "Test Dish") FOR UPDATE
<2016-03-18 17:55:46.964 IST 0 2/34 56ebf3ca.aac0>LOG: statement: SELECT "menu"."id", "menu"."dish_id", "menu"."dish_name" FROM "menu"
WHERE ("menu"."dish_name" = "Test Dish") FOR UPDATE
<2016-03-18 17:55:47.040 IST 23712 3/5 56ebf3ca.aac1>LOG: statement: INSERT INTO "menu" ("dish_id", "dish_name") VALUES (2, "Test Dish")RETURNING "menu"."id"
<2016-03-18 17:55:47.061 IST 23712 3/5 56ebf3ca.aac1>LOG: statement: COMMIT
<2016-03-18 17:55:47.229 IST 23713 2/34 56ebf3ca.aac0>LOG: statement: INSERT INTO "menu" ("dish_id", "dish_name") VALUES (2, "Test Dish")RETURNING "menu"."id"
<2016-03-18 17:55:47.231 IST 23713 2/34 56ebf3ca.aac0>LOG: statement: COMMIT

Postgres.conf:

max_connections = 100
log_destination = 'stderr'
logging_collector = on
log_directory = 'pg_log'
log_line_prefix = '<%m %x %v %c>'
log_statement = 'all'

如您所见,两个日志中 SELECT 和 INSERT 查询的顺序不同。我无法理解为什么会这样。此外,如果您注意到,Postgres 日志中 SELECT 查询的 session_id 是不同的。这能说明什么吗?

如果这是预期的行为,我该如何解决这里的核心问题?避免基于现有记录的并发 INSERT 查询。

更新:

我没有提到忽略重复菜单的实际逻辑不仅仅基于菜名。上面一个是简化的例子。

将菜单模型考虑为:

class Menu:
user_id = models.IntegerField()
dish = models.ForeignKey(Dish)
order_start_time = models.DateTimeField()
order_end_time = models.DateTimeField()

实际逻辑是这样的:

  • 从数据库中获取所有带有 dish_name 的菜单。
  • 检查所有这些菜单的 order_start_timeorder_end_time,看看它们是否与 order_start_timeorder_end_time 重叠 用于新菜单。如果发现冲突,请避免添加。

因此,我们可以为菜肴添加两个菜单 - d1,具有订购窗口 - [9am-10am][2pm-3pm].

最佳答案

已编辑:

如何让 Django 验证重叠保留?

可以为模型validate_unique添加特殊方法:

    from django.db import models
from django.core.validators import ValidationError
from django.forms.forms import NON_FIELD_ERRORS

class Dish(models.Model):
name = models.CharField('Dish name', max_length=200)

class Menu(models.Model):
user_id = models.IntegerField()
dish = models.ForeignKey(Dish)
order_start_time = models.DateTimeField()
order_end_time = models.DateTimeField()

def validate_unique(self, *args, **kwargs):
# call inherited unique validators
super().validate_unique(*args, **kwargs) # or super(Menu, self) for Python2.7
# query if DB already has object with same dish
# and overlapping reservation
# [order_start_time, order_end_time]
qs = self.__class__._default_manager.filter(
order_start_time__lte=self.order_end_time,
order_end_time__gte=self.order_start_time,
dish=self.dish,
)
# and this object is not the same we are working with
if not self._state.adding and self.pk is not None:
qs = qs.exclude(pk=self.pk)
if qs.exists():
raise ValidationError({
NON_FIELD_ERRORS: ['Overlapping order dates for dish'],
})

让我们在控制台中尝试一下:

    from core.models import *
m=Menu(user_id=1, dish_id=1, order_start_time='2016-03-22 10:00', order_end_time='2016-03-22 15:00')
m.validate_unique()
# no output here - all is ok
m.save()
print(m.id)
8

# lets add duplicate
m=Menu(user_id=1, dish_id=1, order_start_time='2016-03-22 12:00', order_end_time='2016-03-22 13:00')
m.validate_unique()
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/el/tmp/hypothesis_test/menu/core/models.py", line 29, in validate_unique
NON_FIELD_ERRORS: ['Overlapping order dates for dish'],
django.core.exceptions.ValidationError: {'__all__': ['Overlapping order dates for dish']}

# excellent! dup is found!

# But! Django helps you find dups but allows you to add them to db if you want it!
# It's responsibility of your application not to add duplicates.

m.save()
print(m.id)
9

如何确定noboby可以添加重复项?

在这种情况下,您需要在数据库级别进行约束。

在 PostgreSQL 控制台中:

    CREATE EXTENSION btree_gist;

-- our table:
SELECT * FROM core_menu;
id | user_id | order_start_time | order_end_time | dish_id
----+---------+------------------------+------------------------+---------
8 | 1 | 2016-03-22 13:00:00+03 | 2016-03-22 18:00:00+03 | 1
9 | 1 | 2016-03-22 15:00:00+03 | 2016-03-22 16:00:00+03 | 1

DELETE FROM core_menu WHERE id=9; -- we should remove dups before adding unique constraint

ALTER TABLE core_menu
ADD CONSTRAINT core_menu_exclude_dish_same_tstzrange_constr
EXCLUDE USING gist (dish_id WITH =, tstzrange(order_start_time, order_end_time) WITH &&);

现在让我们创建复制对象并将其添加到数据库:

    m=Menu(user_id=1, dish_id=1, order_start_time='2016-03-22 13:00', order_end_time='2016-03-22 14:00')

m.save()
Traceback (most recent call last):
File "/Users/el/tmp/hypothesis_test/venv/lib/python3.5/site-packages/django/db/backends/utils.py", line 64, in execute
return self.cursor.execute(sql, params)
psycopg2.IntegrityError: ОШИБКА: конфликтующее значение ключа нарушает ограничение-исключение "core_menu_exclude_dish_same_tstzrange_constr"
DETAIL: Key (dish_id, tstzrange(order_start_time, order_end_time))=(1, ["2016-03-22 13:00:00+00","2016-03-22 14:00:00+00")) conflicts with existing key (dish_id, tstzrange(order_start_time, order_end_time))=(1, ["2016-03-22 10:00:00+00","2016-03-22 15:00:00+00")).

太棒了!现在,数据在程序和数据库级别得到验证。

关于python - Django 和 Postgres 中为 select_for_update 生成的查询顺序的差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36087107/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com