【问题标题】:Django: object creation in atomic transactionDjango:原子事务中的对象创建
【发布时间】:2018-04-08 01:35:44
【问题描述】:

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

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)

View 将现有的任务转移到 +1 新创建的之后,然后创建一个新的。

而且这种方法有很多用户,我想有一天订购会出错,因为updatecreate绝对应该一起执行。 p>

所以,我只想说清楚,是否足以避免任何数据损坏:

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) 可能,在现有的Task.objectsfilter 之前的select_for_update 之类的任务创建行中应该做更多的事情?

2) return HttpResponse() 的位置重要吗?交易块内部还是外部?

谢谢

【问题讨论】:

    标签: django transactions atomic


    【解决方案1】:

    1) 可能,在过滤现有Task.objects 之前,应该在select_for_update 之类的任务创建行中做更多的事情?

    否 - 您目前拥有的东西看起来不错,应该可以按照您想要的方式工作。

    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)
    

    【讨论】:

    【解决方案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 设置为空的任务。所以要拿到队列的第一个任务:

    # 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') =)
    • 已编辑答案以添加功能以获取有序的任务列表。它在内存中,而不仅仅是单个数据库查询
    • 伙计,我可能会使用您的解决方案,我非常感谢您的想法,但之前的答案与标题完全相同,并且其他用户应该会发现它更有用,所以,我的投票是给你的,但社区接受以前的答案是正确的。可以吗?)
    • 我可以忍受。感谢您的反馈。
    猜你喜欢
    • 2018-03-16
    • 1970-01-01
    • 1970-01-01
    • 2016-06-25
    • 2020-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多