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:
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):
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.
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:
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)
.
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))