mongoengine ReferenceField 的奇怪问题

3 投票
1 回答
1946 浏览
提问于 2025-04-17 07:56

这是一个让人困惑的问题,甚至连名字都很难起,更别提描述了。我先说说基本情况,然后再提供一些可能相关的背景信息。

考虑两个mongoengine文档模型:

class Bar(Document):
    # ...
    # field definitions
    # ...
    def bar_func(self):
        pass  # ...or some arbitrary code


class Foo(Document):
    bar = ReferenceField(Bar)

在我们的生产服务器上,下面的代码不一致地出现了一个AttributeError错误:

# Assume foo_id references a valid Foo document in Mongo
# and that its 'bar' reference is to a valid Bar document.
foo = Foo.objects.with_id(foo_id)
foo.bar.bar_func()  # <-- AttributeError on 'bar_func'

如果我在错误发生的位置之前放入调试代码,评估type(foo.bar)的结果是一个字符串,显示为<class 'bson.dbref.DBRef'>。显然,DBRef没有bar_func这个属性,但为什么返回的是DBRef而不是Bar的实例呢?

进一步的调试代码显示,在mongoengine/fields.py中的ReferenceField.__get__函数里,以下条件失败了:

if isinstance(value, (pymongo.dbref.DBRef)):
        value = _get_db().dereference(value)

但是(pymongo.dbref.DBRef)实际上是bson.dbref.DBRef,这似乎和type(foo.bar)是一样的!为什么isinstance会失败呢?

这时候事情变得真的奇怪了:

id(type(foo.bar)) == id(bson.dbref.DBRef)  # <-- Evaluates to False!

换句话说,type(foo.bar)得到的是一个不同的 bson.dbref.DBRef,而不是直接引用bson.dbref.DBRef得到的那个。实际上,检查这两种类型的__dict__会发现它们的函数和属性在内存中的位置是不同的。

注意:为了方便起见,我将type(foo.bar)返回的类型称为fooDBRef,以便与直接引用的bson.dbref.DBRef区分开。

为了进一步调试,我修改了DBRef的代码,添加了一个元类,用来在创建DBRef类型时检查系统模块,并将这些模块的ID存储在DBRef的一个额外类属性中。结果显示,当fooDBRef被创建时,存在的模块集合与创建裸bson.dbref.DBRef类型时存在的模块集合是完全不同的。一个的所有模块ID与另一个的所有模块ID都不同。

一些可能相关的因素:

  • 出现这个错误的服务器在Apache下运行mod_wsgi。
  • 该服务器在wsgi下运行两个不同的Django网站(称为site_asite_b)。
  • Foo在site_a.foo_app.models中定义,而Bar在site_b.bar_app.models中定义。
  • site_a的settings.py中在INSTALLED_APPS里包含了site_b.bar_app
  • 产生错误的请求由site_a处理。
  • 在创建fooDBRef时,sys.modules中有site_b.*模块,但没有site_a.*模块。对于bson.dbref.DBRef则正好相反。
  • httpd reload后,错误有时会暂时消失,但在0到10次尝试内又会返回。

有没有人能帮我弄清楚为什么fooDBRefbson.dbref.DBRef会不同呢?

1 个回答

4

你是在用mod_wsgi的嵌入模式还是守护进程模式呢?如果你在用守护进程模式的话,是不是把每个网站都分配给不同的守护进程组,然后强制应用在主Python解释器中运行呢?

可能会出现mongodb的Python客户端模块在Python子解释器中运行不正常的情况,尤其是当这个模块在同一个进程的不同子解释器中同时使用时。

所以,你可能需要使用WSGIDaemonProcess/WSGIProcessGroup把每个网站都放在不同的守护进程组里,然后通过WSGIApplicationGroup强制使用主Python解释器,参数设置为'%{GLOBAL}'。

需要注意的是,当你强制两个网站都使用主解释器时,就不能再使用嵌入模式,也不能让它们在同一个守护进程组中运行。这就是为什么你需要让每个网站都在不同的守护进程组中运行的原因。

撰写回答