Search code examples
djangodjango-admindjango-querysetmodelformmodelchoicefield

Django MultipleObjectsReturned error on form submit


I've got quite the mysterious MultipleObjectsReturned error that just popped up after weeks of not having an issue. I'm hoping is just something simple that I'm missing.

I've got an Order model, an OrderLine model, which has an Item foreign key. Each Item has a foreign key to a Product. Here are the dumbed down models:

class OrderLine(models.Model):
    order = models.ForeignKey(Order, related_name="lines", on_delete=models.CASCADE)
    item = models.ForeignKey(Item, on_delete=models.SET_NULL, blank=True, null=True)


class Product(TimeStampedModel):
   ...


class Item(TimeStampedModel):
   product = models.ForeignKey(Product, related_name='items', on_delete=models.CASCADE)

OrderLineForm and OrderLineAdmin for reference:

class OrderLineForm(forms.ModelForm):
    class Meta:
        model = OrderLine
        ...

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['category'].queryset = ItemType.objects.all()
        self.fields['product'].queryset = Product.objects.none()
        self.fields['item'].queryset = Item.objects.none()

        if self.instance.pk:
            try: # When changing an existing OrderLine
                ...
                current_item = Item.objects.filter(pk=self.instance.item.pk)
                available_items = current_item.union(
                    get_available_items(...)
                )
                self.fields['item'].queryset = available_items
                self.fields['category'].initial = item_type_id
            except:
                self.fields['item'].queryset = Item.objects.all()
        ...


@admin.register(OrderLine)
class OrderLineAdmin(admin.ModelAdmin):
    form = OrderLineForm

Now, when I use Django admin to edit an OrderLine which has more than one Item in the ModelChoiceField queryset: enter image description here

I get the following error during form clean: get() returned more than one Item -- it returned 2!

Upon closer inspection of the logs, it appears the ModelChoiceField is getting passed the correct Item id/pk, but the self.queryset.get(**{key:value}) is somehow returning 2 Items from a single id/pk, even though the Items have different id/pks (49 and 50): enter image description here

Again, this only happens when the OrderLine form's Item field has more than one object in the queryset. If it's only a single Item, it saves just fine. Any ideas why I'm getting this error now? Thanks!

The only thing I can think has changed in terms of database relationhips is that I added formset.save_m2m() to the Item model admin, however Item isn't a m2m relationship, so perhaps that could have led to some database indexing error?

P.S. I found this https://code.djangoproject.com/ticket/23354 from years ago that seems to reference the error in this context, but the ticket said it was fixed.


Solution

  • From these two lines we can see that you perform a union and set this as the fields queryset:

    current_item = Item.objects.filter(pk=self.instance.item.pk)
    available_items = current_item.union(
        get_available_items(...)
    )
    

    From the documentation on union:

    In addition, only LIMIT, OFFSET, COUNT(*), ORDER BY, and specifying columns (i.e. slicing, count(), exists(), order_by(), and values()/values_list()) are allowed on the resulting QuerySet. Further, databases place restrictions on what operations are allowed in the combined queries. For example, most databases don’t allow LIMIT or OFFSET in the combined queries.

    Considering that the field will call get on this queryset to validate the selected choice a union is not feasible for it. Considering your use case in fact there is a better option of us just using the SQL OR operator. There are mainly 2 ways to do this:

    1. Use the | operator:

       available_items = current_item | get_available_items(...)
      

      This is equivalent to saying SELECT ... WHERE (condition for current item) OR (conditions for available items).

    2. Use Q objects:

      The previous method was not very great considering we may want to make queries having quite complex conditions. This would result in us writing a bunch of querysets and then using | and & on them. Rather than doing this we have the great option of using Q objects which can take as keyword arguments the same arguments you would have passed to filter:

       from django.db.models import Q
      
      
       available_items = Item.objects.filter(Q(pk=self.instance.item.pk) | Q(some_condition_for_available=True))