Saltar al contenido

Django ORM y tabla de bloqueo

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 ModelToLockesto 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.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *