Ya no tienes que investigar más en otras webs ya que has llegado al lugar exacto, poseemos la respuesta que buscas sin problema.
Solución:
Sé que esta pregunta es un poco más antigua, pero acabo de tener el mismo problema y quería compartir mis aprendizajes.
No estaba del todo satisfecho con la respuesta de st0nes, ya que (al menos para postgres) un LOCK TABLE
El extracto sólo puede emitirse dentro de una transacción. Y aunque en Django suele pasar casi todo dentro de una transacción, esto LockingManager
no se asegura de que realmente esté dentro de una transacción, al menos a mi entender. Además, no quería cambiar completamente los modelos. Manager
solo para poder bloquearlo en un lugar y, por lo tanto, estaba más buscando algo que funcione como el with transaction.atomic():
pero también bloquea un modelo dado.
Así que se me ocurrió esto:
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()
Entonces, si ahora quiero bloquear el modelo ModelToLock
esto se puede usar así:
with LockedAtomicTransaction(ModelToLock):
# do whatever you want to do
ModelToLock.objects.create()
EDITAR: tenga en cuenta que solo he probado esto usando postgres. Pero, según tengo entendido, también debería funcionar en mysql así.
Creo que este fragmento de código satisface sus necesidades, suponiendo que esté utilizando MySQL. De lo contrario, es posible que deba modificar un poco la sintaxis, pero la idea aún debería funcionar.
Fuente: Tablas de bloqueo
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
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')
try:
yield
finally:
cursor.close()
Esto es muy similar a la solución @jdepoix, pero más densa.
Puedes usarlo así:
with lock_table(MyModel):
MyModel.do_something()
Tenga en cuenta que esto solo funciona con PostgreSQL y usa las cadenas f de python 3.6, también conocidas como literales string interpolación.
valoraciones y reseñas
Puedes añadir valor a nuestra información aportando tu veteranía en los comentarios.