Django ORM仅在查询集的子集上不同

tvz2xvvm  于 2023-01-10  发布在  Go
关注(0)|答案(2)|浏览(159)

正在使用Django Rest Framework(DRF)、django-filter和PostgreSQL,并且我们的一个端点出现了问题。
假设如下:

# models.py
class Company(models.Model):
    name = models.CharField(max_length=50)

class Venue(models.Model):
    company = models.ForeignKey(to="Company", on_delete=models.CASCADE)
    name = models.CharField(max_length=50)

# create some data

company1 = Company.objects.create(name="Proper Ltd")
company2 = Company.objects.create(name="MyCompany Ltd")

Venue.objects.create(name="Venue #1", company=company1)
Venue.objects.create(name="Venue #2", company=company1)
Venue.objects.create(name="Property #1", company=company2)
Venue.objects.create(name="Property #2", company=company2)

# viewset
class CompanyViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = CompanyVenueSearchSerializer
    queryset = (
        Venue.objects.all()
        .select_related("company")
        .order_by("company__name")
    )
    permission_classes = (ReadOnly,)
    http_method_names = ["get"]
    filter_backends = (filters.DjangoFilterBackend,)
    filterset_class = CompanyVenueListFilter
    pagination_class = None

# filterset
class CompanyVenueListFilter(filters.FilterSet):
    text = filters.CharFilter(method="name_search")

    def name_search(self, qs, name, value):
        return qs.filter(
            Q(name__icontains=value)
            | Q(company__name__icontains=value)
        )

    class Meta:
        model = Venue
        fields = [
            "name",
            "company__name",
        ]

# serializer
class CompanyVenueSearchSerializer(serializers.ModelSerializer):
    company_id = serializers.IntegerField(source="company.pk")
    company_name = serializers.CharField(source="company.name")
    venue_id = serializers.IntegerField(source="pk")
    venue_name = serializers.CharField(source="name")

    class Meta:
        model = Venue
        fields = (
            "company_id",
            "company_name",
            "venue_id",
            "venue_name",
        )

现在我们希望允许用户通过在请求中发送查询来过滤结果,例如curl -X GET https://example.com/api/company/?text=pr
序列化程序结果将类似于:

[
   {
      "company_id":1,
      "company_name":"Proper Ltd",
      "venue_id":1,
      "venue_name":"Venue #1"
   },
   {  // update ORM to exclude this dict
      "company_id":1,
      "company_name":"Proper Ltd",
      "venue_id":2,
      "venue_name":"Venue #1"
   },
   {
      "company_id":2,
      "company_name":"MyCompany Ltd",
      "venue_id":3,
      "venue_name":"Property #1"
   },
   {
      "company_id":2,
      "company_name":"MyCompany Ltd",
      "venue_id":4,
      "venue_name":"Property #1"
   }
]

预期结果:

希望重写ORM查询,以便如果筛选器("* pr *")匹配venue__name,则返回所有地点。但如果筛选器匹配company__name,则仅返回 * 一次 *,因此在上面的示例中,列表中的第二个dict将被排除/删除。
这可能吗?

yqhsw0fo

yqhsw0fo1#

您可以做的是过滤与name过滤匹配的Company,并使用第一个相关的Venue对其进行注解,然后将其结果与第二个要求组合,以使用name=value返回venue

from django.db.models import OuterRef, Q, Subquery

value = "pr"
first_venue = Venue.objects.filter(company__in=OuterRef("id")).order_by("id")
company_qs = Company.objects.filter(name__icontains=value).annotate(
    first_venue_id=Subquery(first_venue.values("id")[:1])
)
venue_qs = Venue.objects.filter(
    Q(name__icontains=value)
    | Q(id__in=company_qs.values_list("first_venue_id", flat=True))
)

访问venue_qs的值时执行的查询如下所示

SELECT
    "venues_venue"."id",
    "venues_venue"."company_id",
    "venues_venue"."name"
FROM
    "venues_venue"
WHERE
    (
        UPPER("venues_venue"."name"::TEXT) LIKE UPPER(% pr %)
        OR "venues_venue"."id" IN (
            SELECT
                (
                    SELECT
                        U0."id"
                    FROM
                        "venues_venue" U0
                    WHERE
                        U0."company_id" IN (V0."id")
                    ORDER BY
                        U0."id" ASC
                    LIMIT
                        1
                ) AS "first_venue_id"
            FROM
                "venues_company" V0
            WHERE
                UPPER(V0."name"::TEXT) LIKE UPPER(% pr %)
        )
    )

这是过滤器的外观

class CompanyVenueListFilter(filters.FilterSet):
    text = filters.CharFilter(method="name_search")

    def name_search(self, qs, name, value):
        first_venue = Venue.objects.filter(company__in=OuterRef("id")).order_by("id")
        company_qs = Company.objects.filter(name__icontains=value).annotate(
            first_venue_id=Subquery(first_venue.values("id")[:1])
        )
        return qs.filter(
            Q(name__icontains=value)
            | Q(id__in=company_qs.values_list("first_venue_id", flat=True))
        )

    class Meta:
        model = Venue
        fields = [
            "name",
            "company__name",
        ]

更新Django 3.2.16

上面的查询似乎不适用于此版本,因为它生成的查询在V0."id"周围的WHERE子句中没有括号,查询块如下所示

WHERE
    U0."company_id" IN V0."id"

这会让PostgreSQL报错

ERROR: syntax error at or near "V0"
LINE 17: U0."company_id" IN V0."id"

对于Django==3.2.16CompanyVenueListFilter中的过滤方法可能如下所示:

def name_search(self, qs, name, value):
        company_qs = Company.objects.filter(name__icontains=value)
        venues_qs = (
            Venue.objects.filter(company__in=company_qs)
            .order_by("company_id", "id")
            .distinct("company_id")
        )
        return qs.filter(Q(name__icontains=value) | Q(id__in=venues_qs.values_list("id")))

答案是基于其他stackoverflow答案和django文档

s71maibg

s71maibg2#

我们有一个临时的解决方案,我们对此有点担心,但它似乎起到了作用。我们不会把这个答案标记为被接受,因为我们仍然希望有人能有一个更像Python/ Django 的解决方案。

# viewset
class CompanyViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = CompanyVenueSearchSerializer
    queryset = (
        Venue.objects.all()
        .select_related("company")
        .order_by("company__name")
    )
    permission_classes = (ReadOnly,)
    http_method_names = ["get"]
    filter_backends = (filters.DjangoFilterBackend,)
    filterset_class = CompanyVenueListFilter
    pagination_class = None

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(queryset, many=True)
        text = request.GET.get("text").lower()
        first_idx = 0
        to_remove = []

        for data in serializer.data:
            if text in data.get("name").lower() and text not in data.get("venue_name").lower():
                if data.get("id") != first_idx:
                    """We don't want to remove the first hit of a company whose name matches"""
                    first_idx = data.get("id")
                    continue
                to_remove.append((data.get("id"), data.get("venue_id")))

        return Response(
            [
                data
                for data in serializer.data
                if (data.get("id"), data.get("venue_id")) not in to_remove
            ],
            status=status.HTTP_200_OK,
        )

相关问题