【问题标题】:'private' models, default query sets and chaining methods“私有”模型、默认查询集和链接方法
【发布时间】:2011-09-17 01:14:54
【问题描述】:

我的模型上有一个 private 布尔标志,以及一个使用过滤器覆盖 get_query_set 方法的自定义管理器,删除 private=True:

class myManager(models.Manager):
    def get_query_set(self):
        qs = super(myManager, self).get_query_set()
        qs = qs.filter(private=False)
        return qs

class myModel(models.Model):
    private = models.BooleanField(default=False)
    owner = models.ForeignKey('Profile', related_name="owned")
    #...etc...

    objects = myManager()

我希望将排除私有模型的默认查询集作为默认安全措施,防止意外使用显示私有模型的模型。

不过,有时我会想展示私人模型,所以我在管理器上有以下内容:

def for_user(self, user):
    if user and not user.is_authenticated():
        return self.get_query_set()
    qs = super(myManager, self).get_query_set()
    qs = qs.filter(Q(owner=user, private=True) | Q(private=False))
    return qs

这非常有效,但我无法链接过滤器。当我有一个指向 myModel 并使用 otherModel.mymodel_set 的 fk 时,这会成为一个问题。 otherModel.mymodel_set.for_user(user) 不起作用,因为 mymodel_set 返回一个 QuerySet 对象,而不是管理器。

现在真正的问题开始了,因为我看不到让 for_user() 方法在 QuerySet 子类上工作的方法,因为我无法从 QuerySet 访问完整的、未过滤的查询集(基本上覆盖 get_query_set)子类,就像我可以在管理器中一样(使用 super() 获取基本查询集。)

解决此问题的最佳方法是什么?

我没有绑定到任何特定的界面,但我希望它尽可能的 djangoy/DRY。显然,我可以放弃安全性,只在每次调用时调用一个方法来过滤掉私人任务,但我真的不想这样做。

更新

manji 下面的答案非常接近,但是当我想要的查询集不是默认查询集的子集时它会失败。我想这里真正的问题是如何从链式查询中删除特定过滤器?

【问题讨论】:

    标签: django django-models orm django-queryset


    【解决方案1】:

    定义一个自定义查询集(包含您的自定义过滤方法):

    class MyQuerySet(models.query.QuerySet):
    
        def public(self):
            return self.filter(private=False)
    
        def for_user(self, user):
            if user and not user.is_authenticated():
                return self.public()
            return self.filter(Q(owner=user, private=True) | Q(private=False))
    

    定义一个将使用MyQuerySet 的自定义管理器(MyQuerySet 自定义过滤器将可以访问,就好像它们是在管理器中定义的一样[通过覆盖__getattr__]):

    # A Custom Manager accepting custom QuerySet
    class MyManager(models.Manager):
    
        use_for_related_fields = True
    
        def __init__(self, qs_class=models.query.QuerySet):
            self.queryset_class = qs_class
            super(QuerySetManager, self).__init__()
    
        def get_query_set(self):
            return self.queryset_class(self.model).public()
    
        def __getattr__(self, attr, *args):
            try:
                return getattr(self.__class__, attr, *args)
            except AttributeError:
                return getattr(self.get_query_set(), attr, *args) 
    

    然后在模型中:

    class MyModel(models.Model):
        private = models.BooleanField(default=False)
        owner = models.ForeignKey('Profile', related_name="owned")
        #...etc...
    
        objects = myManager(MyQuerySet)
    

    现在你可以:

       ¤默认仅访问公共模型:

        MyModel.objects.filter(..
    

       ¤ 访问 for_user 个模型:

        MyModel.objects.for_user(user1).filter(..
    

    由于 (use_for_related_fields = True),同一管理器将用于相关管理器。所以你也可以:

       ¤默认仅访问相关经理的公开模型:

        otherModel.mymodel_set.filter(..
    

       ¤ 访问相关经理的for_user

        otherModel.mymodel_set.for_user(user).filter(..
    


    更多信息:Subclassing Django QuerySets & Custom managers with chainable filters (django sn-p)

    【讨论】:

    • MyModel.objects.all() 怎么样?看起来这会返回一切。尽管如此,这看起来仍然是最好的选择。我试试看。
    • 不,MyModel.objects.all() 将返回过滤后的模型(仅公开)。 models.Manager的所有过滤方法都使用get_query_set
    • 绑定后,SQL 中的 WHERE 语句被 ANDed 在一起: SELECT [*] FROM ["table"] WHERE ("model"."private" = False AND (("model". "owner_id" = 2 AND "model"."private" = True ) OR "model"."private" = False )) ORDER BY "model"."id" DESC 我需要的是用 OR 加入过滤器比 AND...
    • 澄清一下,我的灯具应该得到 4 个对象(私有对象和 3 个公共对象),而我实际上得到 1 个。
    【解决方案2】:

    要使用链,您应该覆盖管理器中的 get_query_set 并将 for_user 放置在您的自定义 QuerySet 中。

    我不喜欢这个解决方案,但它确实有效。

    class CustomQuerySet(models.query.QuerySet):
        def for_user(self):
            return super(CustomQuerySet, self).filter(*args, **kwargs).filter(private=False)
    
    class CustomManager(models.Manager):
        def get_query_set(self):
            return CustomQuerySet(self.model, using=self._db)
    

    【讨论】:

      【解决方案3】:

      如果您需要“重置”查询集,您可以访问查询集的模型并再次调用原始管理器(以完全重置)。但是,这对您来说可能不是很有用,除非您跟踪以前的过滤器/排除等语句并且可以在重置查询集上再次重播它们。有一点计划,实际上不会太难做,但可能有点蛮力。

      总体而言,manji的答案绝对是正确的方法。

      所以修改 manji 的答案,您需要将现有的 "model"."private" = False 替换为 ("model"."owner_id" = 2 AND "model"."private" = True ) OR "model"."private" = False )。为此,您需要遍历查询集的query 对象上的where 对象,以找到要删除的相关位。查询对象有一个WhereNode 对象,它表示 where 子句的树,每个节点都有多个子节点。您必须调用节点上的 as_sql 来确定它是否是您所追求的:

      
      from django.db import connection
      qn = connection.ops.quote_name
      q = myModel.objects.all()
      print q.query.where.children[0].as_sql(qn, connection)
      

      这应该会给你类似的东西:

      
      ('"model"."private" = ?', [False])
      

      然而,尝试这样做可能比它的价值付出更多的努力,而且它正在深入研究可能不是 API 稳定的 Django。

      我的建议是使用两名经理。一个可以访问所有内容(一种逃生舱),另一个应用了默认过滤。默认管理器是第一个管理器,因此您需要根据需要执行的操作来处理排序。然后重构你的代码以知道使用哪一个 - 这样你就不会有额外的 private=False 子句的问题了。

      【讨论】:

        猜你喜欢
        • 2018-05-06
        • 2016-01-16
        • 2020-09-03
        • 2015-04-20
        • 1970-01-01
        • 2013-11-18
        • 2023-03-24
        • 1970-01-01
        • 2018-11-17
        相关资源
        最近更新 更多