Django ORM и таблица блокировки
моя проблема заключается в следующем:
у меня есть автомобильный дилер A и таблица db с именем sold_cars
. Когда автомобиль продается, я создаю запись в этой таблице.
таблица имеет целочисленный столбец с именем order_no
. Он должен быть уникальным в автомобилях, продаваемых дилером.
так если дилер продал автомобили 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. Есть советы? Я думал о закрытии этого процесса в блоке транзакций и блокировке этой таблицы до завершения транзакции, но не могу найти никакой информации о том, как это сделать.
2 ответов
Я думаю, что этот фрагмент кода отвечает вашим потребностям, предполагая, что вы используете 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
Я знаю, что этот вопрос немного старше, но у меня была та же проблема и я хотел поделиться своими знаниями.
Я не был вполне удовлетворен ответом st0nes, так как (по крайней мере, для postgres) a 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 точно так же.