Подтвердить что ты не робот

Django ORM и таблица блокировки

Моя проблема такова:

У меня есть автодилер A и таблица db с именем sold_cars. Когда автомобиль продается, я создаю запись в этой таблице.

Таблица имеет целочисленный столбец с именем order_no. Он должен быть уникальным в автомобилях, продаваемых дилером.

Итак, если дилер A продал автомобили a, b and c, то этот столбец должен быть 1, 2, 3. Я должен использовать этот столбец, а не первичный ключ, потому что я не хочу иметь никаких отверстий в моей нумерации - дилер A и B (который может быть добавлен позже) должен иметь порядковые номера 1, 2, 3, а не A: 1, 3, 5 и B: 2, 4, 6. Итак... Я выбираю последний наибольший order_no для данного дилера, увеличиваю его на 1 и сохраняю.

Проблема в том, что два человека купили автомобиль у дилера A в том же миллисекунде, и оба заказа получили тот же order_no. Любой совет? Я думал закрыть этот процесс в блоке транзакций и заблокировать эту таблицу до завершения транзакции, но не могу найти информацию о том, как с этим.

4b9b3361

Ответ 1

Я думаю, что этот фрагмент кода соответствует вашим потребностям, если вы используете MySQL. Если нет, вам может потребоваться немного изменить синтаксис, но идея все равно должна работать.

Источник: Блокировка таблиц

class LockingManager(models.Manager):
    """ Add lock/unlock functionality to manager.

    Example::

        class Job(models.Model):

            manager = LockingManager()

            counter = models.IntegerField(null=True, default=0)

            @staticmethod
            def do_atomic_update(job_id)
                ''' Updates job integer, keeping it below 5 '''
                try:
                    # Ensure only one HTTP request can do this update at once.
                    Job.objects.lock()

                    job = Job.object.get(id=job_id)
                    # If we don't lock the tables two simultanous
                    # requests might both increase the counter
                    # going over 5
                    if job.counter < 5:
                        job.counter += 1                                        
                        job.save()

                finally:
                    Job.objects.unlock()


    """    

    def lock(self):
        """ Lock table. 

        Locks the object model table so that atomic update is possible.
        Simulatenous database access request pend until the lock is unlock()'ed.

        Note: If you need to lock multiple tables, you need to do lock them
        all in one SQL clause and this function is not enough. To avoid
        dead lock, all tables must be locked in the same order.

        See http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html
        """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        logger.debug("Locking table %s" % table)
        cursor.execute("LOCK TABLES %s WRITE" % table)
        row = cursor.fetchone()
        return row

    def unlock(self):
        """ Unlock the table. """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        cursor.execute("UNLOCK TABLES")
        row = cursor.fetchone()
        return row  

Ответ 2

Я знаю, что этот вопрос немного старше, но у меня была одна и та же проблема, и я хотел поделиться своими знаниями.

Я был не вполне удовлетворен ответом st0nes, поскольку (по крайней мере, для postgres) оператор LOCK TABLE может быть выпущен только в транзакции. И хотя в Django обычно почти все происходит внутри транзакции, этот LockingManager не гарантирует, что вы на самом деле находитесь внутри транзакции, по крайней мере, до моего понимания. Кроме того, я не хотел полностью изменять Модели Manager, чтобы иметь возможность заблокировать его в одном месте, и поэтому я больше искал что-то, что работает вроде как with transaction.atomic():, но также блокирует данную модель.

Итак, я придумал это:

from django.conf import settings
from django.db import DEFAULT_DB_ALIAS
from django.db.transaction import Atomic, get_connection


class LockedAtomicTransaction(Atomic):
    """
    Does a atomic transaction, but also locks the entire table for any transactions, for the duration of this
    transaction. Although this is the only way to avoid concurrency issues in certain situations, it should be used with
    caution, since it has impacts on performance, for obvious reasons...
    """
    def __init__(self, model, using=None, savepoint=None):
        if using is None:
            using = DEFAULT_DB_ALIAS
        super().__init__(using, savepoint)
        self.model = model

    def __enter__(self):
        super(LockedAtomicTransaction, self).__enter__()

        # Make sure not to lock, when sqlite is used, or you'll run into problems while running tests!!!
        if settings.DATABASES[self.using]['ENGINE'] != 'django.db.backends.sqlite3':
            cursor = None
            try:
                cursor = get_connection(self.using).cursor()
                cursor.execute(
                    'LOCK TABLE {db_table_name}'.format(db_table_name=self.model._meta.db_table)
                )
            finally:
                if cursor and not cursor.closed:
                    cursor.close()

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

with LockedAtomicTransaction(ModelToLock):
    # do whatever you want to do
    ModelToLock.objects.create()

EDIT: Обратите внимание, что я тестировал это только с помощью postgres. Но, на мой взгляд, он также должен работать на mysql именно так.

Ответ 3

from contextlib import contextmanager
from django.db import transaction
from django.db.transaction import get_connection


@contextmanager
def lock_table(model):
    with transaction.atomic():
        cursor = get_connection().cursor()
        cursor.execute(f'LOCK TABLE {model._meta.db_table}')
        yield
        cursor.close()

Это очень похоже на решение @jdepoix, но более плотное.

Вы можете использовать это так:

with lock_table(MyModel):
    MyModel.do_something()

Обратите внимание, что это работает только с PostgreSQL