Сигнал m2m_changed

Сигнал m2m_changed отправляется при изменении связи ManyToManyField. Он позволяет выполнять дополнительные действия до или после добавления, удаления или очистки связанных объектов. Сигнал принимает несколько параметров: отправитель, экземпляр модели, действие, обратный набор моделей, идентификаторы объектов и флаг инициализации.

Параметры сигнала

Сигнал m2m_changed передает следующие аргументы в функцию-обработчик:

⁅b⁆sender⁅/b⁆ - класс модели, которая содержит ManyToManyField.

⁅b⁆instance⁅/b⁆ - экземпляр модели, для которого изменяется связь.

⁅b⁆action⁅/b⁆ - строка, указывающая тип операции: pre_add, post_add, pre_remove, post_remove, pre_clear или post_clear.

⁅b⁆reverse⁅/b⁆ - булево значение, указывающее, является ли операция обратной (через связанную модель).

⁅b⁆model⁅/b⁆ - класс модели, которая добавляется или удаляется.

⁅b⁆pk_set⁅/b⁆ - множество идентификаторов объектов, которые участвуют в операции.

⁅b⁆using⁅/b⁆ - псевдоним базы данных, на которой выполняется операция.

Синтаксис

pythonfrom django.db.models.signals import m2m_changed
from django.dispatch import receiver

@receiver(m2m_changed, sender=Model.m2m_field.through)
def handler(sender, instance, action, reverse, model, pk_set, using, **kwargs):
  # код обработчика
  pass

Важно указывать sender как промежуточную модель через атрибут through поля ManyToManyField.

Пример

Создадим модели Article и Tag со связью ManyToMany, и добавим обработчик, который выводит информацию о добавлении тегов:

pythonfrom django.db import models
from django.db.models.signals import m2m_changed
from django.dispatch import receiver

class Tag(models.Model):
  name = models.CharField(max_length=50)

  def __str__(self):
    return self.name

class Article(models.Model):
  title = models.CharField(max_length=100)
  tags = models.ManyToManyField(Tag)

  def __str__(self):
    return self.title

@receiver(m2m_changed, sender=Article.tags.through)
def article_tags_changed(sender, instance, action, reverse, model, pk_set, using, **kwargs):
  if action == 'post_add' and pk_set:
    tag_names = Tag.objects.filter(pk__in=pk_set).values_list('name', flat=True)
    print(f"Added tags to article '{instance.title}': {list(tag_names)}")

Теперь при добавлении тегов к статье сработает сигнал и выведет информацию:

pythonfrom myapp.models import Article, Tag

article = Article.objects.create(title="Django signals")
tag1 = Tag.objects.create(name="django")
tag2 = Tag.objects.create(name="python")

article.tags.add(tag1, tag2)

Результат выполнения кода:

python"Added tags to article 'Django signals': ['django', 'python']"

Пример

Используем сигнал для предотвращения удаления последнего тега у статьи. Если у статьи остается только один тег, запретим его удаление:

pythonfrom django.db import models
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from django.core.exceptions import ValidationError

class Tag(models.Model):
  name = models.CharField(max_length=50)

  def __str__(self):
    return self.name

class Article(models.Model):
  title = models.CharField(max_length=100)
  tags = models.ManyToManyField(Tag)

  def __str__(self):
    return self.title

@receiver(m2m_changed, sender=Article.tags.through)
def prevent_last_tag_removal(sender, instance, action, reverse, model, pk_set, using, **kwargs):
  if action == 'pre_remove' and pk_set:
    current_tags = instance.tags.count()
    removing_count = len(pk_set)
    if current_tags - removing_count < 1:
      raise ValidationError("Article must have at least one tag")

При попытке удалить последний тег будет вызвана ошибка:

pythonfrom myapp.models import Article, Tag

article = Article.objects.create(title="My article")
tag = Tag.objects.create(name="important")
article.tags.add(tag)

# Попытка удалить единственный тег
article.tags.remove(tag)

Результат выполнения кода:

python"ValidationError: Article must have at least one tag"

Пример

Обработаем событие очистки всех тегов у статьи с помощью действия pre_clear:

pythonfrom django.db import models
from django.db.models.signals import m2m_changed
from django.dispatch import receiver

class Tag(models.Model):
  name = models.CharField(max_length=50)

  def __str__(self):
    return self.name

class Article(models.Model):
  title = models.CharField(max_length=100)
  tags = models.ManyToManyField(Tag)

  def __str__(self):
    return self.title

@receiver(m2m_changed, sender=Article.tags.through)
def log_tag_clear(sender, instance, action, reverse, model, pk_set, using, **kwargs):
  if action == 'pre_clear':
    tag_count = instance.tags.count()
    print(f"Clearing all {tag_count} tags from article '{instance.title}'")
  elif action == 'post_clear':
    print(f"All tags cleared from article '{instance.title}'")

Теперь при очистке тегов сработает сигнал и выведет информацию:

pythonfrom myapp.models import Article, Tag

article = Article.objects.create(title="Test article")
article.tags.add(Tag.objects.create(name="tag1"))
article.tags.add(Tag.objects.create(name="tag2"))

article.tags.clear()

Результат выполнения кода:

python"Clearing all 2 tags from article 'Test article'"
"All tags cleared from article 'Test article'"

Смотрите также

  • сигнал pre_save,
    который отправляется перед сохранением модели
  • сигнал post_save,
    который отправляется после сохранения модели
  • сигнал pre_delete,
    который отправляется перед удалением модели
  • сигнал post_delete,
    который отправляется после удаления модели