过滤/排除

时间:2016-08-04 21:58:42

标签: django django-models greatest-n-per-group

如果我有:

class Info(Model):
    ...

class Ad(Model):
    listed_date = DatetimeField()
    info = ForeignKey('Info', related_name='ads', null=True)
    ....

我想根据Info中的字段查询Ad,但只查看最新广告。我知道我能做到:

Ad.objects.latest('listed_date')

但是,由于我将通过将多个过滤器/排除链接在一起来构建查询,我想要类似的东西:

query = query.filter(
    Q(**{
        'ads__latest__'+attr_name: query_value
    })
)

或者甚至可能有一个字段' latest_ad'总是基于某个领域指向最新的。目标是能够在构建的过滤/排除方法中仅查询相关字段中的最新内容。

我该怎么做?

修改

一点背景...... 我有2个模型(LegalAd,TrusteeInfo)存储有关同一拍卖项目的数据,某些字段需要进行大量处理以提取必要的值(因此我决定将信息存储在单独的模型中)存储数据不同的加工阶段。然后,我尝试将两个模型合并为一个(AuctionItem),并广泛使用属性来优先处理来自LegalAd的TrusteeInfo中的数据,以获得它们共享的类似字段。问题是我想查询那些使用属性禁止的字段。所以我创建了一个管理器并覆盖了过滤器并排除了保存优先级逻辑的方法。以下是代码:

class LegalAd(models.Model):
    listed_date = models.DateField(null=True)  # field I would like to use for latest query
    auction = models.ForeignKey('auction_table.Auction', related_name='legal_ads', null=True)
    ...


class TrusteeInfo(models.Model):
    auction = models.OneToOneField('auction_table.Auction', null=True)
    ...


class AuctionManager(models.Manager):
    def do_query_action(self, action, kwargs):
        trusteeinfo = apps.get_model('scrapers', 'TrusteeInfo')
        trustee_fields = [field.name for field in trusteeinfo._meta.get_fields()]
        legalad = apps.get_model('scrapers', 'LegalAd')
        legalad_fields = [field.name for field in legalad._meta.get_fields()]
        related_fields = trustee_fields + legalad_fields
        auction_native_fields = [
            'legal_ads',
            'trusteeinfo',
            'properties',
            'id',
            'pk',
            'created_date',
            'updated_date'
        ]
        query = super(AuctionManager, self)
        for attr, value in kwargs.items():
            attr_base = attr.split('__')[0]  # get the base attr name
            if attr_base in auction_native_fields:
                query = getattr(query, action)(**{attr: value})
            elif attr_base in related_fields:
                qs = []
                if attr_base in trustee_fields:
                    trustee_attr_name = 'trusteeinfo__' + attr
                    qs.append(Q(**{trustee_attr_name: value}))
                if attr_base in legalad_fields:
                    legalad_attr_name = 'legalads__' + attr
                    qs.append(Q(**{legalad_attr_name: value}))
                query = getattr(query, action)(reduce(or_, qs))
            else:
                raise AttributeError("type object `Auction` has no attribute '{attr}'".format(attr=attr))
        return query.distinct()

    def filter(self, **kwargs):
        return self.do_query_action('filter', kwargs)

    def exclude(self, **kwargs):
        return self.do_query_action('exclude', kwargs)


