【问题标题】:Django: retrieve distinct QuerySetDjango:检索不同的查询集
【发布时间】:2012-01-04 05:50:57
【问题描述】:

我的应用中有以下模型。 Addition 模型用于管理 Book 模型和 Collection 模型之间的多对多关系,因为我需要在中间模型上包含额外的字段。

class Book(models.Model):
    name = models.CharField(max_length=200)
    picture = models.ImageField(upload_to='img', max_length=1000)
    price = models.DecimalField(max_digits=8, decimal_places=2)

class Collection(models.Model):
    user = models.ForeignKey(User)
    name = models.CharField(max_length=100)
    books = models.ManyToManyField(Book, through='Addition')
    subscribers = models.ManyToManyField(User, related_name='collection_subscriptions', blank=True, null=True)

class Addition(models.Model):
    user = models.ForeignKey(User)
    book = models.ForeignKey(Book)
    collection = models.ForeignKey(Collection)
    created = models.DateTimeField(auto_now=False, auto_now_add=True)
    updated = models.DateTimeField(auto_now=True, auto_now_add=True)

在我的应用中,用户可以将书籍添加到他们创建的收藏中(例如小说、历史等)。然后其他用户可以关注他们喜欢的那些收藏。

当用户登录网站时,我想显示最近添加到他们所关注的收藏中的所有书籍。对于每本书,我还想显示添加它的人的姓名,以及它所在的集合的名称。

我可以得到所有的补充如下......

additions = Addition.objects.filter(collection__subscribers=user).select_related()

...但这会导致重复的书籍被检索并显示给用户,通常并排显示。

如果有办法检索用户正在关注的收藏中的不同书籍列表?

我正在使用 Django 1.3 + MySQL。

谢谢。

更新

我应该补充一点,一般来说,出于几个原因,我不会寻找任何“遍历结果并以这种方式去重复”的解决方案。

可能会有数万甚至数十万的添加(我也在列出用户添加的所有新添加的页面上显示此信息),响应时间非常重要。

这种解决方案在限制初始结果集时可能会变得更实用,但它会产生分页问题,​​这也是必需的。即,您如何对整个结果集进行分页,同时仅对该结果集的一小部分进行重复数据删除。我愿意接受任何可以解决这个问题的想法。

更新

我还应该提到,如果同一本书被多个用户添加,我实际上并没有偏好使用哪个添加,无论是原始添加还是最近添加都可以。

【问题讨论】:

  • 您是否尝试在查询集末尾添加.distinct()
  • @gorus 只会给我一组不同的 Addition 对象。我需要的是一组具有不同书籍的 Addition 对象,类似于Addition.objects.all().distinct('book')
  • Book.objects.filter(addition__collection__subscribers=user).distinct()
  • @armonge 但是如果没有来自模板的额外查询,我无法显示每个图书实例的相关添加和收藏信息,导致每页有数百个查询。
  • 我想你也应该澄清一下:如果同一本书被多个用户添加,你更喜欢哪个添加......

标签: mysql django django-queryset


【解决方案1】:

以下怎么样 - 它不是一个纯 SQL 解决方案,它会花费您一个额外的数据库查询和一些循环时间,但它应该仍然可以执行,并且它可以让您更好地控制哪些添加优先于其他人:

def filter_additions(additions):
    # Use a ValuesQuerySet for performance
    additions_values = additions.values()

    # The following code just eliminates duplicates. You could do 
    # something much more powerful/interesting here if you like,
    # e.g. give preference to additions by a user`s friends 

    book_pk_registry = {}
    excluded_addition_pks = []

    for addition in additions_values:
        addition_pk = addition['id']
        book_pk = addition['book_id']
        if book_pk not in book_pk_registry:
            book_pk_registry[book_pk] = True
        else:
            excluded_addition_pks.append(addition_pk)

    additions = additions.exclude(pk__in=excluded_addition_pks)


