Форум программистов, компьютерный форум, киберфорум
Наши страницы
netBool
Войти
Регистрация
Восстановить пароль
Оценить эту запись

Расширенные возможности ManyToMany в Django

Запись от netBool размещена 24.03.2018 в 16:57

В свое время меня интересовали возможности нестандартные возможности ManyToMany в Джанго с использованием through (так сказать, промежуточных классов). И в этом обзоре я хочу осветить именно этот вопрос.

На эту тему я мало нашел информации в сети, особенно русскоязычной. Но есть классный перевод на хабре, к сожалению, не очень читабельный из-за неуместного использования тегов. Из-за этого чтиво ее превратилось в квест по дешифровке примеров кода из хтмл-абракадабры, и я решил привести его часть в более менее читабельный вид тут.

Итак. В статье автор предлагает рассмотреть случай, когда приложение отслеживает группы, в которых некоторые музыканты принимали участие.

Отношение между музыкантом и группами, участником которых он являлся или является, будут вида многие-к-многим, поэтому для его представления вы можете использовать ManyToManyField. Однако так же вас может интересовать большое количество деталей, таких как дата вступления в группу или причина, по которой музыкант ее покинул:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#класс музыкант:
class Person(models.Model):
     name = models.CharField(max_length=128)
 
     def __unicode__(self):
          return self.name
 
#класс группа:
class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')
 
    def __unicode__(self):
        return self.name
 
class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)
При создании промежуточной модели вы явно указываете внешние ключи для моделей, которые вовлечены в отношение вида многие-к-многим. Это однозначное объявление и определяет способы взаимодействия объектов.

Существует несколько ограничений, касающихся промежуточных моделей:
  1. Ваша промежуточная модель должна содержать один и только один внешний ключ к целевой модели (в нашем примере это Person), иначе будет сгенерирована ошибка проверки (validation error).
  2. Ваша промежуточная модель должна содержать один и только один внешний ключ к исходной модели (в нашем примере это Group), иначе будет сгенерирована ошибка проверки (validation error).
  3. Единственным исключением является модель, которая имеет отношение многие-к-многим сама с собой и ссылается на себя с помощью промежуточной модели. В этом случае разрешается использовать два внешних ключа к одной и той же модели, но они буду рассматриваться как две различные части в отношениях вида многие-к-многим.
  4. Для определения модели, которая ссылается сама на себя с помощью промежуточной модели, вы должны использовать symmetrical=False (подробнее в справке по полям моделей).

Теперь, после того как вы настроили ManyToManyField для взаимодействия с вашей промежуточной моделью, вы можете начать создание нескольких отношений вида многие-к-многим. Делается это путем создания экземпляров вашей промежуточной модели:

Python
1
2
3
4
5
6
7
8
9
ringo = Person.objects.create(name="Ringo Starr")
paul = Person.objects.create(name="Paul McCartney")
beatles = Group.objects.create(name="The Beatles")
m1 = Membership(person=ringo, 
   group=beatles,
   date_joined=date(1962, 8, 16),
   invite_reason= "Needed a new drummer.")
m1.save()
print(beatles.members.all())
Цитата:
[<Person: Ringo Starr>]
Python
1
print(ringo.group_set.all())
Цитата:
[<Group: The Beatles>]
Python
1
2
3
4
m2 = Membership.objects.create(person=paul, group=beatles,
    date_joined=date(1960, 8, 1),
    invite_reason= "Wanted to form a band.")
print(beatles.members.all())
Цитата:
[<Person: Ringo Starr>, <Person: Paul McCartney>]
Не очень похоже на обычные поля: вы не можете использовать методы add и create, а также операцию присваивания (например, beatles.members = [...]) для определения отношений:

Python
1
2
3
4
5
6
# это не будет работать:
beatles.members.add(john)
# NEITHER WILL THIS
beatles.members.create(name="George Harrison")
# AND NEITHER WILL THIS
beatles.members = [john, paul, ringo, george]
Почему? Вы не можете просто создать отношение между Person и Group, поэтому вы должны указать все детали отношения, требуемые промежуточной моделью Membership. Простые методы add, create и присваивание не предоставляют возможности определить дополнительные детали. Поэтому они отключены в отношения вида многие-к-многим с использованием промежуточной модели. Единственный вариант создания данного вида отношений, заключается в создании экземпляров промежуточной модели.

