mongoengine ReferenceField 的奇怪问题
这是一个让人困惑的问题,甚至连名字都很难起,更别提描述了。我先说说基本情况,然后再提供一些可能相关的背景信息。
考虑两个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_a
和site_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次尝试内又会返回。
有没有人能帮我弄清楚为什么fooDBRef
和bson.dbref.DBRef
会不同呢?
1 个回答
你是在用mod_wsgi的嵌入模式还是守护进程模式呢?如果你在用守护进程模式的话,是不是把每个网站都分配给不同的守护进程组,然后强制应用在主Python解释器中运行呢?
可能会出现mongodb的Python客户端模块在Python子解释器中运行不正常的情况,尤其是当这个模块在同一个进程的不同子解释器中同时使用时。
所以,你可能需要使用WSGIDaemonProcess/WSGIProcessGroup把每个网站都放在不同的守护进程组里,然后通过WSGIApplicationGroup强制使用主Python解释器,参数设置为'%{GLOBAL}'。
需要注意的是,当你强制两个网站都使用主解释器时,就不能再使用嵌入模式,也不能让它们在同一个守护进程组中运行。这就是为什么你需要让每个网站都在不同的守护进程组中运行的原因。