通过自定义 UUIDField 过滤因 Django 3.2 升级而中断
Filtering by custom UUIDField got broken with Django 3.2 upgrade
我有一个最近从 Django 2.2 升级到 3.2 的 Django 项目。在这个项目中,我使用自定义 UUIDField
将 UUID 保存到 MySQL 为 char(36)
,格式如下:12345678-1234-5678-1234-567812345678
.
import uuid
from django.db import models
class UUIDField(models.UUIDField):
"""
Overrides Django UUIDField to store full UUID's including dashes.
"""
def __init__(self, verbose_name=None, **kwargs):
super().__init__(verbose_name, **kwargs)
self.max_length = 36
def get_internal_type(self):
return "CharField"
def get_db_prep_value(self, value, connection, prepared=False):
if value is None:
return None
if not isinstance(value, uuid.UUID):
try:
value = uuid.UUID(value)
except AttributeError:
raise TypeError(self.error_messages['invalid'] % {'value': value})
if connection.features.has_native_uuid_field:
return value
return str(value)
升级后,我发现搜索完整的 UUID 不再有效。如果我只提供 UUID 的第一部分(直到第一个连字符后的第一个字符),它将按预期工作。
Python 3.6.9 (default, Mar 15 2022, 13:55:28)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from foobar.foo.models import Foo
>>>
>>> Foo.objects.all()
<QuerySet [<Foo: Foo object (34c46fe8-caf0-11ec-bdb9-482ae362a4c0)>]>
>>>
>>> Foo.objects.filter(id__icontains='34c46fe8-caf0-11ec-bdb9-482ae362a4c0')
<QuerySet []>
>>>
>>> Foo.objects.filter(id__icontains='34c46fe8-')
<QuerySet [<Foo: Foo object (34c46fe8-caf0-11ec-bdb9-482ae362a4c0)>]>
>>>
>>> Foo.objects.filter(id__icontains='34c46fe8-c')
<QuerySet []>
>>>
我试过 UUIDField
方法,但我似乎无法弄清楚哪里出了问题。这是使用简化模型的 link 到 Gist,我从中获得了上述 shell 示例。
经过相当长的 pdb
调试会话后,我设法找到了问题所在。我希望一路上找到原始的 SQL 片段,但一个名为 WhereNode
的对象引起了我的注意。
> /home/milanb/temp/django_custom_uuid_field/django_playground/lib/python3.6/site-packages/django/db/models/sql/query.py(1399)build_filter()
-> return clause, used_joins if not require_outer else ()
(Pdb) n
--Return--
> /home/milanb/temp/django_custom_uuid_field/django_playground/lib/python3.6/site-packages/django/db/models/sql/query.py(1399)build_filter()->(<WhereNode: (...f9c4f880be0>)>, {'foo_foo'})
-> return clause, used_joins if not require_outer else ()
(Pdb) retval
(<WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>, {'foo_foo'})
(Pdb) pp locals()
{'__return__': (<WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>,
{'foo_foo'}),
'alias': 'foo_foo',
'allow_joins': True,
'allow_many': True,
'arg': 'id__icontains',
'branch_negated': False,
'can_reuse': {'foo_foo'},
'check_filterable': True,
'clause': <WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>,
'col': Col(foo_foo, foo.Foo.id),
'condition': <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>,
'current_negated': False,
'filter_expr': ('id__icontains', '34c46fe8-caf0-11ec-bdb9-482ae362a4c0'),
'join_info': JoinInfo(final_field=<foobar.foo.fields.UUIDField: id>, targets=(<foobar.foo.fields.UUIDField: id>,), opts=<Options for Foo>, joins=['foo_foo'], path=[], transform_function=<function Query.setup_joins.<locals>.final_transformer at 0x7f9c4f89ca60>),
'join_list': ['foo_foo'],
'lookup_type': 'icontains',
'lookups': ['icontains'],
'opts': <Options for Foo>,
'parts': ['id'],
'pre_joins': {},
'reffed_expression': False,
'require_outer': False,
'reuse_with_filtered_relation': False,
'self': <django.db.models.sql.query.Query object at 0x7f9c4f902c18>,
'split_subq': True,
'targets': (<foobar.foo.fields.UUIDField: id>,),
'used_joins': {'foo_foo'},
'value': '34c46fe8-caf0-11ec-bdb9-482ae362a4c0'}
(Pdb) pp clause
<WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>
在这里我注意到了这一行:
'clause': <WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>,
之前不知道什么是django.db.models.lookups.UUIDIContains
对象,直接去Django的源码里找找看。这是 en empty class 由两个混音 IContains
和 UUIDTextMixin
. The latter was the culprit in my case as it removed all hyphens from the lookup value. This was introduced as a fix 组成,我在升级前使用了 2.2 版,以支持没有本机 UUID 类型的数据库后端(例如 MySQL 5.7 在我的例子中)。
修复非常简单,我只需要为我的自定义 UUIDField
.
注册一个没有 UUIDTextMixin
的自定义 UUIDIContains
class
我的新 fields.py
:
import uuid
from django.db import models
from django.db.models.lookups import IContains
class UUIDField(models.UUIDField):
"""
Overrides Django UUIDField to store full UUID's including dashes.
"""
def __init__(self, verbose_name=None, **kwargs):
super().__init__(verbose_name, **kwargs)
self.max_length = 36
def get_internal_type(self):
return "CharField"
def get_db_prep_value(self, value, connection, prepared=False):
if value is None:
return None
if not isinstance(value, uuid.UUID):
try:
value = uuid.UUID(value)
except AttributeError:
raise TypeError(self.error_messages['invalid'] % {'value': value})
if connection.features.has_native_uuid_field:
return value
return str(value)
@UUIDField.register_lookup
class UUIDIContains(IContains):
pass
我有一个最近从 Django 2.2 升级到 3.2 的 Django 项目。在这个项目中,我使用自定义 UUIDField
将 UUID 保存到 MySQL 为 char(36)
,格式如下:12345678-1234-5678-1234-567812345678
.
import uuid
from django.db import models
class UUIDField(models.UUIDField):
"""
Overrides Django UUIDField to store full UUID's including dashes.
"""
def __init__(self, verbose_name=None, **kwargs):
super().__init__(verbose_name, **kwargs)
self.max_length = 36
def get_internal_type(self):
return "CharField"
def get_db_prep_value(self, value, connection, prepared=False):
if value is None:
return None
if not isinstance(value, uuid.UUID):
try:
value = uuid.UUID(value)
except AttributeError:
raise TypeError(self.error_messages['invalid'] % {'value': value})
if connection.features.has_native_uuid_field:
return value
return str(value)
升级后,我发现搜索完整的 UUID 不再有效。如果我只提供 UUID 的第一部分(直到第一个连字符后的第一个字符),它将按预期工作。
Python 3.6.9 (default, Mar 15 2022, 13:55:28)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from foobar.foo.models import Foo
>>>
>>> Foo.objects.all()
<QuerySet [<Foo: Foo object (34c46fe8-caf0-11ec-bdb9-482ae362a4c0)>]>
>>>
>>> Foo.objects.filter(id__icontains='34c46fe8-caf0-11ec-bdb9-482ae362a4c0')
<QuerySet []>
>>>
>>> Foo.objects.filter(id__icontains='34c46fe8-')
<QuerySet [<Foo: Foo object (34c46fe8-caf0-11ec-bdb9-482ae362a4c0)>]>
>>>
>>> Foo.objects.filter(id__icontains='34c46fe8-c')
<QuerySet []>
>>>
我试过 UUIDField
方法,但我似乎无法弄清楚哪里出了问题。这是使用简化模型的 link 到 Gist,我从中获得了上述 shell 示例。
经过相当长的 pdb
调试会话后,我设法找到了问题所在。我希望一路上找到原始的 SQL 片段,但一个名为 WhereNode
的对象引起了我的注意。
> /home/milanb/temp/django_custom_uuid_field/django_playground/lib/python3.6/site-packages/django/db/models/sql/query.py(1399)build_filter()
-> return clause, used_joins if not require_outer else ()
(Pdb) n
--Return--
> /home/milanb/temp/django_custom_uuid_field/django_playground/lib/python3.6/site-packages/django/db/models/sql/query.py(1399)build_filter()->(<WhereNode: (...f9c4f880be0>)>, {'foo_foo'})
-> return clause, used_joins if not require_outer else ()
(Pdb) retval
(<WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>, {'foo_foo'})
(Pdb) pp locals()
{'__return__': (<WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>,
{'foo_foo'}),
'alias': 'foo_foo',
'allow_joins': True,
'allow_many': True,
'arg': 'id__icontains',
'branch_negated': False,
'can_reuse': {'foo_foo'},
'check_filterable': True,
'clause': <WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>,
'col': Col(foo_foo, foo.Foo.id),
'condition': <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>,
'current_negated': False,
'filter_expr': ('id__icontains', '34c46fe8-caf0-11ec-bdb9-482ae362a4c0'),
'join_info': JoinInfo(final_field=<foobar.foo.fields.UUIDField: id>, targets=(<foobar.foo.fields.UUIDField: id>,), opts=<Options for Foo>, joins=['foo_foo'], path=[], transform_function=<function Query.setup_joins.<locals>.final_transformer at 0x7f9c4f89ca60>),
'join_list': ['foo_foo'],
'lookup_type': 'icontains',
'lookups': ['icontains'],
'opts': <Options for Foo>,
'parts': ['id'],
'pre_joins': {},
'reffed_expression': False,
'require_outer': False,
'reuse_with_filtered_relation': False,
'self': <django.db.models.sql.query.Query object at 0x7f9c4f902c18>,
'split_subq': True,
'targets': (<foobar.foo.fields.UUIDField: id>,),
'used_joins': {'foo_foo'},
'value': '34c46fe8-caf0-11ec-bdb9-482ae362a4c0'}
(Pdb) pp clause
<WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>
在这里我注意到了这一行:
'clause': <WhereNode: (AND: <django.db.models.lookups.UUIDIContains object at 0x7f9c4f880be0>)>,
之前不知道什么是django.db.models.lookups.UUIDIContains
对象,直接去Django的源码里找找看。这是 en empty class 由两个混音 IContains
和 UUIDTextMixin
. The latter was the culprit in my case as it removed all hyphens from the lookup value. This was introduced as a fix 组成,我在升级前使用了 2.2 版,以支持没有本机 UUID 类型的数据库后端(例如 MySQL 5.7 在我的例子中)。
修复非常简单,我只需要为我的自定义 UUIDField
.
UUIDTextMixin
的自定义 UUIDIContains
class
我的新 fields.py
:
import uuid
from django.db import models
from django.db.models.lookups import IContains
class UUIDField(models.UUIDField):
"""
Overrides Django UUIDField to store full UUID's including dashes.
"""
def __init__(self, verbose_name=None, **kwargs):
super().__init__(verbose_name, **kwargs)
self.max_length = 36
def get_internal_type(self):
return "CharField"
def get_db_prep_value(self, value, connection, prepared=False):
if value is None:
return None
if not isinstance(value, uuid.UUID):
try:
value = uuid.UUID(value)
except AttributeError:
raise TypeError(self.error_messages['invalid'] % {'value': value})
if connection.features.has_native_uuid_field:
return value
return str(value)
@UUIDField.register_lookup
class UUIDIContains(IContains):
pass