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

时间:2012-09-24 14:33:02

标签: python django many-to-many

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

from xxx import B

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

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

  • ID
  • A_ID
  • B_ID

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

3 个答案:

答案 0 :(得分:62)

使用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)

答案 1 :(得分:1)

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

我无法测试对所有场景的影响,到目前为止它对我来说是正常的。

使用Django 1.8及其后续版本#39; _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'

答案 2 :(得分:1)

@ dm03514回答说,通过M2M表添加列确实非常容易 通过模型明确定义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添加一些列,您将 需要对该功能进行猴子补丁。

解决方案

首先,您应该创建将用于以下功能的新版本的功能 修补原始的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. 请记住,这不会影响已在 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)]
    
  2. 请记住,提出的解决方案仅影响Django的表 自动为未定义以下内容的ManyToManyField字段创建 through模型。如果您已经通过模型拥有一些明确的m2m,您将 需要在此处手动添加需要定制的列。

  3. 已修补的create_many_to_many_intermediary_model函数也将适用 INSTALLED_APPS设置中列出的所有第三方应用程序的模型。

  4. 最后但并非最不重要的一点,请记住,如果您将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

干杯

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