Reputation: 2208
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
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
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.
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()
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