Django多对多关联注解SQLite正常,MariaDB报子查询多行错误
Django ManyToMany字段注解在MariaDB下的多值子查询错误解决
问题场景
模型定义如下:
class Link(models.Model): products = models.ManyToManyField(Product, related_name = "%(class)s_name", related_query_name = "product_link_qs", blank = True) position = models.ForeignKey(Position, on_delete = models.CASCADE) class Position(models.Model): place = models.PositiveIntegerField(unique = True) store = models.ForeignKey(Store, on_delete = models.CASCADE) class Store(models.Model): name = models.CharField("name", max_length = 32)
在Admin中尝试通过注解关联Store名称以实现排序功能:
@admin.register(Link) class LinkAdmin(admin.ModelAdmin): list_display = ["product", "get_store"] list_filter = ["position__store"] ### extend by product_link_qs related name property to make field sortable in the admin def get_queryset(self, request): qs = super().get_queryset(request) return qs.annotate(storename = Product.objects.filter(product_link_qs = OuterRef("id")).values("store__name")) @admin.display(description = "store name", ordering = "storename") def get_store(self, obj): return obj.storename or None
现象:
- 当单个Link仅关联一个Product时,测试环境SQLite和生产环境MariaDB均正常运行。
- 当单个Link关联多个Product时,SQLite仍正常,但MariaDB抛出错误:
django.db.utils.OperationalError: (1242, 'Subquery returns more than 1 row')
错误原因
SQLite对单值注解的子查询有宽松处理:当子查询返回多行时,会自动取第一条结果填充注解字段。但MariaDB严格遵循SQL标准,不允许返回多行的子查询赋值给单个字段,因此触发1242错误。
解决方案
方案1:直接通过关联关系获取Store名称(最优)
Link本身关联Position,Position关联Store,无需绕Product查询,直接通过外键链取值即可避免子查询问题:
@admin.register(Link) class LinkAdmin(admin.ModelAdmin): list_display = ["product", "get_store"] list_filter = ["position__store"] def get_queryset(self, request): qs = super().get_queryset(request) # 直接通过外键关联注解Store名称 return qs.annotate(storename=F('position__store__name')) @admin.display(description = "store name", ordering = "storename") def get_store(self, obj): return obj.storename or None
方案2:限制子查询返回单条结果(若业务必须从Product取)
如果业务逻辑要求从关联的Product中获取Store名称,需确保子查询仅返回一条结果,可使用切片配合Subquery:
from django.db.models import Subquery, CharField @admin.register(Link) class LinkAdmin(admin.ModelAdmin): list_display = ["product", "get_store"] list_filter = ["position__store"] def get_queryset(self, request): qs = super().get_queryset(request) # 子查询限制返回第一条结果 product_store_subquery = Subquery( Product.objects.filter(product_link_qs=OuterRef("id")) .values("store__name")[:1], output_field=CharField() ) return qs.annotate(storename=product_store_subquery) @admin.display(description = "store name", ordering = "storename") def get_store(self, obj): return obj.storename or None
也可使用Django 3.2+支持的聚合函数First:
from django.db.models import First @admin.register(Link) class LinkAdmin(admin.ModelAdmin): list_display = ["product", "get_store"] list_filter = ["position__store"] def get_queryset(self, request): qs = super().get_queryset(request) return qs.annotate( storename=First('products__store__name') ) @admin.display(description = "store name", ordering = "storename") def get_store(self, obj): return obj.storename or None
内容的提问来源于stack exchange,提问作者xtlc
相关产品推荐
相关产品推荐

