Django 管理器链式调用

31

我在思考是否可能(如果可能的话,如何)将多个管理器链接在一起,从而生成一个同时受到这些管理器影响的查询集。我将解释我正在处理的具体示例:

我有多个抽象模型类,用于向其他模型提供小型、特定的功能。其中两个模型是DeleteMixin和GlobalMixin。

DeleteMixin的定义如下:

class DeleteMixin(models.Model):
    deleted = models.BooleanField(default=False)
    objects = DeleteManager()

    class Meta:
        abstract = True

    def delete(self):
        self.deleted = True
        self.save()
基本上它提供了一种伪删除(即删除标志),而不是实际删除对象。
GlobalMixin 定义如下:
class GlobalMixin(models.Model):
    is_global = models.BooleanField(default=True)

    objects = GlobalManager()

    class Meta:
        abstract = True

它允许任何对象定义为全局对象或私有对象(例如公共/私有博客文章)。

这两者都有自己的管理器,影响返回的查询集。我的DeleteManager将查询集过滤,仅返回已将删除标志设置为False的结果,而GlobalManager将查询集过滤,仅返回标记为全局的结果。以下是两者的声明:

class DeleteManager(models.Manager):
    def get_query_set(self):
        return super(DeleteManager, self).get_query_set().filter(deleted=False)

class GlobalManager(models.Manager):
    def globals(self):
        return self.get_query_set().filter(is_global=1)
期望的功能是有一个模型可以扩展这两个抽象模型并且允许只返回既非删除的又是全局的结果。我针对拥有4个实例的模型运行了一项测试用例:其中一个实例是全局的并且未被删除,第二个是全局的但已被删除,第三个是非全局且未被删除的,第四个是非全局且已被删除的。如果我尝试获取如下的结果集:SomeModel.objects.all(),我将得到实例1和3(即两个未被删除的实例-很好!)。如果我尝试SomeModel.objects.globals(),我会收到一个错误消息,指出DeleteManager没有globals方法(这是基于我的模型声明为SomeModel(DeleteMixin, GlobalMixin)的情况)。如果我反转顺序,则不会出现错误,但它不会过滤掉已删除的实例。如果我将GlobalMixin更改为将GlobalManager附加到globals而不是objects上(因此新命令将是SomeModel.globals.globals()),我将得到实例1和2(即两个全局实例),而我想要的结果是只获取实例1(即全局、未被删除的实例)。
我不确定是否有人遇到过类似的情况并得出了结果。不管是在当前思路下使其工作的方法还是提供所需功能的重新设计都将非常感激。我知道这篇文章有点啰嗦。如果需要更多解释,我很乐意提供。
编辑:我在下面发布了我用于解决这个特定问题的最终解决方案。它基于Simon的自定义QuerySetManager的链接。
5个回答

22

在Djangosnippets上查看此片段:http://djangosnippets.org/snippets/734/

与其将自定义方法放在管理器中,不如直接子类化查询集本身。这非常容易且完美地工作。我唯一遇到的问题是在模型继承中,你必须在模型子类中定义管理器(只需在子类中使用“objects = QuerySetManager()”),即使它们将继承查询集。一旦您开始使用QuerySetManager,这将更有意义。


看起来问题已经解决了。我应该知道更多地关注查询集,而不是管理器。我把我的最终解决方案作为答案发布了,尽管我选择了你的答案作为最佳答案。 - Adam

10

这是我的问题的具体解决方案,使用了Scott链接到的Simon的自定义QuerySetManager。

from django.db import models
from django.contrib import admin
from django.db.models.query import QuerySet
from django.core.exceptions import FieldError

class MixinManager(models.Manager):    
    def get_query_set(self):
        try:
            return self.model.MixinQuerySet(self.model).filter(deleted=False)
        except FieldError:
            return self.model.MixinQuerySet(self.model)

class BaseMixin(models.Model):
    admin = models.Manager()
    objects = MixinManager()

    class MixinQuerySet(QuerySet):

        def globals(self):
            try:
                return self.filter(is_global=True)
            except FieldError:
                return self.all()

    class Meta:
        abstract = True

class DeleteMixin(BaseMixin):
    deleted = models.BooleanField(default=False)

    class Meta:
        abstract = True

    def delete(self):
        self.deleted = True
        self.save()

class GlobalMixin(BaseMixin):
    is_global = models.BooleanField(default=True)

    class Meta:
        abstract = True
