Django的ORM Model.objects.raw()查询导致无限递归错误
Django's ORM Model.objects.raw() query causes an infinite recursion error
我有以下(简化)模型:
class Common(models.Model):
id = models.BigAutoField(primary_key=True, editable=False)
date_created = models.DateTimeField()
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super(Common, self).__init__(*args, **kwargs)
self._initial_data = {}
self.track_fields(
'date_created',
)
def track_fields(self, *args):
for field_name in args:
self._initial_data[field_name] = getattr(self, field_name)
class Directory(Common):
directory_path = models.TextField()
parent_directory = models.ForeignKey('self')
class File(Common):
removed = models.BooleanField()
parent_directory = models.ForeignKey(Directory)
现在,我正在尝试查询这样的一些对象(一个简化的示例,不要注意为什么首先使用 sql):
sql_select_dirs_of_deleted_files = '''
select d.id, directory_path
from directory d
left join file f on f.parent_directory_id = d.id
where f.removed = true and f.id in %s
group by d.id, directory_path
order by directory_path asc
'''
dirs_of_deleted_files = Directory.objects.raw(sql_select_dirs_of_deleted_files, [tuple(file_ids)])
parent_of_top_dir = dirs_of_deleted_files[0].parent_directory
访问dirs_of_deleted_files[0]
导致行
出现无限递归错误
self._initial_data[field_name] = getattr(self, field_name)
在通用模型中。我知道继承和使用 getattr 中的递归问题,但是使用 models.Model.__getattribute__(self, field_name)
在这里似乎没有什么不同。然而,真正起作用的是:
dirs_of_deleted_files = Directory.objects \
.filter(files__in=file_ids, files__removed=True) \
.distinct('id', 'directory_path') \
.order_by('directory_path')
现在,访问 dirs_of_deleted_files[0]
不会导致任何无限递归错误。
Common 模型被其他几个模型继承,显然在不同的地方实例化了很多次,并且 getattr()
从来没有引起任何问题,直到使用这个 Directory.objects.raw()
方法。为什么不?我会怀疑它是 Django 中的错误,但我会保留我的判断。
是的,问题在 Django ticket #22858 中已知。
If you access a field in __init__()
, you should include it in all of your only()
calls to avoid hitting the database again for each item in the queryset.
这是 defer()
文档的常见结果:
Each deferred field will be retrieved from the database if you access that field (one at a time, not all the deferred fields at once).
如果方法 __init__
确实访问任何两个或更多延迟字段(即由 .raw()
、.only()
、.defer()
方法延迟的字段),则可以重现问题然后每次访问都会触发数据库查询,并创建一个新的临时实例,再次需要相同的字段。
问题的最小化示例
class SomeModel(models.Model):
a = models.IntegerField()
b = models.IntegerField()
def __init__(self, *args, **kwargs):
super(SomeModel, self).__init__(*args, **kwargs)
(self.a, self.b)
class Test(TestCase):
def test(self):
SomeModel.objects.create(a=0, b=0)
SomeModel.objects.only('id')[0]
# RuntimeError: maximum recursion depth exceeded
FIX
你可以通过条件field_name in self.__dict__
来修复它,因为如果你不需要加载它,你可能有时不需要跟踪字段。
def track_fields(self, *args):
for field_name in args:
if field_name in self.__dict__:
self._initial_data[field_name] = getattr(self, field_name)
我有以下(简化)模型:
class Common(models.Model):
id = models.BigAutoField(primary_key=True, editable=False)
date_created = models.DateTimeField()
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super(Common, self).__init__(*args, **kwargs)
self._initial_data = {}
self.track_fields(
'date_created',
)
def track_fields(self, *args):
for field_name in args:
self._initial_data[field_name] = getattr(self, field_name)
class Directory(Common):
directory_path = models.TextField()
parent_directory = models.ForeignKey('self')
class File(Common):
removed = models.BooleanField()
parent_directory = models.ForeignKey(Directory)
现在,我正在尝试查询这样的一些对象(一个简化的示例,不要注意为什么首先使用 sql):
sql_select_dirs_of_deleted_files = '''
select d.id, directory_path
from directory d
left join file f on f.parent_directory_id = d.id
where f.removed = true and f.id in %s
group by d.id, directory_path
order by directory_path asc
'''
dirs_of_deleted_files = Directory.objects.raw(sql_select_dirs_of_deleted_files, [tuple(file_ids)])
parent_of_top_dir = dirs_of_deleted_files[0].parent_directory
访问dirs_of_deleted_files[0]
导致行
self._initial_data[field_name] = getattr(self, field_name)
在通用模型中。我知道继承和使用 getattr 中的递归问题,但是使用 models.Model.__getattribute__(self, field_name)
在这里似乎没有什么不同。然而,真正起作用的是:
dirs_of_deleted_files = Directory.objects \
.filter(files__in=file_ids, files__removed=True) \
.distinct('id', 'directory_path') \
.order_by('directory_path')
现在,访问 dirs_of_deleted_files[0]
不会导致任何无限递归错误。
Common 模型被其他几个模型继承,显然在不同的地方实例化了很多次,并且 getattr()
从来没有引起任何问题,直到使用这个 Directory.objects.raw()
方法。为什么不?我会怀疑它是 Django 中的错误,但我会保留我的判断。
是的,问题在 Django ticket #22858 中已知。
If you access a field in
__init__()
, you should include it in all of youronly()
calls to avoid hitting the database again for each item in the queryset.
这是 defer()
文档的常见结果:
Each deferred field will be retrieved from the database if you access that field (one at a time, not all the deferred fields at once).
如果方法 __init__
确实访问任何两个或更多延迟字段(即由 .raw()
、.only()
、.defer()
方法延迟的字段),则可以重现问题然后每次访问都会触发数据库查询,并创建一个新的临时实例,再次需要相同的字段。
问题的最小化示例
class SomeModel(models.Model):
a = models.IntegerField()
b = models.IntegerField()
def __init__(self, *args, **kwargs):
super(SomeModel, self).__init__(*args, **kwargs)
(self.a, self.b)
class Test(TestCase):
def test(self):
SomeModel.objects.create(a=0, b=0)
SomeModel.objects.only('id')[0]
# RuntimeError: maximum recursion depth exceeded
FIX
你可以通过条件field_name in self.__dict__
来修复它,因为如果你不需要加载它,你可能有时不需要跟踪字段。
def track_fields(self, *args):
for field_name in args:
if field_name in self.__dict__:
self._initial_data[field_name] = getattr(self, field_name)