web-dev-qa-db-ja.com

ManyToManyテーブルに列を追加する方法(Django)

Django Bookの例から、次のようにモデルを作成するかどうかがわかります。

from xxx import B

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

Djangoは、3つの列を持つテーブルAを超えて新しいテーブル(A_B)を作成します。

  • id
  • a_id
  • 入札

しかし、今度はテーブルA_Bに新しい列を追加したいので、通常のSQLを使用すれば非常に簡単ですが、今では誰でも方法を手伝ってくれるでしょうか。この本に役立つ情報はありません。

31
Wei Lin

Djangoを使用することも非常に簡単です!throughを使用して、独自のmanytomany中間テーブルを定義できます

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)
67
dm03514

内部では、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'
2
ajaest

@ dm03514が回答したように、モデルを介してM2Mを明示的に定義し、そこに目的のフィールドを追加することで、M2Mテーブルに列を追加するのは非常に簡単です。

ただし、すべてのm2mテーブルに列を追加する場合は、モデルを通じてM2Mを定義する必要があるため、このようなアプローチでは不十分です。プロジェクト全体で定義されているすべてのManyToManyFieldについて。

私の場合、Djangoが「内部」を生成するすべてのM2Mテーブルに「作成された」タイムスタンプ列を追加したかったプロジェクトで使用されるすべてのManyToManyFieldフィールドに個別のモデルを定義する必要はありませんでした。

前書き

Django=は、起動時にモデルをスキャンしますが、明示的に定義されていないManyToManyFieldごとに暗黙的なthroughモデルを自動的に作成します。

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. これは、過去にデータベースに作成されたm2mテーブルに影響を与えないことを忘れないでください。このソリューションをすでに持っているプロジェクトに導入する場合ManyToManyFieldフィールドをdbに移行する場合、モンキーパッチの前に作成されたテーブルにカスタム列を追加するカスタム移行を準備する必要があります。以下に提供されるサンプルの移行:)

    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フィールドに対して自動的に作成するテーブルにのみ影響することに注意してくださいmodel。モデルを介した明示的な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

乾杯

私は誰かがこの答えが役に立てば幸いです:)

0
Krzysiek