Django:我如何在 django.db.models.query QuerySet 中重载 Q 以在我的管理器中用于特殊目的

Posted

技术标签:

【中文标题】Django:我如何在 django.db.models.query QuerySet 中重载 Q 以在我的管理器中用于特殊目的【英文标题】:Django : how could I overload Q in django.db.models.query QuerySet to use for a special purpose in my Manager 【发布时间】:2015-09-26 17:03:23 【问题描述】:

首先,由于这是我的第一个问题/帖子,我想感谢大家为这个伟大的社区和令人惊叹的服务,就像世界各地的许多开发人员一样 - *** -- 是我解决代码问题的主要资源。

注意:

这篇文章有点长(抱歉),涵盖了两个不同但相关的情况,并组织如下:

    背景/上下文 一个设计问题,我希望收到您的建议。 我尝试实施的解决方案来解决 2。 与 3 相关的实际问题和问题。

1. 一些上下文:

我有两个不同的 Model 对象(ReportJobs)来自设计不佳的现有实现。 事实上,这两个对象的目的非常相似,但可能是在两个不同的时间范围内实现的。

在这些对象上发生了很多处理,并且由于系统必须发展,我开始编写一个元类/接口,它们都将是子类。目前Models 使用不同的Fields 名称用于相同的目的,例如authorjuser 表示User(这很愚蠢)等等。

因为我不能只更改数据库中的列名称,然后通过数千行代码来更改对这些字段的每个引用(尽管我可以,感谢 Find Usages现代 IDE 的特性),而且因为这些对象可能会在其他地方使用,所以我使用了 db_column= 壮举。能够在每个模型中拥有相同的字段名称并最终以相似的方式处理两个对象(而不是使用数千行重复的代码来做同样的事情)。

所以,我有类似的东西:

from django.db import models
class Report(Runnable):
    _name = models.CharField(max_length=55, db_column='name')
    _description = models.CharField(max_length=350, blank=True, db_column='description')
    _author = ForeignKey(User, db_column='author_id')
    # and so on

class Jobs(Runnable):
    _name = models.CharField(max_length=55, db_column='jname')
    _description = models.CharField(max_length=4900, blank=True, db_column='jdetails')
    _author = ForeignKey(User, db_column='juser_id')
    # and so on

正如我之前所说,为了避免重写对象的客户端代码,我使用了隐藏字段的属性:

from django.db import models
class Runnable(models.Model):   
    objects = managers.WorkersManager() # The default manager.

    @property # Report
    def type(self):
        return self._type

    @type.setter # Report
    def type(self, value):
        self._type = value

    # for backward compatibility, TODO remove both
    @property # Jobs
    def script(self):
        return self._type

    @script.setter # Jobs
    def script(self, value):
        self._type = value
    # and so on

2。设计问题:

这很好,这正是我想要的,除了现在使用 Report.objects.filter(name='something')Jobs.objects.filter(jname='something') 不起作用,显然是由于 Django 设计(等等.get().exclude() 等... ),遗憾的是,客户端代码充满。 我当然打算用我新创建的 WorkersManager 的方法替换它们

公寓: 等等……什么? "新创建的 WorkersManager" ?? 是的,经过两年和数千行代码,这里没有Manager,疯了吧?但你猜怎么着?这是我最不关心的;振作起来,因为大部分代码仍然位于 view.py 和相关文件中(而不是正确地位于它应该操作的对象中),基本上有点“纯”命令式 python... 不错吧?

3. 我的解决方案:

经过大量阅读(这里和那里)和研究,我发现:

    尝试子类化 Field,不是解决方案 我实际上可以超载QuerySet

所以我做到了:

from django.db.models.query_utils import Q as __originalQ
class WorkersManager(models.Manager):   
    def get_queryset(self):
        class QuerySet(__original_QS):
            """
            Overloads original QuerySet class
            """
            __translate = _translate # an external fonction that changes the name of the keys in kwargs

            def filter(self, *args, **kwargs):
                args, kwargs = self.__translate(*args, **kwargs)
                super(QuerySet, self).filter(args, kwargs)

            # and many more others [...]
        return QuerySet(self.model, using=self._db)

这很好。

4. 那么怎么了?

问题是 Django 在内部使用 db.model.query 中的 Q,使用它自己的导入,并且没有任何地方暴露或引用 Q,因此它可能被重载。

>>> a =Report.objects.filter(name='something')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/venv/local/lib/python2.7/site-packages/django/db/models/manager.py", line 143, in filter
    return self.get_query_set().filter(*args, **kwargs)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/query.py", line 624, in filter
    return self._filter_or_exclude(False, *args, **kwargs)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/query.py", line 642, in _filter_or_exclude
    clone.query.add_q(Q(*args, **kwargs))
  File "/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1250, in add_q
    can_reuse=used_aliases, force_having=force_having)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1122, in add_filter
    process_extras=process_extras)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1316, in setup_joins
    "Choices are: %s" % (name, ", ".join(names)))