additions = Addition.objects.filter(collection__subscribers=user)
additions = filter_additions(additions)

如果涉及的书籍可能超过一千本,您可能希望对初始添加查询设置一个限制。在 exclude 中传递大量 id 列表并不是一个好主意。使用 'values()' 非常重要,因为 Python 可以比查询集更快地循环遍历一个基本的 dicts 列表,并且它使用的内存要少得多。

【讨论】:

  • 不过目前“性能不会有问题”,但是如果你使用这样的解决方案,性能很快就会成为一个非常大的问题,即使你有少量的数据。所以避免这种情况。
【解决方案2】:

假设不会显示大量添加内容,这很容易达到目的:

# duplicated..
additions = Addition.objects.filter(collection__subscribers=user, created__gt=DATE_LAST_LOGIN).select_related()

# remove duplication
added_books = {}
for addition in additions:
    added_books[addition.book] = True
added_books = added_books.keys()

根据您对问题的描述,性能不会有问题。

【讨论】:

  • 不过目前“性能不会有问题”,但是如果你使用这样的解决方案,性能很快就会成为一个非常大的问题,即使你有少量的数据。所以避免这种情况。
【解决方案3】:
additions = Addition.objects.filter(collection__subscribers=user).values('book').annotate(user=Min('user'), collection=Min('collection')).order_by()

此查询将为您提供独特书籍的列表及其用户和收藏。书籍、收藏、用户将是 pk 的,而不是对象。但我希望您将它们存储在缓存中,这样就不会成为问题。

但对于您的工作量,我会考虑非规范化。我的查询非常很繁重,如果你要经常添加,缓存它的结果并不容易。我的第一种方法是将latest_additions 字段添加到Collection 模型并使用信号进行更新(不添加重复项)。此字段的格式由您决定。

【讨论】:

    【解决方案4】:

    有时可以使用 SQL,尤其是当仅 ORM 的解决方案性能不佳时。在 SQL 中很容易得到不重复的 Addition 行 ID,然后您可以切换回 ORM 来选择数据。这是两个查询,但会胜过我迄今为止看到的任何单一查询解决方案。

    from django.db import connection
    from operator import itemgetter
    cursor = connection.cursor()
    
    # Select non-duplicate book additions, preferring for most recently updated
    query = '''SELECT id, MAX(updated) FROM %s
        GROUP BY book_id''' % Addition._meta.db_table
    cursor.execute(query)
    
    # Flatten the results to an id list
    addition_ids = map(itemgetter(0), cursor.fetchall())
    
    additions = Addition.objects.filter(
        collection__subscribers=user, id__in=addition_ids).select_related()
    

    【讨论】:

    • addition_ids 变大时这不会失败吗?我相信如果给定一个 IN 运算符的值超过 ~60k,大多数数据库都会呕吐,而且你会比这更早地获得性能下降。
    • 当 IN 操作在索引列上时不是,在这种情况下是正确的。见stackoverflow.com/questions/5367488/…
    • 即使数据库可以处理它,他说的是数十万条条目。这意味着使用这种方法的查询最终可能会达到几个 兆字节。这对于一次性交易来说是可以的,但对于他网站上的每一次页面浏览来说都不是!
    • 如果这是一个问题,他可以简单地将 ORDER BY MAX(updated) LIMIT XXX 添加到原始 SQL 查询中。如果您只获取/显示 10-100 个项目,则没有理由加载 100,000 个 ID。他还可以索引更新的列以加快查询速度。
    • 当然——唯一的缺点是他会失去使用 queryset 对象很好地进行分页的能力。不过,这是一个足够可行的解决方案。
    猜你喜欢
    • 2017-08-23
    • 2022-01-23
    • 2012-10-04
    • 2019-04-12
    • 2021-05-10
    • 1970-01-01
    • 1970-01-01
    • 2012-04-14
    • 2012-11-26
    相关资源
    最近更新 更多