如何避免 Django 中的导入时数据库访问?

Posted

技术标签:

【中文标题】如何避免 Django 中的导入时数据库访问?【英文标题】:How to avoid import-time database access in Django? 【发布时间】:2017-09-05 16:08:01 【问题描述】:

我的 Django 应用程序有许多类别,用于存储在 Category 模型中的东西。我在代码中经常引用这些,所以我发现有一个模块对这些类别和它们的组有引用(“常量”)很有用,所以拼写错误会很快失败。这也提供了缓存的好处。最后,它是实际模型,因此它具有所有相关功能。它看起来像这样:

def load_category(name):
  return Category.objects.get(name=name)

DOGS = load_category("dogs")
CATS = load_category("cats")

但是,这会导致导入时数据库访问并导致各种问题。在添加具有这样引用的新类别后,我必须运行数据迁移,./manage.py 才能起作用。我在切换到使用 Django 的测试框架时遇到了一个新问题,即这些从默认(例如 dev 或 prod)数据库加载,而不是 this warning 中明确提到的测试数据库。

如果您的代码尝试访问数据库时,它的模块 编译,这将在设置测试数据库之前发生,与 潜在的意想不到的结果。例如,如果您有一个数据库 在模块级代码中查询并且存在真实数据库,生产数据 可能会污染您的测试。有这样的导入时间是个坏主意 无论如何,您的代码中的数据库查询 - 重写您的代码,以便它 不这样做。

在避免导入时数据库访问的同时获得这些引用的好处的最佳模式是什么?

一种可能的解决方案是一种代理模式,它返回一个伪类别,它转发所有模型的功能,但在必要之前不访问数据库。我想看看其他人是如何用这种方法或其他解决方案解决这个问题的。

(相关但不同的问题:Django test. Finding data from your production database when running tests?)

最终方法

@kevin-christopher-henry 的方法对我来说效果很好。但是,除了修复这些声明的引用之外,我还不得不延迟从其他代码访问引用。在这里,我发现两种方法很有帮助。

首先,我发现了Python Lazy Object Proxy。这个简单的对象将工厂函数作为输入,延迟执行以生成包装对象。

MAP_OF_THINGS = Proxy(lambda: 
        DOG: ...
        CAT: ...
)

完成同样事情的类似方法是将代码推送到用memoize 装饰的工厂函数中,这样它们只会被执行一次。

注意:我最初尝试使用上面的代理对象作为我对模型对象的延迟访问问题的直接解决方案。然而,尽管是 非常 很好的仿制品,但在对这些对象进行查询和过滤时,我得到了:

TypeError: 'Category' object is not callable

果然,Proxycallable 返回True(尽管文档说这并不能保证它是可调用的)。似乎 Django 查询太聪明了,必然会找到与虚假模型不兼容的东西。

对于您的应用程序,Proxy 可能就足够了。

【问题讨论】:

【参考方案1】:

我自己也遇到过同样的问题,并且同意在这里有一些最佳实践会很棒。

我最终找到了一种基于descriptor protocol 的方法:

class LazyInstance:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        self.instance = None

    def __get__(self, obj, cls):
        if self.instance is None:
            self.instance, _ = cls.objects.get_or_create(*self.args, **self.kwargs)

        return self.instance

然后在我的模型类中我有一些特殊的对象:

class Category(models.Model):
    name = models.CharField()

    DOGS = LazyInstance(name="dogs")
    CATS = LazyInstance(name="cats")

所以在导入时什么都不会发生。第一次访问特殊对象时,会查找相关实例(并在必要时创建)并缓存。

【讨论】:

谢谢。这似乎不支持方法调用,但添加 getattr 可以解决此问题。但是,平等仍然存在问题,所以我想我需要处理特殊方法? code.activestate.com/recipes/496741-object-proxying @JohnLehmann:我不确定您所说的支持方法调用是什么意思。这不是使用代理对象;当你访问 Category.DOGS 时,你会得到一个普通的 Django 模型实例。 知道了!顺便说一句,您的 LazyInstance 需要从 object 继承,否则它会默默地失败。感谢您的回答。 @JohnLehmann:乐于助人。我使用的是 Python 3,您永远不需要从 object 显式继承,但我确信您适合 Python 2。【参考方案2】:

对于模块级变量,您无能为力,因为您无法覆盖它们的访问函数。但是,您可以通过__getattribute__ 对类和实例变量执行此操作。您可以使用它来懒惰地加载您的类别:

class Categories(object):
    _categories = 'DOGS': 'dogs', 'CATS': 'cats'
    def __getattribute__(self, key):
        try:
            return super(Categories, self).__getattribute__(key)
        except AttributeError:
            pass
        try:
            value = load_category(self._categories[key])
        except KeyError:
            raise AttributeError(key)
        setattr(self, key, value)
        return value

Categories = Categories()  # Shadow class with singleton instance

然后您将使用module.Categories.DOGS 而不是module.DOGS。在第一次访问时,会加载并存储类别以供将来查找。

【讨论】:

【参考方案3】:

我在 functools.partial 中使用了lazy_object_proxy(它适用于传递函数但不传递参数),如下所示:

import lazy_object_proxy
from functools import partial

def load_category(name):
  # prepare an argument-less runnable function
  loader = partial(Category.objects.get, name)

  # pass the function to the proxy
  return lazy_object_proxy.Proxy(loader)

DOGS = load_category("dogs")
CATS = load_category("cats")

【讨论】:

以上是关于如何避免 Django 中的导入时数据库访问?的主要内容,如果未能解决你的问题,请参考以下文章

使用 DJANGO_SETTINGS_MODULE 时如何导入设置?

如何在 Django 自定义数据库函数调用周围避免 SQL 中的括号?

如何避免Spark SQL做数据导入时产生大量小文件

Django:扩展用户模型避免数据库连接

使用asp.net插入数据时如何避免数据库中的不同记录?

PHP Laravel:如何在将 xl/csv 导入 mysql 时避免重复数据?