如何在 ModelAdmin.formfield_for_manytomany() 中使用 Django QuerySet.union()?

Posted

技术标签:

【中文标题】如何在 ModelAdmin.formfield_for_manytomany() 中使用 Django QuerySet.union()?【英文标题】:How to use Django QuerySet.union() in ModelAdmin.formfield_for_manytomany()? 【发布时间】:2020-10-23 23:23:39 【问题描述】:

不知道我在这里做错了什么:

我尝试在 Django 2.2.10 中使用 QuerySet.union() 在 ModelAdmin.formfield_for_manytomany() 中组合两个查询集(对于同一模型)。但是,保存表单时,会选择整个查询集,而不考虑实际所做的选择。

请考虑下面基于标准 Django Article/Publication example 的最小示例。

from django.db import models
from django.contrib import admin


class Publication(models.Model):
    pass


class Article(models.Model):
    publications = models.ManyToManyField(to=Publication, blank=True)


class ArticleAdmin(admin.ModelAdmin):
    def formfield_for_manytomany(self, db_field, request, **kwargs):
        if db_field.name == 'publications':
            # the following query makes no sense, but it shows an attempt to
            # combine two separate QuerySets using QuerySet.union()
            kwargs['queryset'] = Publication.objects.all().union(
                Publication.objects.all())
        return super().formfield_for_manytomany(db_field, request, **kwargs)


admin.site.register(Publication)
admin.site.register(Article, ArticleAdmin)

publications 字段的初始queryset 使用formfield_for_manytomany 过滤,如docs 中所述。

请注意:这个例子中的实际查询没有意义,它只是返回所有内容,但这并不重要:关键是QuerySet.union() 弄乱了选择。如果您删除union(),它可以正常工作。

当我在管理员中添加一个新的Article 时会发生以下情况,没有选择任何出版物

在“保存”之前(未选择)

在“保存”之后(所有内容都被选中)

无论我做什么,每次保存表单时都会自动选择所有选项。

鉴于QuerySet.union()返回的查询集上的restrictions,我是否以错误的方式使用QuerySet.union(),或者这是预期的行为?

【问题讨论】:

【参考方案1】:

正如@tom-carrick 指出的那样,QuerySet 返回的 QuerySet 似乎无法过滤。我想documentation 的以下摘录暗示了这一点:

此外,仅允许在结果QuerySet

如果您使用的是 Django 3.0,则在 QuerySet.union() 的结果上调用 filter() 将引发异常并显示非常清晰的消息:

django.db.utils.NotSupportedError: Calling QuerySet.filter() after union() is not supported.

但是,如果您使用的是 Django 2.2,则不会引发异常:在这种情况下,它只会返回完整的查询集,而不管过滤器参数如何。这里有一个小测试来说明这一点(在 Django 2.2 中):

# using Django 2.2.10
class PublicationTests(TestCase):
    def test_union_filter(self):
        for i in range(2):
            Publication.objects.create()
        queryset_union = Publication.objects.filter(id=1).union(
            Publication.objects.filter(id=2))
        self.assertEqual(2, len(queryset_union))
        for obj in queryset_union.all():
            self.assertIn(obj, queryset_union.filter(id=1))
            self.assertIn(obj, queryset_union.filter())
            self.assertIn(obj, queryset_union.filter(id=0))

所以,当我们使用QuerySet.union() 限制ModelAdmin 中的查询集时,一定会发生这种情况:选择小部件按预期工作,但是当表单被验证时,filter() 在@ 的输出上被调用987654342@(请参阅source 了解ModelMultipleChoiceField),并且无论实际的子选择如何,它始终返回完整的查询集。

根据实际用例,可能有使用union() 的方法,如tom-carrick's answer 中所述。

但是,在这种情况下,至少有一种方法可以解决QuerySet.union() 施加的限制,那就是从查询集联合创建一个新的查询集:

这是原始示例中ArticleAdmin 的修改版本:

class ArticleAdmin(admin.ModelAdmin):
    def formfield_for_manytomany(self, db_field, request, **kwargs):
        if db_field.name == 'publications':
            queryset_union = Publication.objects.all().union(
                Publication.objects.all())
            kwargs['queryset'] = Publication.objects.filter(id__in=queryset_union)
        return super().formfield_for_manytomany(db_field, request, **kwargs)

同样,这个人为示例中的实际查询没有意义,但这在这里并不重要。

就数据库访问而言,这可能不是最有效的解决方案。

【讨论】:

【参考方案2】:

问题似乎确实是.union(),但我不知道为什么。这似乎是一个错误,或者至少是时髦的行为。

由于您没有指定实际用例,因此很难知道,但对于您提供的示例,您可以改用 OR 运算符,这将适用:

class ArticleAdmin(admin.ModelAdmin):
    def formfield_for_manytomany(self, db_field, request, **kwargs):
        if db_field.name == 'publications':
            # the following query makes no sense, but it shows an attempt to
            # combine two separate QuerySets using QuerySet.union()
            kwargs['queryset'] = (
                Publication.objects.filter(id__lt=3)
                | Publication.objects.filter(id__gt=2)
            )
        return super().formfield_for_manytomany(db_field, request, **kwargs)

【讨论】:

感谢您的努力,但是,正如问题中明确提到的,(请参阅“请注意”部分,示例代码中的注释),我知道查询没有意义,因为它包含所有内容。这不是问题所在:关键是不仅显示了所有内容,而且还选择了所有内容 在这种情况下,我建议您提出更能代表您的问题的 MRE。当您以这种方式更改查询集时,如果您在添加对象时未选择任何内容,您也会更改默认值 - 您不仅仅是更改显示的内容。管理员似乎没有很好地显示这一点。我不确定这是一个错误还是故意的,但至少用户体验不是很好。 我已经用另一个去编辑了。如果 OR 查询也能正常工作,您似乎可以这样做。 Django 中似乎有一个错误。 实际上我的目标是了解为什么union 不能用于过滤管理查询集。 (也向其他人指出这个事实。)实际上,您最初的答案在最后一段中确实包含了一个要点:如果在union() 之后无法过滤QuerySet,那么观察到的行为更有意义。也许这在docs 中为union() 暗示了这一点,但如果知道管理源代码中的哪里 到底出了什么问题,还是很高兴的。 我不确定这是否特别相关,因为当它发生时会引发异常/黄页。令我惊讶的是,为什么如果你什么都不选,一切都会被添加。

以上是关于如何在 ModelAdmin.formfield_for_manytomany() 中使用 Django QuerySet.union()?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 JTextField 中对齐文本?

JavaFX如何在底部获取菜单栏

如何在 Xcode 表中执行一个常量(无滚动)部分?

__future__ 进口如何在幕后工作

如何在标准输出中禁用 Spring Boot 徽标?

如何在模板中显示 Django '__all__' 表单错误?