46

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

from xxx import B

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

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

  • ID
  • 援助
  • 投标

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

4

3 回答 3

105

使用 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)
于 2012-09-24T14:36:09.290 回答
3

在幕后,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'
于 2016-09-16T07:39:02.437 回答
3

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

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

在我的例子中,我想在 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()

为了创建这个隐式模型 create_many_to_many_intermediary_model(),Django 使用该函数构造新类,该类继承自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__.pyDjango 项目文件旁边的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 中创建的 m2m 表,因此,如果您在已经将ManyToManyField字段迁移到 db 的项目中引入此解决方案,则需要准备自定义迁移以添加您的自定义在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 through 模型,您将需要手动添加您的自定义需求列。

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

  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

干杯

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

于 2020-02-26T20:12:44.907 回答