【问题标题】:annotation in admin list with many to many relation管理列表中的注释具有多对多关系
【发布时间】:2022-09-23 17:37:20
【问题描述】:

我在广告、产品和商店之间有一个链接,我想在管理员中按商店对它们进行排序:

class Ad(models.Model):
    products = models.ManyToManyField(Product, blank = True)
    device = models.ForeignKey(Display, on_delete = models.CASCADE)

class Product(models.Model):
    name = models.CharField(\"name\", max_length = 128)
    store = models.ForeignKey(Store, on_delete = models.CASCADE)

class Store(models.Model):
    name = models.CharField(\"name\", max_length = 128)

所以每个广告可以有 0、1、2、3 ... 产品链接到它。现在我想在广告管理列表中使字段“存储”可排序,因此我试图覆盖AdAdminget_queryset 方法,但被卡住了:

class AdAdmin(admin.ModelAdmin):
    list_display = [\"get_store\", ... ]

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        qs = qs.annotate(storename = ####)
        return qs

    @admin.display(ordering = \"storename\")
    def get_store(self, obj):
        try:
            return obj.products.all().first().store.name
        except AttributeError:
            try:
                return obj.device.store.name
            except AttributeError:
                return \"NEW\"

所以我想用storename 注释我的查询集,并且我希望能够在Ad 列表管理页面上按字母顺序对store.name 进行排序。我已经知道如何注释空商店:

qs = qs.annotate(storename = Case(When(products_pk__isnull = True, then = Value(\"NEW\"), default = Value(\"old\")))

但这只是让我到目前为止......我将如何使用我给定的逻辑将值store.name 动态分配给default

每个广告都有一个条件:它只能链接到同一家商店的(许多)产品。

    标签: django-admin django-orm django-annotate django-aggregation


    【解决方案1】:

    正如您所提到的,每个广告只能包含来自一家商店的产品。然后,您可以从任何产品或所有产品的不同结果中获取商店名称。

    要从广告模型访问产品,您可以添加 related_query_name

    # models.py
    class Ad(models.Model):
        products = models.ManyToManyField(
            Product,
            related_name="product_ads",
            related_query_name="product_ads_qs",
            blank=True,
        )
        ...
    
    # admin.py
    from django.db.models import OuterRef
    
    @admin.register(Ad)
    class AdAdmin(admin.ModelAdmin):
        list_display = ["get_store", ...]
    
        def get_queryset(self, request):
            qs = super().get_queryset(request)
            qs = qs.annotate(
                # Get all store names for each Ad object 
                storename=Product.objects.filter(product_ads_qs=OuterRef("id"))
                # Distinct the result. (Will fail it multiple store names returns)
                .distinct("store__name")
                # Get the store name
                .values("store__name")
            )
            return qs
    
        @admin.display(ordering="storename")
        def get_store(self, obj):
            # You can change the default value here. No need for Case operation
            return obj.storename or "NEW"
    

    它应该工作。我不确定性能,也许可以编写更好的ORM。

    更新

    Distinct 可以替换为[:1]。昨天忘记了,不好意思。 商店是 Product 的 FK,但我们需要确定我们从查询中获得了哪个 Product 实例。这就是使用 OuterRef 的原因。这意味着通过 Ad.id 获取给定 Ad 实例的 Product 实例。

    有几种方法可以访问商店名称。使用相关名称可能更快。我不是那么有经验,但会尝试解释:

    没有related_name,这个ORM可以写成 storename=Ad.objects.filter(id=OuterRef("id")).values("products__store__name")[:1]

    此 ORM 创建如下查询。它从 Ad 表开始,3 加入对 store 表的访问。

    SELECT
        "app_ad"."id", 
        (
            SELECT U3."name" 
            FROM "app_ad" U0 
            -- there is a reference table for m2m relation created by Django
            LEFT OUTER JOIN "app_ad_products" U1 ON (U0."id" = U1."ad_id") 
            LEFT OUTER JOIN "app_product" U2 ON (U1."product_id" = U2."id") 
            LEFT OUTER JOIN "app_store" U3 ON (U2."store_id" = U3."id") 
            -- OuterRef works here and searches the Ad.id per Ad record
            WHERE U0."id" = ("app_ad"."id") 
            -- [:1] limits the query
            LIMIT 1
        ) AS "storename" 
    FROM "app_ad"
    

    如果我们使用相关名称 storename=Product.objects.filter(product_ads_qs=OuterRef("id")).values("store__name")[:1]

    查询输出有 2 个连接来访问存储表

    SELECT "app_ad"."id",
        (
            SELECT U3."name" FROM "app_product" U0 
            INNER JOIN "app_ad_products" U1 ON (U0."id" = U1."product_id") 
            INNER JOIN "app_store" U3 ON (U0."store_id" = U3."id") 
            WHERE U1."ad_id" = ("app_ad"."id") LIMIT 1
        ) AS "storename"
    FROM "app_ad"
    

    奖金: 如果将相关名称添加到产品模型关系

    class Product(models.Model):
        name = models.CharField("name", max_length=128)
        store = models.ForeignKey(
            Store,
            related_name="store_product",
            related_query_name="store_product",
            on_delete=models.CASCADE,
        )
    

    ORM 也可以写成如下,查询与前一个几乎相同。 storename=Store.objects.filter(store_product__product_ads_qs__id=OuterRef("id")).values("name")[:1]

    您可以检查相关的命名约定。

    【讨论】:

    • 您的回答有效,因此我接受了。但我有两个问题。第一:为什么我们需要在Ad 类的ManyToMany 字段products 中添加related_name?第二:SQLite 不支持distinct() - 但我们为什么需要它(StoreForeignKey 对应于Product,所以只能返回一个值......)?
    • 我已经更新了答案@xtlc
    猜你喜欢
    • 2014-06-21
    • 2010-10-14
    • 1970-01-01
    • 2014-05-11
    • 2012-06-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多