[Django]-Django ManyToManyField and on_delete

9👍

I think the smartest thing to do is use an explicit through table. I realise that you’ve stated you would prefer not to “because this would result in a new table (I’d like to keep the old one).”

I suspect your concern is over losing the data you have. If you’re using South, you can easily “convert” your existing, automatic intermediate table to an explicit one OR, you can create a completely new one, then migrate your existing data to the new table before dropping your old one.

Both of these methods are explained here: Adding a "through" table to django field and migrating with South?

Considering the change you’d like to make to its definition, I’d probably go with the option of creating a new table, then migrating your data over. Test to make sure all your data is still there (and that your change does what you want), then drop the old intermediate table.

Considering that these tables will both only hold 3 integers per row, this is likely to be a very manageable exercise even if you have a lot of houses and owners.

4👍

If I understand you want, this is similar to what I need some time ago.

Your problem: you need to protect a record that is used in another table from accidental deletion.

I solved it from this way (tested on Django 2 and Django 3).

Imagine, you have:

TABLE1 and TABLE 2, and they are under M2M relationship where TABLE1 has ManyToManyField.

I put the main keys to you understand at uppercase, you will need to adjust to what you want.

Look at views.py that use the exists() method and rise the exception are crucial.

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)

That is a ugly solution, but it works.

2👍

Posting my own solution as requested by @Andrew Fount. Quite an ugly hack just to change a single line.

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)
👤Clash

Leave a comment