django ManyToManyField和on_delete

时间:2013-03-08 20:50:18

标签: django django-models

django上的

ForeignKey具有属性on_delete,用于指定删除引用对象时的行为。有没有办法为ManyToManyField获得类似的内容?

假设我有以下模型

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

默认行为是级联,所以如果我删除碰巧拥有房子的人,它就会从所有者身上消失(显然,它不再拥有任何房屋)。我想要的是,如果某人是所有者,则无法将其删除。也就是说,我想要on_delete=models.PROTECT。这可能吗?

我知道内部ManyToManyField被翻译成另一个模型,其中有两个ForeignKey s(在这种情况下是一个到一个房子,一个到一个人),所以应该可以实现这一点。任何想法如何?我想避免将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创建一个新类之外,还有什么方法可以做到这一点?

3 个答案:

答案 0 :(得分:5)

我认为最明智的做法是使用显式的直通表。我意识到你已经说过你不愿意“因为这会产生一个新表格(我想保留旧表格)。”

我怀疑您关注的是丢失了您的数据。如果您正在使用South,则可以轻松地将现有的自动中间表“转换”为显式的OR,您可以创建一个全新的,然后将现有数据迁移到新表中,然后再删除旧表。

这两种方法都在这里解释:Adding a "through" table to django field and migrating with South?

考虑到您要对其定义所做的更改,我可能会选择创建新表,然后迁移您的数据。测试以确保所有数据仍然存在(并且您的更改符合您的要求),然后删除旧的中间表。

考虑到这些表每行只能容纳3个整数,即使你拥有很多房屋和业主,这也很可能是一个非常易于管理的工作。

答案 1 :(得分:1)

根据@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's 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)

答案 2 :(得分:0)

如果我理解你想要的,这和我前段时间需要的类似。

您的问题:您需要保护另一个表中使用的记录不被意外删除。

我通过这种方式解决了它(在 Django 2 和 Django 3 上测试)。

想象一下,你有:

TABLE1 和 TABLE 2,它们是 M2M 关系,其中 TABLE1 有 ManyToManyField。

我把主要的键都写成大写,你需要调整到你想要的。

查看使用exists() 方法并引发异常的views.py 至关重要。

models.py

class TABLE1(models.Model):
    FIELD_M2M = models.ManyToManyField(
        TABLE2,
        blank=False,
        related_name='FIELD_M2M',
    )
#put here your code

models.py

class TABLE2(models.Model):
#Put here your code

views.py

# Delete
@login_required
def delete(request, pk=None):
    try:  # Delete register selected
        if TABLE1.objects.filter(FIELD_M2M=pk).exists():
            raise IntegrityError
        register_to_delete = get_object_or_404(TABLE2, pk=pk)
        # register_to_delete.register_to_delete.clear() // Uncomment this, if you need broken relationship M2M before delete
        register_to_delete.delete()
    except IntegrityError:
        message = "The register couldn't be deleted!"
        messages.info(request, message)

这是一个丑陋的解决方案,但它有效。