如何在多对多表中添加列(Django)

Posted

技术标签:

【中文标题】如何在多对多表中添加列(Django)【英文标题】:How to add column in ManyToMany Table (Django) 【发布时间】:2012-09-16 00:54:38 【问题描述】:

从 Django Book 的例子中,我明白如果我创建模型如下:

from xxx import B

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

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

身份证 a_id b_id

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

【问题讨论】:

表格A_B 的目的仅仅是说明哪些B 模型映射到哪些A 模型,反之亦然。您确定这是完成您需要做的事情的最佳方式吗? Django's ManyToMany Relationship with Additional Fields的可能重复 【参考方案1】:

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

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

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)

【讨论】:

确保不要忘记在新表名 ('Membership') 上添加单引号,这样编译器就不会在表名被进一步声明时报错。 @dm03514, through='Membership' ,它没有在数据库中创建表。请帮助我。 现有的 ManyToMany 字段不能通过添加或删除 'through=' 来更改,根据我刚刚遇到的迁移异常。 绕过“M2M 字段无法更改”异常:***.com/questions/26927705/…【参考方案2】:

在底层,Django 会自动创建一个直通模型。可以修改此自动模型外键列名称。

我无法测试所有场景的含义,到目前为止它对我来说都可以正常工作。

使用 Django 1.8 及更高版本的_meta api:

class Person(models.Model):
    pass

class Group(models.Model):
    members = models.ManyToManyField(Person)

Group.members.through._meta.get_field('person').column = 'alt_person_id'
Group.members.through._meta.get_field('group' ).column =  'alt_group_id'

# Prior to Django 1.8 _meta can also be used, but is more hackish than this
Group.members.through.person.field.column = 'alt_person_id'
Group.members.through.group .field.column =  'alt_group_id'

【讨论】:

当数据库是物理的时,虚拟是什么意思?如果表已经存在怎么办? 嗯,也许虚拟不是最好的术语。我的意思是自动和隐藏。我会改变它。如果模型存在,我不知道 Django 应用哪个策略。【参考方案3】:

正如@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

干杯

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

【讨论】:

以上是关于如何在多对多表中添加列(Django)的主要内容,如果未能解决你的问题,请参考以下文章

web框架开发-Django模型层-多表操作

创建多对多表关系的三种方式

在多对多 SQL 表中查找数据关系或图形

如何从多对多表中选择一对一关系

Flask-msearch python ,在多对多表上添加 msearch

Django之模型层-多表操作