Django ORM distinct 查询,其中订单由带注释的字段完成,您需要 distinct('id')

Posted

技术标签:

【中文标题】Django ORM distinct 查询,其中订单由带注释的字段完成,您需要 distinct(\'id\')【英文标题】:Django ORM distinct query where order is done by annotated field and you need distinct('id')Django ORM distinct 查询,其中订单由带注释的字段完成,您需要 distinct('id') 【发布时间】:2017-11-09 12:32:30 【问题描述】:

商家有地方。地方有一个位置坐标。我需要通过 API 调用 (DRF) 返回一个 UNIQUE 商家列表,该列表按与他们关联的任何地方的距离进行过滤,并从最近的地方返回距离值。现在我得到重复(即,如果附近有多个商家的地方,商家会被多次退回)。

如果我尝试使用annotate(distance=...).distinct('pk'),我会收到错误消息

django.db.utils.ProgrammingError: 
    SELECT DISTINCT ON expressions must match initial ORDER BY expressions
    LINE 1: SELECT COUNT(*) FROM (SELECT DISTINCT ON ("merchants_merchan

如果我添加.order_by('pk'),然后我可以使用.distinct('pk'),但我无法按距离对返回的查询集进行排序。

这是我到目前为止所做的:

查询集

class MerchantQuerySet(models.QuerySet):    
    def nearby(self, latitude, longitude, proximity=None):
        """Get nearby Merchants.

        Custom queryset method for getting merchants associated with
        nearby places.

        Returns:
            A queryset of ``Merchant`` objects.

        """
        point = Point(latitude, longitude)

        # we query for location nearby for places first and then
        # annotate with distance to the same place
        return self.filter(
            places__location__distance_lte=(point, D(ft=proximity))).\
            annotate(distance=Distance(
                'places__location', point)).distinct()

型号

class Merchant(TimeStampedModel):

    name = models.CharField(max_length=255, verbose_name=_('Name'))
    description = models.TextField(
        blank=True,
        verbose_name=_('Description'),
    )
    logo = imagekitmodels.ProcessedImageField(
        max_length=512,
        upload_to=get_upload_path_for_model,
        processors=[ResizeToFill(300, 300)],
        format='PNG',
        options='quality': 100,
        editable=True,
        null=True,
        blank=True,
        verbose_name=_('Company logo'),
        help_text=_('Image will be resized to 300x300px.')
    )
    categories = models.ManyToManyField(
        'categories.Category',
        blank=True,
        related_name='merchants',
        verbose_name=_('Categories'),
    )
    address = models.TextField(blank=True, verbose_name=_('Address'))
    contact = models.CharField(
        max_length=32,
        blank=True,
        verbose_name=_('Contact phone'),
    )
    url = models.URLField(blank=True, verbose_name=_('Site URL'))
    social_urls = ArrayField(
        models.URLField(blank=True),
        blank=True,
        null=True,
        verbose_name=_('Social URLs'),
    )
    budget_tips = models.TextField(
        blank=True,
        verbose_name=_('Budget tips'),
        help_text=_('Recommendations to determine merchant budget.'),
    )

    objects = query.MerchantQuerySet.as_manager()

    class Meta:
        verbose_name = _('Merchant')
        verbose_name_plural = _('Merchants')


class Place(TimeStampedModel):

    id = models.CharField(
        max_length=32,
        primary_key=True,
        unique=True,
        verbose_name=_('ID'),
        help_text=_('Forsquare ID of the venue.'),
    )
    merchant = models.ForeignKey(
        'merchants.Merchant',
        related_name='places',
        verbose_name=_('Merchant'),
        help_text=_('Merchant, business owner.'),
    )
    categories = models.ManyToManyField(
        'categories.Category',
        blank=True,
        related_name='places',
        verbose_name=_('Categories'),
    )
    name = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Name')
    )
    address = models.TextField(blank=True, verbose_name=_('Address'))
    contact = models.CharField(
        max_length=32,
        blank=True,
        verbose_name=_('Contact phone')
    )
    location = PointField(blank=True, null=True, verbose_name=_('Location'))

    objects = PlaceQuerySet.as_manager()

    class Meta:
        verbose_name = _('Place')
        verbose_name_plural = _('Places')

DRF 视图

class MerchantViewSet(

            mixins.ListModelMixin,
            mixins.RetrieveModelMixin,
            favorites_api_mixins.UserFavoritesMixin,
            viewsets.GenericViewSet,
    ):

        queryset = models.Merchant.objects.all()
        serializer_class = serializers.MerchantSerializer
        filter_backends = (MerchantOrderingFilter,
                           filters.DjangoFilterBackend,
                           filters.SearchFilter,
                           utils_filters.LocationDistanceFilter)

        ordering_fields = ('name', 'created', 'distance')
        ordering = ('-created')
        search_fields = ('name',)

utils_filters.LocationDistanceFilter

class LocationDistanceFilter(BaseFilterBackend):
    """Location distance filter.

    Class for filtering objects by distance. Takes three GET params ``lat``,
    ``long`` and ``dist``:

        .../?dist=300&lat=55.56&long=98.01

    """
    dist_param = 'dist'

    def get_filter_point(self, request):
        """Get filter point.

        Get point for filtering by distance to this point.

        Args:
            request (Request): A ``Request`` object.

        Returns:
            A ``Point`` object or None.

        Raises:
            Parse error if point is invalid.

        """
        latitude = request.query_params.get('lat')
        longitude = request.query_params.get('long')

        if latitude and longitude:
            try:
                latitude = float(latitude)
                longitude = float(longitude)
            except ValueError:
                raise ParseError(
                    'Invalid geometry string supplied for '
                    'latitude or longitude'
                )

            return Point(latitude, longitude)

        else:
            return None

    def filter_queryset(self, request, queryset, view):
        """Filter queryset.

        Filter queryset by ``lat``, ``long`` and ``dist` params. Queryset
        should have method ``nearby``.

        Args:
            request (Request): A ``Request`` object.
            queryset (QuerySet): A ``QuerySet`` object.
            view (ViewSet): Current API view instance.

        Returns:
            A query set of objects filtered by latitude, longitude
            and distance.

        """
        distance = request.query_params.get(self.dist_param)
        point = self.get_filter_point(request)

        if not point:
            return queryset

        return queryset.nearby(
            latitude=point.x,
            longitude=point.y,
            proximity=distance,
        )

有什么想法吗?

【问题讨论】:

您是否仅尝试过.distinct(),即没有'pk' 参数。 嘿,@DmitrySemenov 我想知道我的回答是否有帮助 【参考方案1】:

如果您查看distinct() 文档(正如@Todor 建议的那样),您会发现:

order_by() 调用中使用的任何字段都包含在SQL SELECT 列中。与distinct() 结合使用时,这有时会导致意外结果。如果您按相关模型中的字段排序,这些字段将被添加到选定的列中,否则它们可能会使重复的行看起来不同。由于额外的列不会出现在返回的结果中(它们只是为了支持排序),因此有时看起来返回的结果不明确。

所以无论你做什么,都要牢记这一点。


让我们尝试解决这个问题:

annotate(distance=...) 创建一个名为distance 的列,您可以使用它对查询进行排序。您需要不同的商家,可以通过不同的pks 来保证:

...annotate(distance=...).order_by('pk', 'distance').distinct('pk')

这将首先按pk 对您的查询集进行排序,然后按distance 排序,最后它将仅返回具有不同pk 的商家。

【讨论】:

【参考方案2】:

我有类似的任务,对我有帮助 https://code.djangoproject.com/ticket/24218#comment:11

所以我有模型Listing + 模型ListingAddress(多对一Listing)+ 点。任务是获得由最接近的ListingAddress 排序的Listings 的不同列表。这是我的代码:

orm_request = ListingAddress.objects.filter(
    loc__distance_lte=(point, D(m=search_distance)),
    **make_category_filter(category_id),
).annotate(
    distance=Distance('loc', point)
)

closest_addresses = [rec['pk'] for rec in orm_request.order_by(
    'listing_id',
    'distance',
).distinct(
    'listing_id',
).values('pk')]

final_request = orm_request.filter(
    pk__in=subquery,
).order_by(
   'distance',
).select_related(
    'listing',
)

【讨论】:

以上是关于Django ORM distinct 查询,其中订单由带注释的字段完成,您需要 distinct('id')的主要内容,如果未能解决你的问题,请参考以下文章

django ORM model filter 条件过滤,及多表连接查询反向查询,某字段的distinct

Django ORM查询,不同的值并加入同一张表

Django orm

python测试开发django(12)--ORM查询表结果

SQL COUNT(DISTINCT <column>) 的 Django ORM 版本

Django Query distinct values 有效,但我无法使用查询结果