FieldError: Cannot resolve keyword 'name' into field. Choices are: _author, _description, _name, # and many more

但我确实记得读过一些关于 Django 如何仅加载第一次出现的 Model 的内容,以及如何在使用 import 之前通过重新定义这样的 Model 来欺骗它(显然这不适用于 python) 所以最终我试图重载Q,在导入相关类之前或之后重新定义它,但我无法弄清楚。

这是我尝试过的:

from django.db.models.query_utils import Q as __originalQ

__translation = 'name': '_name', # has much more, just for exemple

def _translate(*args, **kwargs):
    for key in kwargs:
        if key in __translation.keys():
            kwargs[__translation[key]] = kwargs[key]
            del kwargs[key]
    return args, kwargs

class Q(__originalQ):
    """
    Overloads original Q class
    """
    def __init__(self, *args, **kwargs):
        super(Q, self).__init__(_translate(*args, **kwargs))

# now import QuerySet which should use the new Q class
from django.db.models.query import QuerySet as __original_QS

class QuerySet(__original_QS):
    """
    Overloads original QuerySet class
    """
    __translate = _translate # writing shortcut

    def filter(self, *args, **kwargs):
        args, kwargs = self.__translate(*args, **kwargs)
        super(QuerySet, self).filter(args, kwargs)
    # and some others

# now import QuerySet which should use the new QuerySet class
from django.db import models

class WorkersManager(models.Manager):
    def get_queryset(self):
        # might not even be required if above code was successful
        return QuerySet(self.model, using=self._db)

这当然没有效果,因为 Q 在 _filter_or_exclude 的定义中从 django.db.model.query 重新导入。 所以当然,一个直观的解决方案是重载_filter_or_exclude,并复制其原始代码而不调用super但这里有一个问题:我使用的是旧版本的 Django,可能有一天会更新,并且我不想弄乱 Django 实现细节,就像我已经对 get_queryset 所做的那样,但我想这还不错,因为它(据我所知)是一个用于重载的占位符,而且它也是唯一的办法。

所以我在这里,我的问题是:没有其他办法吗?我没有办法在 Django 模块中重载 Q 吗?

非常感谢您一路阅读:)

这是一个土豆(Oups,错误的网站,抱歉 :))

编辑:

所以,在尝试重载_filter_or_exclude之后,似乎没有任何效果。 我可能遗漏了一些关于调用堆栈顺序或类似内容的信息……我明天继续,让你知道。

【问题讨论】:

我认为每次新的 hack 都会使问题变得更加复杂。老实说,不要认为有灵丹妙药,你最终会重载 Django db 的每个部分,以便(可能)出现更多错误。可悲的是,恕我直言,清理/重构代码可能更有价值。 【参考方案1】:

是的!我找到了解决方案。

原来,首先,忘记在我的函数中有一个return,比如:

def filter(self, *args, **kwargs):
    args, kwargs = self.__translate(*args, **kwargs)
    super(QuerySet, self).filter(args, kwargs)

而不是:

def filter(self, *args, **kwargs):
    args, kwargs = self.__translate(*args, **kwargs)
    return super(QuerySet, self).filter(args, kwargs)

我也有:

args, kwargs = self.__translate(*args, **kwargs)

而不是:

args, kwargs = self.__translate(args, kwargs)

这会导致函数调用解包,因此原始kwargs 中的所有内容都以args 结尾,从而阻止translate 产生任何影响。

但更糟糕的是,我无法理解我可以直接从我的自定义管理器中直接重载 filterget 等等... 这让我省去了处理QuerySetQ 的工作量。

最后,以下代码按预期工作:

def _translate(args, kwargs):
    for key in kwargs.keys():
        if key in __translation.keys():
            kwargs[__translation[key]] = kwargs[key]
            del kwargs[key]
    return args, kwargs

class WorkersManager(models.Manager):
    def filter(self, *args, **kwargs):
        args, kwargs = _translate(args, kwargs)
        return super(WorkersManager, self).filter(*args, **kwargs)

    # etc...

【讨论】:

以上是关于Django:我如何在 django.db.models.query QuerySet 中重载 Q 以在我的管理器中用于特殊目的的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Django 2 中实现 Django-Private-Chat

如何在 Django 中进行故障排除?

django - 如何渲染我在网页中随处使用的模板?

如何在 django 设置文件中添加相同站点无选项

如何在 Django 中编辑和删除数据?

如何在 Django 模型上存储字典?