Подтвердить что ты не робот

Django ManyToManyField и on_delete

ForeignKey на django есть атрибут on_delete, чтобы указать поведение при удалении объекта ссылки. Есть ли способ получить что-то подобное для ManyToManyField?

Предположим, что у меня есть следующая модель

class House(models.Model):
    owners = models.ManyToManyField(Person)

Поведение по умолчанию - это каскад, поэтому, если я удалю человека, который имеет собственный дом, он просто исчезнет у владельцев (то есть, очевидно, он больше не владеет никакими домами). Я бы хотел, чтобы, если человек является владельцем, его нельзя удалить. То есть, я хочу on_delete=models.PROTECT. Возможно ли это?

Я знаю, что внутренне ManyToManyField переводится в другую модель с двумя ForeignKey (в этом случае один для дома и один к человеку), поэтому этого должно быть возможно. Любые идеи, как? Я бы не хотел устанавливать атрибут through для новой модели, потому что это приведет к созданию новой таблицы (я бы хотел держите старый).

Изменить: я отслеживал, где django создает подходящую модель m2m:

def create_many_to_many_intermediary_model(field, klass):
    from django.db import models
    # ... 
    # Construct and return the new class.
    return type(name, (models.Model,), {
        'Meta': meta,
        '__module__': klass.__module__,
        from_: models.ForeignKey(klass,
                                 related_name='%s+' % name,
                                 db_tablespace=field.db_tablespace),
        to: models.ForeignKey(to_model,
                              related_name='%s+' % name,
                              db_tablespace=field.db_tablespace)
    })

Соответствующая строка

to: models.ForeignKey(to_model,
                      related_name='%s+' % name,
                      db_tablespace=field.db_tablespace)

Я хотел бы, чтобы это было

to: models.ForeignKey(to_model,
                      related_name='%s+' % name,
                      db_tablespace=field.db_tablespace,
                      on_delete=models.PROTECT)

Любой способ сделать это, кроме обезьяны, патчировать все это и создать новый класс для ManyToManyField?

4b9b3361

Ответ 1

Я думаю, что самая умная вещь - использовать явную сквозную таблицу. Я понимаю, что вы заявили, что предпочли бы не "потому что это приведет к новой таблице (я бы хотел сохранить старый)".

Я подозреваю, что ваша озабоченность связана с потерей данных, которые у вас есть. Если вы используете Юг, вы можете легко "преобразовать" существующую автоматическую промежуточную таблицу в явную или OR, вы можете создать совершенно новую, а затем перенести существующие данные в новую таблицу, прежде чем отбрасывать свой старый.

Оба этих метода объясняются здесь: Добавление "through" стол в поле django и переход с юга?

Учитывая изменение, которое вы хотели бы внести в его определение, я, вероятно, поеду с возможностью создания новой таблицы, а затем переноса данных. Тест, чтобы убедиться, что все ваши данные все еще существуют (и что ваше изменение делает то, что вы хотите), затем отбросьте старую промежуточную таблицу.

Учитывая, что эти таблицы будут содержать только 3 целых числа в строке, это, вероятно, будет очень управляемым упражнением, даже если у вас много домов и владельцев.

Ответ 2

Публикация моего собственного решения по запросу @Andrew Fount. Довольно уродливый взломать только одну строку.

from django.db.models import ManyToManyField
from django.db.models.fields.related import ReverseManyRelatedObjectsDescriptor, add_lazy_relation, create_many_to_many_intermediary_model, RECURSIVE_RELATIONSHIP_CONSTANT
from django.utils import six
from django.utils.functional import curry


def create_many_to_many_protected_intermediary_model(field, klass):
    from django.db import models
    managed = True
    if isinstance(field.rel.to, six.string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
        to_model = field.rel.to
        to = to_model.split('.')[-1]

        def set_managed(field, model, cls):
            field.rel.through._meta.managed = model._meta.managed or cls._meta.managed
        add_lazy_relation(klass, field, to_model, set_managed)
    elif isinstance(field.rel.to, six.string_types):
        to = klass._meta.object_name
        to_model = klass
        managed = klass._meta.managed
    else:
        to = field.rel.to._meta.object_name
        to_model = field.rel.to
        managed = klass._meta.managed or to_model._meta.managed
    name = '%s_%s' % (klass._meta.object_name, field.name)
    if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or to == klass._meta.object_name:
        from_ = 'from_%s' % to.lower()
        to = 'to_%s' % to.lower()
    else:
        from_ = klass._meta.object_name.lower()
        to = to.lower()
    meta = type('Meta', (object,), {
        'db_table': field._get_m2m_db_table(klass._meta),
        'managed': managed,
        'auto_created': klass,
        'app_label': klass._meta.app_label,
        'db_tablespace': klass._meta.db_tablespace,
        'unique_together': (from_, to),
        'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to},
        'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to},
        })
    # Construct and return the new class.
    return type(name, (models.Model,), {
        'Meta': meta,
        '__module__': klass.__module__,
        from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace),

        ### THIS IS THE ONLY LINE CHANGED
        to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace, on_delete=models.PROTECT)
        ### END OF THIS IS THE ONLY LINE CHANGED
    })


class ManyToManyProtectedField(ManyToManyField):
    def contribute_to_class(self, cls, name):
        # To support multiple relations to self, it useful to have a non-None
        # related name on symmetrical relations for internal reasons. The
        # concept doesn't make a lot of sense externally ("you want me to
        # specify *what* on my non-reversible relation?!"), so we set it up
        # automatically. The funky name reduces the chance of an accidental
        # clash.
        if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name):
            self.rel.related_name = "%s_rel_+" % name

        super(ManyToManyField, self).contribute_to_class(cls, name)

        # 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 self.rel.through and not cls._meta.abstract and not cls._meta.swapped:
            self.rel.through = create_many_to_many_protected_intermediary_model(self, cls)

        # Add the descriptor for the m2m relation
        setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))

        # Set up the accessor for the m2m table name for the relation
        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)

        # Populate some necessary rel arguments so that cross-app relations
        # work correctly.
        if isinstance(self.rel.through, six.string_types):
            def resolve_through_model(field, model, cls):
                field.rel.through = model
            add_lazy_relation(cls, self, self.rel.through, resolve_through_model)