如何在Django中查询具有特定数量的外键关系并且在这些外键值中具有特定值的对象?

Posted

技术标签:

【中文标题】如何在Django中查询具有特定数量的外键关系并且在这些外键值中具有特定值的对象?【英文标题】:How to query for an object that has specific number of foreignKey relations and has specific values in those foreinKey values in Django? 【发布时间】:2021-09-11 11:13:05 【问题描述】:

我正在构建一个用于聊天应用程序的 Django GraphQL API。我有以下 2 种模型,一种用于聊天,一种用于聊天成员,它们作为外键绑定:-

class Chat(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    members = models.ManyToManyField(User, related_name="privateChats",
                                     through="ChatMember", through_fields=('chat', 'member'), blank=True)
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name


class ChatMember(models.Model):
    chat = models.ForeignKey(Chat, on_delete=models.CASCADE)
    member = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return self.chat

我有一种方法应该接受 2 个用户 ID,首先它会检查这 2 个成员之间是否存在现有聊天。如果存在,则返回该聊天。如果不是,那么它会为这 2 个成员创建一个新聊天并返回新创建的聊天。

棘手的部分是它尝试搜索是否存在现有聊天。因为我在群聊中使用了相同的聊天模型,所以它可能会返回一个也有这 2 个成员的群组。因此,检查这 2 个成员之间是否存在一对一聊天的条件是,首先检查聊天中是否只有 2 个成员,并且这 2 个成员具有我作为该方法的输入获得的 id。

我对 Django 和 Python 非常陌生,所以我不知道要编写查询来查找是否存在这样的聊天。到目前为止,这就是我为此所做的:-

class ChatWithMember(graphene.Mutation):

    class Meta:
        description = "Mutation to get into a Chat"

    class Arguments:

        # ***This is the first user's Id.***

        id = graphene.ID(required=True) 

    ok = graphene.Boolean()
    chat = graphene.Field(ChatType)

    @staticmethod
    @login_required
    def mutate(root, info, id):
        ok = True
        
        # ***Second user's id is taken from the request***

        current_user = info.context.user 
        member = User.objects.get(pk=id)

        if member is None:
            return ChatWithMember(ok=False, chat=None)

        # ***First I annotate the number of members in 'individual_chats'***

        individual_chats = Chat.objects.annotate(member_count=Count('members'))

        # ***Then I filter those chats that have only 2 members***

        individual_chats = individual_chats.filter(member_count=2)

        # ***Then I loop through those to check if there's one that has the two members in that chat and if it exists, I return it.***

        for chat in individual_chats.all():
            if current_user.id in chat.members and member.id in chat.members:
                return ChatWithMember(ok=ok, chat=chat)

        chat_instance = Chat()
        chat_instance.save()

        # Adding the creator of the chat and the member
        chat_instance.members.set([current_user.id, id])

        return ChatWithMember(ok=ok, chat=chat_instance)

目前我收到错误__str__ returned non-string (type NoneType),之前我收到了TypeError: ManyRelatedManager object is not iterable。我在这方面花了很多时间,显然我需要一些帮助。

我根据自己对如何完成它的理解编写了这个方法,但我不确定是否存在更简单的方法。请告诉我该怎么做。

更新:-

我根据@Henri 的建议修改了方法:-

    def mutate(root, info, id):
        ok = True
        current_user = info.context.user
        member = User.objects.get(pk=id)
        if member is None:
            return ChatWithMember(ok=False, chat=None)
        individual_chats = Chat.objects.annotate(member_count=Count('members'))
        individual_chats = individual_chats.filter(member_count=2)
        print('Individual chats before try => ',
              individual_chats.values_list('name', 'members__id'))
        try:
            chats_with_these_2_members = individual_chats.get(
                members__in=[member, current_user])
            return ChatWithMember(ok=ok, chat=chats_with_these_2_members)
        except MultipleObjectsReturned:
            print('multiple chats found!')
            return ChatWithMember(ok=False, chat=None)
            # Error because multiple chats found with these 2 members (and only them)
        except Chat.DoesNotExist:

            print('Creating new chat')
            chat_instance = Chat()
            chat_instance.save()

            # Adding the creator of the chat and the member
            chat_instance.members.set([current_user.id, id])

            return ChatWithMember(ok=ok, chat=chat_instance)

有了这个,当我尝试使用已经与他们聊天的两个用户作为仅有的 2 个成员触发此方法时,我在控制台中得到以下输出:-

Individual chats before try => <QuerySet []> Creating new chat

所以基本上它无法识别符合条件的聊天的存在并继续并创建一个新的聊天。

更糟糕的是,如果我在创建第一个聊天后尝试与不同的成员组合发起聊天,我会得到:-

Individual chats before try => <QuerySet []> multiple chats found!

这意味着它不再为不同的成员组合创建新的聊天。

这部分有很大的问题:-

try:
            chats_with_these_2_members = individual_chats.get(
                members__in=[member, current_user])
            return ChatWithMember(ok=ok, chat=chats_with_these_2_members)
        except MultipleObjectsReturned:
            print('multiple chats found!')
            return ChatWithMember(ok=False, chat=None)
            # Error because multiple chats found with these 2 members (and only them)
        except Chat.DoesNotExist:

但我不知道如何解决它,因为我对 Python 太陌生了。

【问题讨论】:

【参考方案1】:

第一部分看起来不错:

    individual_chats = Chat.objects.annotate(member_count=Count('members'))

    # ***Then I filter those chats that have only 2 members***

    individual_chats = individual_chats.filter(member_count=2)

我猜你现在得到了与 2 个成员聊天的正确查询集。

然后你可以过滤:

    chats_with_these_2_members = individual_chats.filter(members__in=[current_user, member])
    if chats_with_these_2_members.exists():
         return chats_with_these_2_members.first()
    else:
        # Create it

编辑:有错误,一个更好的版本会是

    from django.core.exceptions import MultipleObjectsReturned
    try:
        chats_with_these_2_members = individual_chats.get(members__in=[current_user, member])
    except MultipleObjectsReturned:
        # Error because multiple chats found with these 2 members (and only them)
    except Chat.DoesNotExist:
        # Create it 

编辑 2:这个版本写得更好,可以帮助您识别问题。

def mutate(root, info, id):
    ok = True
    current_user = info.context.user
    try:
        member = User.objects.get(pk=id)
    except User.DoesNotExist:
        return ChatWithMember(ok=False, chat=None)
    
    chats_annotated = Chat.objects.annotate(member_count=Count('members'))
    individual_chats = chats_annotated.filter(member_count=2)
    print(f'Individual chats => individual_chats.count() found')
    print(f'Chats with member and current_user => individual_chats.filter(members__in=[member, current_user]).count() found')
    
    chat_instance = None
    try:
        chat_instance = individual_chats.get(members__in=[member, current_user])
    except MultipleObjectsReturned:
        print('multiple chats found!')
        ok = False
        # Error because multiple chats found with these 2 members (and only them)
    except Chat.DoesNotExist:
        print('Creating new chat')
        chat_instance = Chat(name='name of the chat')
        chat_instance.save()
        # Adding the creator of the chat and the member
        chat_instance.members.add(current_user, member)

    return ChatWithMember(ok=ok, chat=chat_instance)

使用前 2 次打印,您应该得到个人聊天的数量(应该 > 1)以及与成员和当前用户的个人聊天数量(应该是 0 或 1)。

顺便问一下,你为什么使用ChatMember 作为through 参数?就像doc 中解释的那样,当您想向多对多关系添加额外数据时使用它。但就您而言,您没有任何额外数据。只需memberchat

【讨论】:

我在这里根据您的代码添加了第二部分,但我收到错误 name 'members' is not defined。我认为过滤器语句可能需要以不同的方式编写,但不确定如何。 我的错,我犯了一个错误。我编辑了我的答案 我尝试了新的编辑,但这部分失败了 => members__in=[current_user, member]。它似乎没有做我们希望它做的事情。它只是简单地返回它找到的任何聊天。 你是什么意思“返回它找到的任何聊天”?获取查询members__in=[current_user, member] 将使您与这两个成员聊天。这种表示法常用***.com/questions/4507893/… 我刚刚检查过了。由于某种原因,它没有按预期工作。早些时候它返回了我在数据库中唯一的聊天,即使它不满足条件(即即使聊天没有这些用户 ID)。现在我刚刚创建了其他聊天,现在它似乎在 try 块中遇到了一些错误,即使存在满足这些条件的聊天,它也会自动创建一个新聊天。不知道如何继续调试,因为当我在 try 块之前添加 print(individual_chats) 时,它会显示 TypeError: __str__ returned non-string (type NoneType)【参考方案2】:

我将聊天模型修改为以下内容:-

class Chat(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    individual_member_one = models.ForeignKey(
        User, related_name="chat_member_one", on_delete=models.CASCADE, blank=True, null=True)
    individual_member_two = models.ForeignKey(
        User, related_name="chat_member_two", on_delete=models.CASCADE, blank=True, null=True)
    # Type Choices

    class TypeChoices(models.TextChoices):
        INDIVIDUAL = "IL", _('Individual')
        GROUP = "GP", _('Group')
    # End of Type Choices

    chat_type = models.CharField(
        max_length=2, choices=TypeChoices.choices, default=TypeChoices.INDIVIDUAL)
    admins = models.ManyToManyField(User, related_name="adminInChats", through="ChatAdmin", through_fields=(
        'chat', 'admin'), blank=True)
    members = models.ManyToManyField(User, related_name="privateChats",
                                     through="ChatMember", through_fields=('chat', 'member'), blank=True)
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

而新的变异方法如下:-

class ChatWithMember(graphene.Mutation):

    class Meta:
        description = "Mutation to get into a Chat"

    class Arguments:
        id = graphene.ID(required=True)

    ok = graphene.Boolean()
    chat = graphene.Field(ChatType)

    @staticmethod
    @login_required
    def mutate(root, info, id):
        ok = True
        current_user = info.context.user
        member = User.objects.get(pk=id)
        print('current_user => ', current_user, 'member => ', member)
        if member is None:
            print('There is no member with id ', id, 'member =>', member)
            return ChatWithMember(ok=False, chat=None)
        try:
            first_possibility = Chat.objects.get(
                individual_member_one=current_user.id, individual_member_two=member.id)
            return ChatWithMember(ok=ok, chat=first_possibility)
        except Chat.DoesNotExist:
            try:
                second_possibility = Chat.objects.get(
                    individual_member_one=member.id, individual_member_two=current_user.id)
                return ChatWithMember(ok=ok, chat=second_possibility)
            except:
                pass

            chat_instance = Chat(
                individual_member_one=current_user, individual_member_two=member)
            chat_instance.save()

            return ChatWithMember(ok=ok, chat=chat_instance)

这并不是对原始问题的直接回答,但它解决了我将群聊和个人聊天存储在单个模型中的问题。

【讨论】:

以上是关于如何在Django中查询具有特定数量的外键关系并且在这些外键值中具有特定值的对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Graphene-Django Relay 中的外键关系更新模型?

如何使用Django中的外键关系中的任何/ exists / all逻辑检索查询集?

Django - 夹具中的外键

使用 django 查询集访问不同的外键关系

如何在 1:m 关系中设置默认值,Django 中 user_auth_model 的外键

如何将 Django 模型对象转换为字典并且仍然拥有它们的外键? [复制]