views:

30

answers:

1

I have a model with a unique integer that needs to increment with regards to a foreign key, and the following code is how I currently handle it:

class MyModel(models.Model):
    business = models.ForeignKey(Business)
    number = models.PositiveIntegerField()
    spam = models.CharField(max_length=255)

    class Meta:
        unique_together = (('number', 'business'),)

    def save(self, *args, **kwargs):
        if self.pk is None:  # New instance's only
            try:
                highest_number = MyModel.objects.filter(business=self.business).order_by('-number').all()[0].number
                self.number = highest_number + 1
            except ObjectDoesNotExist:  # First MyModel instance
                self.number = 1
        super(MyModel, self).save(*args, **kwargs)

I have the following questions regarding this:

  1. Multiple people can create MyModel instances for the same business, all over the internet. Is it possible for 2 people creating MyModel instances at the same time, and .count() returns 500 at the same time for both, and then both try to essentially set self.number = 501 at the same time (raising an IntegrityError)? The answer seems like an obvious "yes, it could happen", but I had to ask.
  2. Is there a shortcut, or "Best way" to do this, which I can use (or perhaps a SuperAutoField that handles this)?

I can't just slap a while model_not_saved: try:, except IntegrityError: in, because other restraints in the model could lead to an endless loop, and a disaster worse than Chernobyl (maybe not quite that bad).

+1  A: 

You want that constraint at the database level. Otherwise you're going to eventually run into the concurrency problem you discussed. The solution is to wrap the entire operation (read, increment, write) in a transaction.

Why can't you use an AutoField for instead of a PositiveIntegerField?

number = models.AutoField()

However, in this case number is almost certainly going to equal yourmodel.id, so why not just use that?

Edit:

Oh, I see what you want. You want a numberfield that doesn't increment unless there's more than one instance of MyModel.business.

I would still recommend just using the id field if you can, since it's certain to be unique. If you absolutely don't want to do that (maybe you're showing this number to users), then you will need to wrap your save method in a transaction.

You can read more about transactions in the docs:

http://docs.djangoproject.com/en/dev/topics/db/transactions/

If you're just using this to count how many instances of MyModel have a FK to Business, you should do that as a query rather than trying to store a count.

Paul McMillan
@Paul - thanks. I don't understand. I didn't know a transaction would guarantee no changes to any rows read during the transaction, as well as, of course, protecting the commit phase for partly failing. This is so? If so I can simply wrap save in `@commit_on_success`, no?
orokusaki
@Paul - It also seems that transactions will not address the issue of a different broken constraint's `IntegrityError` potentially being confused with one caused by `number` during concurrency.
orokusaki
Precise transaction behavior will depend on the database you're using and how you have it configured. If you enable the transaction middleware and set the isolation level to repeatable read on your database, your method will work as-is. This probably won't be a performance problem in this case, but it might impact the rest of your code.
Paul McMillan
If you have transactions enabled as described above, you shouldn't have to worry about catching an IntegrityError, since each save action affecting the same rows will happen in serial.
Paul McMillan
Also, you should use the `Max()` function on that queryset rather than the filter. It'll be more efficient in your DB. http://docs.djangoproject.com/en/1.2/ref/models/querysets/#django.db.models.QuerySet.Max
Paul McMillan
Also, do you have a good reason that MyModel.id isn't an acceptible substitute for your current MyModel.number?
Paul McMillan
@Paul - excellent! Thanks a lot.
orokusaki
@Paul - One last question: To be "in serial" do I need to use a transaction wrapper, or by "transactions enabled as described above" are you saying the way my question has it, or the way you've said to use transactions?
orokusaki
If you enable the transaction middleware and set your database to the appropriate transaction isolation level, your code will work as you have it in the question because each http request will be processed in a single transaction.
Paul McMillan