30

ForeignKeydjango 上的 s 具有属性on_delete来指定删除引用对象时的行为。有没有办法为ManyToManyField获得类似的东西?

假设我有以下模型

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

默认行为是级联的,所以如果我删除一个碰巧拥有房子的人,它就会从所有者那里消失(也就是说,很明显,它不再拥有任何房子)。我想要的是,如果一个人是所有者,它就不能被删除。也就是说,我想要on_delete=models.PROTECT. 这可能吗?

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

4

3 回答 3

8

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

我怀疑您担心丢失您拥有的数据。如果您使用 South,您可以轻松地将现有的自动中间表“转换”为显式中间表,或者,您可以创建一个全新的表,然后在删除旧表之前将现有数据迁移到新表。

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

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

考虑到这些表每行都只能保存 3 个整数,即使您有很多房屋和所有者,这也可能是一个非常易于管理的练习。

于 2013-04-30T23:58:50.183 回答
3

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

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

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

想象一下,你有:

TABLE1 和 TABLE 2,它们属于 M2M 关系,其中 TABLE1 具有 ManyToManyField。

我把主要的键给你理解为大写,你需要调整到你想要的。

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

模型.py

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

模型.py

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

视图.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)

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

于 2021-01-24T14:41:00.520 回答
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)
于 2016-03-06T14:20:38.103 回答