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

首先,由于这是我的第一个 question/post,我想 感谢大家 这个伟大的社区和出色的服务,就像——世界各地的许多开发人员 -- Whosebug -- 是我解决代码问题的主要资源。

通知:

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

  1. 背景/背景
  2. 一个设计问题,我想听听您的建议。
  3. 我正在尝试实施以解决 2 的解决方案。
  4. 与 3.
  5. 相关的实际问题和疑问

1. 一些上下文:

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

很多处理都是在这些对象上进行的,由于系统要进化,我开始写一个metaclass/interface,两者都将是subclasses。目前,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

的方法替换它们

Aparté :
Wait ... what ? "newly created WorkersManager" ??
Yes, after two years and thousands of line of code, there was no Manager in here, crazy right ?
But guess what ? that's the least of my concerns; and cheer up, since most of the code still lies in view.py and assosiated files (instead of being properly inside the objects it is suposed to manipulate), basicaly somewhat "pure" imperative python...

Great right ?


3. 我的解决方案:

经过大量阅读(到处)和研究之后,我发现:

  1. 尝试子class Field,不是解决方案
  2. 我实际上可以超载 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 在内部使用 Q 在 db.model.query 中使用它自己的导入,并且没有任何地方公开或引用 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 的内容,以及如何在使用导入之前通过重新定义这样的 Model 来欺骗它(很明显这不会'适用于python)
所以最终我试图通过在导入相关 class 之前或之后重新定义它来重载 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 吗?

非常感谢大家一路阅读:)

这是一个土豆(哎呀,网站错了,抱歉:))

编辑:

所以,在尝试重载_filter_or_exclude之后,好像没有效果。
我可能遗漏了一些关于调用堆栈顺序或类似的东西......我明天继续,让你知道。

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

首先,我的函数中忘记了 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...