Django:原子事务中的对象创建

6

I have a simple Task model:

class Task(models.Model):

    name = models.CharField(max_length=255)
    order = models.IntegerField(db_index=True)

以下是一个简单的task_create视图:

def task_create(request):

    name = request.POST.get('name')
    order = request.POST.get('order')

    Task.objects.filter(order__gte=order).update(order=F('order') + 1)
    new_task = Task.objects.create(name=name, order=order)

    return HttpResponse(new_task.id)

查看任务的现有转移会在新创建的任务后面+1,然后创建一个新任务。

许多用户使用此方法,我认为由于更新和创建一定要一起执行,某一天排序肯定会出问题。

所以,我只想确保,是否足以避免任何数据损坏:

from django.db import transaction

def task_create(request):

    name = request.POST.get('name')
    order = request.POST.get('order')

    with transaction.atomic():
        Task.objects.select_for_update().filter(order__gte=order).update(order=F('order') + 1)
        new_task = Task.objects.create(name=name, order=order)

    return HttpResponse(new_task.id)

1) 可能需要在任务创建行中使用类似于select_for_update的操作,来保证存在于Task.objectsfilter运行之前。

2) return HttpResponse()的位置是否重要?它应该放在事务块内还是外面?

非常感谢!

2个回答

6

1) 可能需要在创建任务行中添加select_for_update,然后再筛选现有的Task.objects吗?

不需要。您当前的代码看起来很好,应该可以按照您想要的方式工作。

2) return HttpResponse()的位置是否重要?在事务块内还是外面?

是的,位置很重要。无论事务成功与否,您都需要向客户端返回响应 - 所以它一定需要在事务块之外。如果您将其放在事务内部,则如果事务失败,客户端将收到500服务器错误。

但是,如果事务失败,则您将没有新的任务ID,并且无法在响应中返回该ID。因此,您可能需要根据事务是否成功返回不同的响应,例如:

from django.db import IntegrityError, transaction

try:
    with transaction.atomic():
        Task.objects.select_for_update().filter(order__gte=order).update(
                                                           order=F('order') + 1)
        new_task = Task.objects.create(name=name, order=order)
except IntegrityError:
    # Transaction failed - return a response notifying the client
    return HttpResponse('Failed to create task, please try again!')

# If it succeeded, then return a normal response
return HttpResponse(new_task.id)

非常感谢您的出色解释,顺便问一下,只有IntegrityError是可能的吗? - MaxCore
1
是的,这就是一个失败的事务会引发的异常 - 请参见 https://docs.djangoproject.com/en/dev/topics/db/transactions/#django.db.transaction.atomic。 - solarissmoke

2

您也可以尝试更改模型,以便在插入新行时无需更新太多其他行。

例如,您可以尝试类似于双向链表的东西。

(我在这里使用了长而明确的字段和变量名称)。

# models.py
class Task(models.Model):
    name = models.CharField(max_length=255)
    task_before_this_one = models.ForeignKey(
        Task,
        null=True,
        blank=True,
        related_name='task_before_this_one_set')
    task_after_this_one = models.ForeignKey(
        Task,
        null=True,
        blank=True,
        related_name='tasks_after_this_one_set')

队列顶部的任务是那些 task_before_this_one 字段设置为 null 的任务。因此,要获取队列中的第一个任务:

# these will throw exceptions if there are many instances
first_task = Task.objects.get(task_before_this_one=None)
last_task = Task.objects.get(task_after_this_one=None)

当插入一个新实例时,您只需要知道它应该放在哪个任务之后(或者在哪个任务之前)。以下代码可以实现此功能:

def task_create(request):
    new_task = Task.objects.create(
        name=request.POST.get('name'))

    task_before = get_object_or_404(
        pk=request.POST.get('task_before_the_new_one'))
    task_after = task_before.task_after_this_one

    # modify the 2 other tasks
    task_before.task_after_this_one = new_task
    task_before.save()
    if task_after is not None:
        # 'task_after' will be None if 'task_before' is the last one in the queue
        task_after.task_before_this_one = new_task
        task_after.save()

    # update newly create task
    new_task.task_before_this_one = task_before
    new_task.task_after_this_one = task_after  # this could be None
    new_task.save()

    return HttpResponse(new_task.pk)

当插入新行时,此方法仅更新其他2行。如果您的应用程序具有非常高的并发性,则可能仍希望将整个方法包装在事务中,但此事务仅锁定最多3行,而不是所有其他行。

如果您有非常长的任务列表,则可以使用此方法。


编辑:如何获取任务的有序列表

据我所知,无法在单个查询中在数据库级别上完成此操作,但您可以尝试使用此函数:

def get_ordered_task_list():
    # get the first task
    aux_task = Task.objects.get(task_before_this_one=None)

    task_list = []
    while aux_task is not None:
        task_list.append(aux_task)
        aux_task = aux_task.task_after_this_one

    return task_list

只要您只有几百个任务,这个操作不应该花费太多时间,从而影响响应时间。但是您需要在您的环境、数据库和硬件中自己尝试一下。

这种方法很棒,我之前没有想过,但是老实说我无法想象如何获取有序的任务列表?这将是一个轻量级查询吗?类似于 - Task.objects.all().order_by('DO_NOT_KNOW') =) - MaxCore
1
编辑答案以添加获取任务有序列表的函数。它在内存中,不仅是单个数据库查询。 - Ralf
兄弟,我可能会使用你的解决方案,非常感谢你的想法,但是之前的答案确切地回答了标题问题,而且其他用户应该会发现它更有用,所以我的投票是给你的,但是对于社区来说接受之前的答案是正确的。可以吗? - MaxCore
1
我可以接受那个。感谢您的反馈。 - Ralf

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