正如@dm03514 已经回答的那样,通过
通过模型明确定义 M2M 并在其中添加所需的字段。
但是,如果您想在所有 m2m 表中添加一些列 - 这种方法
还不够,因为它需要通过以下方式定义 M2M
已在整个项目中定义的所有 ManyToManyField 的模型。
在我的例子中,我想向所有 M2M 表添加一个“创建”时间戳列
Django 生成“幕后”,无需单独定义
项目中使用的每个 ManyToManyField 字段的模型。我想出了一个
整洁的解决方案如下所示。干杯!
简介
当 Django 在启动时扫描你的模型时,它会自动创建一个隐式的
通过未明确定义的每个 ManyToManyField 的模型。
class ManyToManyField(RelatedField):
# (...)
def contribute_to_class(self, cls, name, **kwargs):
# (...)
super().contribute_to_class(cls, name, **kwargs)
# The intermediate m2m model is not auto created if:
# 1) There is a manually specified intermediate, or
# 2) The class owning the m2m field is abstract.
# 3) The class owning the m2m field has been swapped out.
if not cls._meta.abstract:
if self.remote_field.through:
def resolve_through_model(_, model, field):
field.remote_field.through = model
lazy_related_operation(resolve_through_model, cls, self.remote_field.through, field=self)
elif not cls._meta.swapped:
self.remote_field.through = create_many_to_many_intermediary_model(self, cls)
来源:ManyToManyField.contribute_to_class()
为了创建这个隐式模型,Django 使用
create_many_to_many_intermediary_model() 函数,构造新类
继承自models.Model 并包含指向两侧的外键
M2M 关系。来源:django.db.models.fields.related.create_many_to_many_intermediary_model()
为了通过表格向所有自动生成的 M2M 添加一些列,您将
需要monkeypatch这个函数。
解决办法
首先,您应该创建将用于
修补原始的 Django 功能。为此,只需复制函数的代码
来自 Django 源并将所需的字段添加到它返回的类中:
# For example in: <project_root>/lib/monkeypatching/custom_create_m2m_model.py
def create_many_to_many_intermediary_model(field, klass):
# (...)
return type(name, (models.Model,), {
'Meta': meta,
'__module__': klass.__module__,
from_: models.ForeignKey(
klass,
related_name='%s+' % name,
db_tablespace=field.db_tablespace,
db_constraint=field.remote_field.db_constraint,
on_delete=CASCADE,
),
to: models.ForeignKey(
to_model,
related_name='%s+' % name,
db_tablespace=field.db_tablespace,
db_constraint=field.remote_field.db_constraint,
on_delete=CASCADE,
),
# Add your custom-need fields here:
'created': models.DateTimeField(
auto_now_add=True,
verbose_name='Created (UTC)',
),
})
那么你应该将补丁逻辑包含在一个单独的函数中:
# For example in: <project_root>/lib/monkeypatching/patches.py
def django_m2m_intermediary_model_monkeypatch():
""" We monkey patch function responsible for creation of intermediary m2m
models in order to inject there a "created" timestamp.
"""
from django.db.models.fields import related
from lib.monkeypatching.custom_create_m2m_model import create_many_to_many_intermediary_model
setattr(
related,
'create_many_to_many_intermediary_model',
create_many_to_many_intermediary_model
)
最后你必须在 Django 启动之前执行补丁。将这样的代码放入
__init__.py 位于您的 Django 项目旁边的文件 settings.py 文件:
# <project_root>/<project_name>/__init__.py
from lib.monkeypatching.patches import django_m2m_intermediary_model_monkeypatch
django_m2m_intermediary_model_monkeypatch()
其他几件事值得一提
-
请记住,这不会影响已在
db 过去,所以如果你在一个项目中引入这个解决方案,
已经将ManyToManyField 字段迁移到数据库,您需要准备一个
自定义迁移,将您的自定义列添加到表中
在monkeypatch之前创建。下面提供了示例迁移:)
from django.db import migrations
def auto_created_m2m_fields(_models):
""" Retrieves M2M fields from provided models but only those that have auto
created intermediary models (not user-defined through models).
"""
for model in _models:
for field in model._meta.get_fields():
if (
isinstance(field, models.ManyToManyField)
and field.remote_field.through._meta.auto_created
):
yield field
def add_created_to_m2m_tables(apps, schema_editor):
# Exclude proxy models that don't have separate tables in db
selected_models = [
model for model in apps.get_models()
if not model._meta.proxy
]
# Select only m2m fields that have auto created intermediary models and then
# retrieve m2m intermediary db tables
tables = [
field.remote_field.through._meta.db_table
for field in auto_created_m2m_fields(selected_models)
]
for table_name in tables:
schema_editor.execute(
f'ALTER TABLE {table_name} ADD COLUMN IF NOT EXISTS created '
'timestamp with time zone NOT NULL DEFAULT now()',
)
class Migration(migrations.Migration):
dependencies = []
operations = [migrations.RunPython(add_created_to_m2m_tables)]
请记住,提出的解决方案只影响 Django
为未定义的 ManyToManyField 字段自动创建
through 模特。如果您已经有一些明确的 m2m through 模型,您将
需要手动添加您的自定义需求列。
修补后的create_many_to_many_intermediary_model 函数也将适用
到您的INSTALLED_APPS 设置中列出的所有第 3 方应用的型号。
最后但同样重要的是,请记住,如果您升级 Django 版本的原始版本
修补函数的源代码可能会更改 (!)。设置一个是个好主意
简单的单元测试,如果将来发生这种情况,会向您发出警告。
为此修改修补函数以保存原始 Django 函数:
# For example in: <project_root>/lib/monkeypatching/patches.py
def django_m2m_intermediary_model_monkeypatch():
""" We monkey patch function responsible for creation of intermediary m2m
models in order to inject there a "created" timestamp.
"""
from django.db.models.fields import related
from lib.monkeypatching.custom_create_m2m_model import create_many_to_many_intermediary_model
# Save the original Django function for test
original_function = related.create_many_to_many_intermediary_model
setattr(
create_many_to_many_intermediary_model,
'_original_django_function',
original_function
)
# Patch django function with our version of this function
setattr(
related,
'create_many_to_many_intermediary_model',
create_many_to_many_intermediary_model
)
计算原始Django函数源代码的hash并准备
检查它是否仍与您修补时相同的测试:
def _hash_source_code(_obj):
from inspect import getsourcelines
from hashlib import md5
source_code = ''.join(getsourcelines(_obj)[0])
return md5(source_code.encode()).hexdigest()
def test_original_create_many_to_many_intermediary_model():
""" This test checks whether the original Django function that has been
patched did not changed. The hash of function source code is compared
and if it does not match original hash, that means that Django version
could have been upgraded and patched function could have changed.
"""
from django.db.models.fields.related import create_many_to_many_intermediary_model
original_function_md5_hash = '69d8cea3ce9640f64ce7b1df1c0934b8' # hash obtained before patching (Django 2.0.3)
original_function = getattr(
create_many_to_many_intermediary_model,
'_original_django_function',
None
)
assert original_function
assert _hash_source_code(original_function) == original_function_md5_hash
干杯
我希望有人会发现这个答案很有用:)