class Auction(models.Model):
    objects = AuctionManager()
    created_date = models.DateTimeField(auto_now_add=True)
    updated_date = models.DateTimeField(auto_now=True)

    @property
    def latest_ad(self):
        return self.legal_ads.exists() and self.legal_ads.latest('listed_date')

    @property
    def sale_datetime(self):
        if self.trusteeinfo and self.trusteeinfo.sale_datetime:
            return self.trusteeinfo.sale_datetime
        else:
            return self.latest_ad and self.latest_ad.sale_datetime

    @property
    def county(self):
        if self.trusteeinfo and self.trusteeinfo.county:
            return self.trusteeinfo.county
        else:
            return self.latest_ad and self.latest_ad.county

    @property
    def sale_location(self):
        return self.latest_ad and self.latest_ad.sale_address

    @property
    def property_addresses(self):
        if self.trusteeinfo and self.trusteeinfo.parsed_addresses.exists():
            return self.trusteeinfo.parsed_addresses
        else:
            return self.latest_ad and self.latest_ad.parsed_addresses

    @property
    def raw_addresses(self):
        if self.trusteeinfo and self.trusteeinfo.addresses:
            return self.trusteeinfo.addresses
        else:
            return self.latest_ad and self.latest_ad.addresses.get('addresses', None)

    @property
    def parcel_numbers(self):
        return self.latest_ad and self.latest_ad.parcel_numbers

    @property
    def trustee(self):
        if self.trusteeinfo:
            return self.trusteeinfo.trustee
        else:
            return self.latest_ad and self.latest_ad.trustee.get('trustee', None)

    @property
    def opening_bid(self):
        if self.trusteeinfo and self.trusteeinfo.opening_bid:
            return self.trusteeinfo.opening_bid
        else:
            return self.latest_ad and self.latest_ad.dollar_amounts.get('bid_owed', [[None]])[0][0]

    @property
    def deposit_amount(self):
        if self.trusteeinfo and self.trusteeinfo.deposit_amount:
            return self.trusteeinfo.deposit_amount
        else:
            return self.latest_ad and self.latest_ad.dollar_amounts.get('deposit', [[None]])[0][0]

    @property
    def sale_status(self):
        return self.trusteeinfo and self.trusteeinfo.sale_status

    @property
    def trustors(self):
        if self.trusteeinfo and self.trusteeinfo.parsed_names.exists():
            return self.trusteeinfo.parsed_names
        else:
            return self.latest_ad and self.latest_ad.parsed_names

事实上,广告通常一次列出2个,因此很有可能会有2个广告出现在最新日期,这意味着我必须运行类似{{1}的广告。也就是它的方法。我可以查找某些kwargs并为此运行一个特殊的查询但是如何将其合并到链式查询中的其余kwargs中?理想情况下,如果我可以保持一个到多个first(),但也可以执行以下操作:

legal_ads

或:

query.filter(latest_ad__<queryfield>=value)

那太棒了。

2 个答案:

答案 0 :(得分:1)

你所拥有的是所谓的greatest-n-per-group问题,它很难与ORM打交道甚至不可能。

可以找到解决问题的一种方法here

在你的情况下,它可能是这样的:

Info.objects.filter(
    ad__listed_date__in=Info.objects.annotate(
            last_date=Max('ad__listed_date')
        ).values_list('last_date', flat=True)
    #now you can add more
    #ad__<somefiled> statements
    #but you need to make it in a single `.filter` call
    #otherwise the ORM will do separate joins per `.filter` call
)

我个人不喜欢这样。对我来说,它看起来像是一个黑客,它效率不高,如果某个组中的倒数第二个ad与最后一个listed_date ad相同,则很容易返回错误结果在另一组。

变通方法

如果你给我们一些关于为什么你需要过滤每个信息的latest_ad的背景知识,也许我们可以找到另一种方法来获得相同/相似的结果。

但是,我更喜欢的一种解决方法是过滤一些date_range。例如,请勿在{{1​​}}或latest_ad.filter中的latest_ads上搜索last_daytwo,根据您的需要。它非常简单有效的(易于优化)查询。

week

你还提到了一个很好的解决方法,如果你能够轻松掌握最新的Info.objects.filter( ad__listed_date__gte=(today-timedelta(days=1)) #now again you can keep adding more `ad__<somefiled>` statements #but make sure to enclose them in a single `.filter` call. ) 字段,那么我想你会很高兴。

如果您采用这种方法,请务必设置Info.latest_ad,因为默认行为(级联删除)可能会给您带来问题。

on_delete=models.SET_NULL

答案 1 :(得分:0)

您可以.latest()

使用.filter()
Ad.objects.filter(your_filter=your_value).latest('listed_date')

或使用oder_by

Ad.objects.filter(your_filter=your_value).order_by('-listed_date')[0]