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

时间:2018-04-08 01:35:45

标签: django transactions atomic

我有一个简单的任务模型:

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的{​​{1}}之前的filter之类的任务创建行中应该做更多的工作?

2)Task.objects位于何处?内部交易区块还是外部?

Big thx

2 个答案:

答案 0 :(得分:6)

  

1)在过滤现有select_for_update之前,可能还需要在Task.objects之类的任务创建行中完成更多工作?

不 - 你现在看起来很好,应该按你想要的方式工作。

  

2)return HttpResponse()位于何处?内部交易区块还是外部?

是的,这很重要。无论事务是否成功,您都需要向客户端返回响应 - 因此它肯定需要在事务块之外。如果您在事务内部执行此操作,则如果事务失败,客户端将收到500 Server Error。

但是,如果事务失败,那么您将没有新的任务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)

答案 1 :(得分: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

只要你只有一些繁琐的任务,这个操作不应该花费那么多时间,以免影响响应时间。但是您必须在自己的环境,数据库和硬件中尝试这一点。