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

时间:2017-06-08 00:26:45

标签: python django python-3.x django-models django-orm

商人有地方。地方有一个位置坐标。我需要通过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,
        )

有什么想法吗?

2 个答案:

答案 0 :(得分:1)

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

  

order_by()来电中使用的所有字段都包含在SQL SELECT列中。当与distinct()结合使用时,这有时会导致意外结果。如果您按相关模型中的字段进行排序,则这些字段将添加到选定的列中,否则可能会使重复的行显示为不同。由于额外的列没有出现在返回的结果中(它们仅用于支持排序),因此有时看起来会返回非独特的结果。

所以无论你做什么,都要考虑到这一点。

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

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

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

这将首先按pk然后按distance订购您的查询集,最后它将仅返回具有不同pk的商家。

答案 1 :(得分:0)

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

所以我有模型Listing +模型ListingAddress(很多Listing)+点。任务是获取Listings最接近的ListingAddress顺序的唯一列表。这是我的代码:

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',
)