如何使用对象列表过滤多个字段

How to filter multiple fields with list of objects

我想构建一个类似 Quora or Medium 的网络应用程序,用户可以在其中关注用户或某些主题。

例如:用户 A 正在关注(用户 B、用户 C、标签-健康、标签-财务)。

这些是模型:

class Relationship(models.Model):
    user = AutoOneToOneField('auth.user')
    follows_user = models.ManyToManyField('Relationship', related_name='followed_by')
    follows_tag = models.ManyToManyField(Tag)

class Activity(models.Model):
    actor_type = models.ForeignKey(ContentType, related_name='actor_type_activities')
    actor_id = models.PositiveIntegerField()
    actor = GenericForeignKey('actor_type', 'actor_id')
    verb = models.CharField(max_length=10)
    target_type = models.ForeignKey(ContentType, related_name='target_type_activities')
    target_id = models.PositiveIntegerField()
    target = GenericForeignKey('target_type', 'target_id')
    tags = models.ManyToManyField(Tag)

现在,这将给出以下列表:

following_user = userA.relationship.follows_user.all()
following_user
[<Relationship: userB>, <Relationship: userC>]
following_tag = userA.relationship.follows_tag.all()
following_tag
[<Tag: tag-job>, <Tag: tag-finance>]

为了过滤我试过这样:

Activity.objects.filter(Q(actor__in=following_user) | Q(tags__in=following_tag))

但是因为 actor 是一个 GenericForeignKey,所以我得到一个错误:

FieldError: Field 'actor' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.

如何使用用户列表和用户关注的标签列表过滤唯一的活动?具体来说,我将如何使用对象列表过滤 GenericForeignKey 以获取以下用户的活动。

您可以分别查询用户和标签,然后将它们结合起来以获得您要查找的内容。请像下面这样做,如果可行请告诉我..

usrs = Activity.objects.filter(actor__in=following_user)    
tags = Activity.objects.filter(tags__in=following_tag)

result = usrs | tags

documentation 中所述:

Due to the way GenericForeignKey is implemented, you cannot use such fields directly with filters (filter() and exclude(), for example) via the database API. Because a GenericForeignKey isn’t a normal field object, these examples will not work:

您可以按照错误消息告诉您的内容进行操作,我认为您必须添加一个 GenericRelation 关系才能做到这一点。我没有这方面的经验,我必须研究它但是...

我个人认为此解决方案对于您要实现的目标而言过于复杂。如果只有 user 模型可以跟随标签或作者,为什么不在其上包含 ManyToManyField。它会是这样的:

class Person(models.Model):
    user = models.ForeignKey(User)
    follow_tag = models.ManyToManyField('Tag')
    follow_author = models.ManyToManyField('Author')

您可以像这样查询每个人所有关注的标签活动:

Activity.objects.filter(tags__in=person.follow_tag.all())

并且您可以搜索 'persons' 后的标签如下:

Person.objects.filter(follow_tag__in=[<tag_ids>])

这同样适用于作者,您可以使用查询集对您的查询执行 OR、AND 等操作。

如果您希望更多模型能够关注标签或作者,例如 System,也许您可​​以创建一个 Following 模型来做同样的事情 Person这样做然后你可以在 PersonSystem

中添加一个 ForeignKeyFollow

请注意,我正在使用此 Person 来满足此 recomendation

您应该只按 ID 过滤。

首先获取要过滤的对象的 ID

following_user = userA.relationship.follows_user.all().values_list('id', flat=True)
following_tag = userA.relationship.follows_tag.all()

您还需要过滤 actor_type。可以这样做 for example.

actor_type = ContentType.objects.get_for_model(userA.__class__)

或者@Todor 在评论中建议。因为 get_for_model 接受模型 class 和模型实例

actor_type = ContentType.objects.get_for_model(userA)

而且你可以像这样过滤。

Activity.objects.filter(Q(actor_id__in=following_user, actor_type=actor_type) | Q(tags__in=following_tag))

您可以使用注释将两个主键连接为一个字符串,然后使用它来过滤您的查询集。

from django.db.models import Value, TextField
from django.db.models.functions import Concat

following_actor = [
    # actor_type, actor
    (1, 100),
    (2, 102),
]
searchable_keys = [str(at) + "__" + str(actor) for at, actor in following_actor]

result = MultiKey.objects.annotate(key=Concat('actor_type', Value('__'), 'actor_id', 
                                              output_field=TextField()))\
    .filter(Q(key__in=searchable_keys) | Q(tags__in=following_tag))

docs 的建议并不是坏事。

问题是,当您创建 Activities 时,您将 auth.User 用作 actor,因此您无法将 GenericRelation 添加到 auth.User (也许你可以通过猴子修补它,但这不是一个好主意).

那你能做什么?

@Sardorbek Imomaliev 解决方案非常好,如果将所有这些逻辑放入自定义 QuerySet class 中,您可以使它变得更好。 (想法是实现 DRY-ness 和可重用性)

class ActivityQuerySet(models.QuerySet):
    def for_user(self, user):
        return self.filter(
            models.Q(
                actor_type=ContentType.objects.get_for_model(user),
                actor_id__in=user.relationship.follows_user.values_list('id', flat=True)
            )|models.Q(
                tags__in=user.relationship.follows_tag.all()
            )
        )

class Activity(models.Model):
    #..
    objects = ActivityQuerySet.as_manager()

#usage
user_feed = Activity.objects.for_user(request.user)

但是还有什么吗?

1. actor 你真的需要 GenericForeignKey 吗?我不知道你的业务逻辑,所以你可能知道,但是只使用常规的 FK 作为演员(就像 tags 一样)将可以像 actor__in=users_following 这样的工作人员.

2. 您是否检查过是否没有相应的应用程序?已经解决您问题的软件包的一个示例是 django-activity-steam 检查它。

3. 如果你不使用 auth.User 作为 actor 你可以完全按照 docs 的建议 -> 添加GenericRelation 字段。事实上,你的 Relationship class 适合这个目的,但我真的会把它重命名为 UserProfile 或至少 UserRelation 之类的东西。假设我们已将 Relation 重命名为 UserProfile,并且我们使用 userprofile 创建新的 Activities。思路是:

class UserProfile(models.Model):
    user = AutoOneToOneField('auth.user')
    follows_user = models.ManyToManyField('UserProfile', related_name='followed_by')
    follows_tag = models.ManyToManyField(Tag)

    activies_as_actor = GenericRelation('Activity',
        content_type_field='actor_type',
        object_id_field='actor_id',
        related_query_name='userprofile'
    )


class ActivityQuerySet(models.QuerySet):
    def for_userprofile(self, userprofile):
        return self.filter(
            models.Q(
                userprofile__in=userprofile.follows_user.all()
            )|models.Q(
                tags__in=userprofile.relationship.follows_tag.all()
            )
        )


class Activity(models.Model):
    #..
    objects = ActivityQuerySet.as_manager()

#usage

#1st when you create activity use UserProfile
Activity.objects.create(actor=request.user.userprofile, ...)

#2nd when you fetch. 
#Check how `for_userprofile` is implemented this time
Activity.objects.for_userprofile(request.user.userprofile)