任何未来想要向查询集添加额外功能的混合(mixin)只需扩展BaseMixin(或在其继承层次结构中有它)。 每次我尝试将查询集过滤下来时,我会用try-catch包装它,以防该字段实际上不存在(即它没有扩展该mixin)。 全局过滤器使用globals()调用,而删除过滤器则会自动调用(如果某些内容被删除,则永远不希望它显示)。 使用此系统可以执行以下类型的命令:
TemporaryModel.objects.all() # If extending DeleteMixin, no deleted instances are returned
TemporaryModel.objects.all().globals() # Filter out the private instances (non-global)
TemporaryModel.objects.filter(...) # Ditto about excluding deleteds

需要注意的一点是,删除过滤器不会影响管理员界面,因为默认Manager首先被声明(使其成为默认值)。我不记得他们何时将管理员更改为使用Model._default_manager而不是Model.objects,但任何已删除的实例仍将出现在管理员中(以防需要取消删除)。


1
要让管理员使用您想要的任何管理器,请查看此处:https://dev59.com/XXI_5IYBdhLWcg3wDOrW - Chris Lawlor

2
我花了一段时间来想如何建立一个好的工厂来实现这个目标,但是我遇到了很多问题。
我能提供的最佳建议是将你的继承链起来。它并不是非常通用,所以我不确定它有多少用处,但你所需要做的就是:
class GlobalMixin(DeleteMixin):
    is_global = models.BooleanField(default=True)

    objects = GlobalManager()

    class Meta:
        abstract = True

class GlobalManager(DeleteManager):
    def globals(self):
        return self.get_query_set().filter(is_global=1)

如果您想要更通用的东西,最好的方法是定义一个基本的Mixin和Manager,重新定义get_query_set()(我假设您只想这样做一次;否则会变得非常复杂),然后通过Mixin列表传递要添加的字段。它看起来像这样(完全没有测试):
class DeleteMixin(models.Model):
    deleted = models.BooleanField(default=False)

    class Meta:
        abstract = True

def create_mixin(base_mixin, **kwargs):
    class wrapper(base_mixin):
        class Meta:
            abstract = True
    for k in kwargs.keys():
        setattr(wrapper, k, kwargs[k])
    return wrapper

class DeleteManager(models.Manager):
    def get_query_set(self):
        return super(DeleteManager, self).get_query_set().filter(deleted=False)

def create_manager(base_manager, **kwargs):
    class wrapper(base_manager):
        pass
    for k in kwargs.keys():
        setattr(wrapper, k, kwargs[k])
    return wrapper

好的,那么这看起来很丑,但它给你带来了什么?本质上,这是相同的解决方案,但更加动态,并且稍微DRY一些,尽管阅读起来更加复杂。

首先,您需要动态地创建您的管理器:

def globals(inst):
    return inst.get_query_set().filter(is_global=1)

GlobalDeleteManager = create_manager(DeleteManager, globals=globals)

这将创建一个新的管理器,它是DeleteManager的子类,并具有名为globals的方法。

接下来,您需要创建您的混入模型:

GlobalDeleteMixin = create_mixin(DeleteMixin,
                                 is_global=models.BooleanField(default=False),
                                 objects = GlobalDeleteManager())

就像我说的那样,这很丑陋。但它意味着你不必重新定义 globals()。如果你想让不同类型的管理器拥有 globals(),你只需使用不同的基础再次调用 create_manager。你可以添加任意数量的新方法。对于管理器也是一样,只需不断添加新函数以返回不同的查询集。

那么,这真的实用吗?也许不是。这个答案更多地是滥用 Python 的灵活性的练习。虽然我确实使用了一些动态扩展类的基本原理来使访问变得更容易。

如果有任何不清楚的地方,请告诉我,我会更新答案。


链式继承肯定可以实现,但遗憾的是这会破坏我的混入的目的。有时候(我不确定什么时候),我只想让某些东西具有GlobalMixin或DeleteMixin(或两者都有)。我一定要研究一下你的其他建议。我同意这有点丑陋,但也许可以进行清理,以提供更干净的功能包。 - Adam
通过使用models.Model作为基本mixin和models.Manager作为基础manager,您可以使用此方法创建它们,但只有在您将拥有大量mixin和manager的排列组合时才值得去做。 - tghw

2

看起来这个包不再提供此功能了。 - raisinrising

0

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - GAVD
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - GAVD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接