Метод remove отключен по этим же причинам. Однако вы можете использовать метод clear() для удаления всех отношений вида многие-к-многим отношений экземпляра:
Python
1
beatles.members.clear()
Итак, вы создали отношения вида многие-к-многим путем создания экземпляров промежуточной модели, теперь вы можете оформлять запросы. Так же как и и в случае с нормальным вариантом отношений, вы можете составлять запросы, используя атрибуты модели:
Python
1
2
#ищет все группы с members, содержащей музыкантов (Person), с именем начинающимся на 'Paul'
Group.objects.filter(members__name__startswith='Paul')
Так как вы используете промежуточную модель, вы также можете оформлять запросы, используя атрибуты модели-посредника:
Python
1
2
3
4
# ищет всех музыкантов, которые присоединились к группе The Beatles после 1.1.1961
Person.objects.filter(
    group__name='The Beatles',
    membership__date_joined__gt=date(1961,1,1))
Цитата:
[<Person: Ringo Starr]
Вы можете получить данные непосредственно из модели Membership:

Python
1
2
ringos_membership = Membership.objects.get(group=beatles, person=ringo)
ringos_membership.date_joined
Цитата:
datetime.date(1962, 8, 16)
Python
1
ringos_membership.invite_reason
Цитата:
u'Needed a new drummer.'
Другой способ - используя обратную связь объекта модели Person:

Python
1
2
ringos_membership = ringo.membership_set.get(group=beatles)
ringos_membership.date_joined
Цитата:
datetime.date(1962, 8, 16)
Python
1
ringos_membership.invite_reason
Цитата:
u'Needed a new drummer.'
Вотс. В принципе этот пример можно найти и в оф документации. Но пойдем дальше. Возьмем тот самый случай с models.ManyToManyField('self',symmetrical=False):


Несимметричные m2m


(пример со англоязычной ветки стэковерфлоу):
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Author(models.Model):
    user = models.OneToOneField(User, parent_link=True)
    introduction = models.TextField(blank=True)
    pictures = models.ManyToManyField('Graphic', related_name='users', null=True)
    organizations = models.ManyToManyField('Organization', related_name='members')
    expertise = models.ManyToManyField('Tag', related_name='experts', null=True)
    interests = models.ManyToManyField('Tag', related_name='interested_in', null=True)
    saved_articles = models.ManyToManyField(Article, related_name='favorited_by', null=True, through='SavedArticles')
    authors_followed = models.ManytoManyField('self', related_name='following_authors', null=True, through='FollowedAuthors', symmetrical=False)
 
 
class FollowedAuthors(models.Model):
    author = models.ForeignKey(Author)
    trustee = models.ForeignKey(Author)
    notes = models.TextField()
    tags = models.ManyToManyField('Tag')
Как видно здесь authors_followed объявлен как m2m c symmetrical=False. Что ж. С ними можно работать так:
Python
1
MyAuthor.following_authors.all()
вернет авторов, следующих за MyAuthor
Python
1
MyAuthor.author_followed.all()
вернет авторов, за которыми следит MyAuthor

Так же хочу обратить внимание на возможную необходимость объявить on_delete=models.CASCADE в m2m поле, как тут:

Python
1
2
3
class Relationship(models.Model):
    from_person = models.ForeignKey(User, on_delete=models.CASCADE, related_name="from_person")
    to_person = models.ForeignKey(User, on_delete=models.CASCADE, related_name="to_person")
Это может пригодиться, если вы хотите оборвать все связи при удалении персоны, например

Кстати интересен пример выборки к нему:
Python
1
User.objects.filter(from_person__from_person__in=request.user.friends.all(), from_person__to_person=request.user)
Неплохо, неправда ли?
Размещено в Django
Просмотров 592 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru