Django: создание объекта в атомарной транзакции

avatar
MaxCore
8 апреля 2018 в 01:35
4081
2
6

У меня есть простая задача модель:

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, а затем создает новую.

И есть много пользователей этого метода, и я полагаю, что однажды что-то пойдет не так с заказом, потому что update и <36843331568447>create определенно должны выполняться вместе.

Итак, я просто хочу уточнить, будет ли этого достаточно, чтобы избежать повреждения данных:

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 перед filter существующих Task.objects?

2) Имеет ли значение, где находится return HttpResponse()? Внутри блока транзакций или снаружи?

Большое спасибо

Источник

Ответы (2)

avatar
solarissmoke
12 апреля 2018 в 14:59
6

1) Возможно, нужно сделать что-то еще в строке создания задачи вида select_for_update перед фильтром существующих Task.objects?

Нет — то, что у вас есть сейчас, выглядит нормально и должно работать так, как вы хотите.

2) Имеет ли значение, где находится return HttpResponse()? Внутри блока транзакций или снаружи?

Да, это важно. Вам нужно вернуть ответ клиенту независимо от того, была ли ваша транзакция успешной или нет, поэтому он определенно должен быть вне блока транзакции. Если бы вы сделали это внутри транзакции, клиент получил бы ошибку сервера 500, если транзакция не удалась.

Однако, если транзакция завершится неудачно, у вас не будет нового идентификатора задачи, и вы не сможете вернуть его в своем ответе. Поэтому вам, вероятно, нужно возвращать разные ответы в зависимости от того, успешна ли транзакция, например:

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)
MaxCore
12 апреля 2018 в 18:59
0

Большое спасибо за отличное объяснение, кстати, возможна ли только IntegrityError?

solarissmoke
13 апреля 2018 в 03:04
1

Да, это исключение, которое возникает при неудачной транзакции — см. docs.djangoproject.com/en/dev/topics/db/transactions/… .

avatar
Ralf
12 апреля 2018 в 15:51
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)

Этот метод обновляет только две другие строки при вставке новой строки. Вы все равно можете обернуть весь метод в транзакцию, если в вашем приложении действительно высокий параллелизм, но эта транзакция будет блокировать только до 3 строк, а не все остальные.

Этот подход может быть вам полезен, если у вас очень длинный список задач.


EDIT: как получить упорядоченный список задач

Это невозможно сделать на уровне базы данных в одном запросе (насколько мне известно), но вы можете попробовать эту функцию:

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

Пока у вас всего несколько сотен задач, эта операция не должна занимать так много времени, чтобы повлиять на время отклика. Но вам придется испытать это на себе, в вашей среде, в вашей базе данных, на вашем оборудовании.

MaxCore
12 апреля 2018 в 19:08
0

Этот подход потрясающий, я не думал об этом, но, честно говоря, я не могу представить, как получить список упорядоченных задач? Будет ли это легкий запрос? Что-то вроде - Task.objects.all().order_by('DO_NOT_KNOW') =)

Ralf
12 апреля 2018 в 19:22
1

Отредактирован ответ, чтобы добавить функцию для получения упорядоченного списка задач. Это в памяти, а не только один запрос к базе данных

MaxCore
18 апреля 2018 в 19:49
0

Человек, возможно, я воспользуюсь вашим решением, я очень ценю вашу идею, но предыдущий ответ точен в отношении названия, и другие пользователи должны найти его более полезным, поэтому мой голос за вас, но это будет правильно для сообщество, чтобы принять предыдущий ответ. Это нормально?)

Ralf
18 апреля 2018 в 19:51
1

Я могу жить с этим. Спасибо за ответ.