mohammad
mohammad

Reputation: 2208

locking some special rows of a database table n django

I have a model in Django like follows:

class A(models.Model):
    STATUS_DEFAULT = "default"
    STATUS_ACCEPTED = "accepted"
    STATUS_REJECTED = "rejected"
    STATUS_CHOICES = (
        (STATUS_DEFAULT, 'Just Asked'),
        (STATUS_ACCEPTED, 'Accepted'),
        (STATUS_REJECTED, 'Rejected'),
    )

    status = models.CharField(choices=STATUS_CHOICES, max_length=20, default=STATUS_DEFAULT)
    question = models.ForeignKey(Question)

Notice that Question is another model in my project. I have a constraint on the A model. Between rows with the same question only one of them can has status=STATUS_ACCEPTED and at the first all of them have status=STATUS_DEFAULT. I want to write a function that does the following :

def accept(self):
    self.status = STATUS_ACCEPTED
    self.save()
    A.objects.filter(question=self.question).update(status=STATUS_REJECTED)

But if two instances of A with same question call this function maybe a race condition will happen. So the one who calls this function sooner should lock other instances with same question to prevent race condition.

How should I do this?

Upvotes: 1

Views: 1219

Answers (2)

Michael Rigoni
Michael Rigoni

Reputation: 1966

Assuming you are using a DB backend that supports locks, you can lock the question using select_for_update

You code could then look like:

@transaction.atomic
def accept(self):
    # Lock related question so no other instance will run the following code at the same time.
    Question.objects.filter(pk=self.question.pk).select_for_update()

    # now we have the lock, reload to make sure we have not been updated meanwhile
    self.refresh_from_db()
    if self.status != STATUS_REJECTED:
        A.objects.filter(question=self.question).exclude(pk=self.pk).update()
        self.status = STATUS_ACCEPTED
        self.save()
    else:
        raise Exception('An answer has already been accepted !')

With that code, only one instance at a time will be able to run the code after select_for_update (for a given question).

Note the refresh_from_db call as while waiting to acquire the lock, another instance may have accepted another answer...

Upvotes: 3

Owen Hempel
Owen Hempel

Reputation: 434

As I understand it, you want to make sure that two instances of A which share a Question cannot both simultaneously have the 'accepted' status. A objects are initiated at the default status.

Perhaps you should rethink your approach:

Let the question itself tell you which A has the accepted status.

Solution:

add the following to your Question model:

accepted_a = models.OneToOneField(A, null = true, default = null)

since you seem to want the accept method to be part of the A class, you can write your accept the way you have it laid out in your question. I disagree though, I think the behaviour of the Question is that the Question accepts the A, so the method should be defined in Question class.

def accept(self,A):
  self.accepted_a = A

now, in your views, when you want the A to get accepted, you would write:

q = Question.objects.get(Question_id)
a = A.objects.get(A_id)
q.accept(A)
q.save()

How this works:

Django (and databases in general) provides a mechanism by which a relationship can specify One-to-One relationships. By using that in the Question model, we specify that each question can have exactly one accepted A. This does not override or alter the behaviour of the Many-to-One relationship the Question has with A.

Our accept is a bit naive though, it doesn't look to see if the question is a foreign key to A. We chan change that (or any other logic you wish): Edit: With information provided in comments, we need to ensure that the first Ask (A) To accept the question locks it out. To that end, we will check if the question already has an acceptor Ask. Since a question defaults to null, we can simply test if it is null.

def accept(self, A):
  if (A.question == self) and (self.accepted_a==null):
    self.accepted_a = A
    return True
  else:
    return False

Upvotes: 1

Related Questions