如何在 ManyToMany 表中添加列(Django)

新手上路,请多包涵

从 Django Book 的示例中,我了解到我是否按如下方式创建模型:

 from xxx import B

class A(models.Model):
    b = ManyToManyField(B)

Django 将在表 A 之外创建一个新表 (A_B),该表具有三列:

  • ID
  • 援助
  • 出价

但是现在我想在表A_B中添加一个新列,这样如果我使用普通的SQL会很容易,但是现在有人可以帮我怎么做吗?我在这本书中找不到任何有用的信息。

原文由 Wei Lin 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 907
2 个回答

使用 django 也非常容易!您可以使用 through 来定义您自己的多对多中间表

文档 提供了一个解决您的问题的示例:

 Extra fields on many-to-many relationships

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __unicode__(self):
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

    def __unicode__(self):
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

原文由 dm03514 发布,翻译遵循 CC BY-SA 4.0 许可协议

正如 @dm03514 回答的那样,通过模型明确定义 M2M 并在其中添加所需的字段,将列添加到 M2M 表确实非常容易。

但是,如果您想向所有 m2m 表添加一些列 - 这种方法是不够 的,因为它需要通过模型为所有 ManyToManyField 定义 M2M 已经在整个项目中定义.

在我的例子中,我想在 Django 生成的所有 M2M 表中添加一个“创建的”时间戳列, 而无需为项目中使用的每个 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 启动之前,您必须执行修补。将此类代码放入位于 Django 项目旁边的 __init__.py 文件 settings.py 文件中:

 # <project_root>/<project_name>/__init__.py
from lib.monkeypatching.patches import django_m2m_intermediary_model_monkeypatch
django_m2m_intermediary_model_monkeypatch()

其他一些值得一提的事情

  1. 请记住,这不会影响过去在数据库中创建的 m2m 表,因此如果您在已经将 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)]

  1. 请记住, 所提供的解决方案仅影响 Django 自动为 ManyToManyField 字段创建的表,这些字段未定义 through 模型。如果您已经通过模型有一些明确的 m2m,您将需要在那里手动添加您的自定义需求列。

  2. 修补后的 create_many_to_many_intermediary_model 功能也将适用于你的 INSTALLED_APPS 设置中列出的所有第3方应用程序的模型。

  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 函数源代码的哈希值,并准备一个测试来检查它是否仍然与修补时相同:

 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

干杯

我希望有人会发现这个答案有用:)

原文由 Krzysiek 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题