ORM-команды для связи many-to-one

Курс по Django: https://stepik.org/a/183363

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

Перейдем в консоль фреймворка Django:

python manage.py shell_plus --print-sql

и прочитаем запись с id=1 из таблицы women:

w = Women.objects.get(pk=1)

В итоге получаем объект класса Women с набором локальных атрибутов в виде значений полей:

w.title  # 'Анджелина Джоли'
w.time_create # datetime.datetime(2023, 6, 15)

И так далее. А что из себя будет представлять атрибут cat, который мы в модели объявляли как внешний ключ? Смотрите, если обратиться к атрибуту cat_id:

w.cat_id  # 1

то увидим значение поля (внешнего ключа) cat_id. Но если обратиться к атрибуту:

w.cat

то в этот момент у нас выполнился еще один SQL-запрос для формирования объекта класса Category. И в этом объекте присутствуют соответствующие значения записи из таблицы category, связанные с полем cat_id из таблицы women. В частности, поле name:

w.cat.name

соответствует строке «Актрисы». Вот так вторичная модель Women связывается с первичной моделью Category и получает соответствующие данные.

Но можно сделать и наоборот, используя первичную модель Category получить все связанные с ней посты из вторичной модели Women. Для этого у любой первичной модели по умолчанию автоматически создается специальное свойство (объект) с именем:

<вторичная модель>_set

И уже с его помощью можно выбирать связанные записи. Давайте сделаем это. Сначала прочитаем запись из таблицы category, например, с id=1:

c = Category.objects.get(pk=1)

чтобы получить ссылку на объект записи первичной модели. А, затем, используя механизм обратного связывания, прочитаем все связанные с данной категорией посты:

c.women_set.all()

Если мы хотим переименовать атрибут women_set, то для этого в классе ForeignKey вторичной модели Women следует дополнительно прописать параметр:

related_name='posts'

Здесь posts – это новое имя атрибута для обратного связывания. Чтобы изменения вступили в силу, нужно выйти из оболочки Django, снова зайти, прочитать категорию и отобразить связанные посты с уже новым именем атрибута:

c = Category.objects.get(pk=1)
c.posts.all()

Разумеется, вместо метода all() мы можем использовать и другие уже известные нам методы, например, filter():

c.posts.filter(is_published=1)

Фильтрация по внешнему ключу

После того, как мы с вами определили поле cat в модели Women, его можно использовать в качестве критерия для отбора записей. Например, так:

Women.objects.filter(cat_id=1)

Получим список всех статей, у которых внешний ключ равен единице. Также мы можем использовать различные фильтры (lookups), например, in для отбора записей с указанными идентификаторами категорий. Причем, записать его можно в двух видах:

Women.objects.filter(cat__in=[1, 2])
Women.objects.filter(cat_id__in=[1, 2])

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

cats = Category.objects.all()

а, затем, подставить их вместо списка:

Women.objects.filter(cat__in=cats)

По аналогии используются все другие lookups фильтры с внешним ключом. Ограничений здесь никаких нет.

Выборка записей по полям связанных моделей

Помимо фильтров (field lookups) мы можем указывать названия полей в связанных таблицах. Для этого параметр следует формировать по правилу:

<первичная модель>__<название поля первичной модели>

Например, вот так можно выбрать все записи из модели Women для определенной категории, используя слаг:

Women.objects.filter(cat__slug='aktrisy')

То есть, здесь мы обращаемся к первичной модели через атрибут cat, который прописан во вторичной модели Women, а затем, через два подчеркивания указываем нужное имя поля первичной модели, по которому отбираются записи уже вторичной модели. На выходе получаем список постов для актрис.

Этот синтаксис немного похож на использование фильтра:

Women.objects.filter(cat_id=1)

Только здесь вместо указания списка идентификаторов рубрик, используется слаг с определенным названием.

Или можно взять другое поле (name) и по нему производить выборку записей из вторичной модели:

Women.objects.filter(cat__name='Певицы')

Мало того, после имени поля можно дополнительно указывать различные фильтры. Например, выберем записи, у которых имя категории содержит букву ‘ы’:

Women.objects.filter(cat__name__contains='ы')

Конечно, это несколько странный, искусственный пример, но он хорошо показывает принцип использования фильтров для полей первичной модели. Если уточнить этот фильтр:

Women.objects.filter(cat__name__contains='цы')

то получим уже записи только по певицам. Или сделать наоборот, выбрать все категории, которые связаны с записями вторичной модели Women, содержащие в заголовке фрагмент строки «ли»:

Category.objects.filter(posts__title__contains='ли')

Обратите внимание, на выходе получим набор из нескольких повторяющихся категорий, каждая из которых соответствует определенной записи из модели Women. Если нужно отобрать только уникальные записи (категории), то дополнительно следует указать метод distinct():

Category.objects.filter(posts__title__contains='ли').distinct()

На этом мы завершим обзор возможностей ORM Django при работе со связанными таблицами. На следующем занятии воспользуемся ими для отображения статей по рубрикам.

Курс по Django: https://stepik.org/a/183363

Видео по теме