Организация связей между таблицами "ОДИН-КО-МНОГИМ"
Рассмотрим организацию связи между таблицами БД «один-ко-многим» или «многие-к-одному», при которой одна главная сущность может быть связаны с несколькими зависимыми сущностями.
Приведем несколько примеров таких связей:
- одна компания, которая выпускает множество видов товаров;
- один автомобиль, который состоит из множества составных частей;
- одна гостиница, которая имеет множество комнат с разными характеристиками;
- одна книга, у которой несколько авторов;
- один город, который имеет множество улиц;
- одна улица, которая имеет множество домов, и т. п.
Создадим новое приложение в нашем проекте:
python manage.py startapp onetomany
Покажем, как можно связать две таблицы в БД через связанные модели на примере «одна компания - множество товаров», добавим модели в models.py приложения onetomany.
from django.db import models
class Company(models.Model):
name = models.CharField(max_length=30)
class Product(models.Model):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
price = models.IntegerField()
Зарегистрируем приложение в INSTALLED_APPS.
Для создания связи такого типа в классе вторичной модели следует объявить поле типа ForeignКey. Вот формат конструктора этого класса:
ForeignKey(<связываемая первичная модель>, on_delete=<поведение при удалении записи>, [<остальные параметры>])
В этом примере модель Company представляет собой производителя продукции и является главной моделью (главной таблицей в БД), а модель Product представляет собой различные товары, производимые этой компанией, и является зависимой моделью (зависимой таблицей в БД).
Конструктор типа models.ForeignKey в классе Product настраивает связь с главной сущностью.
Здесь первый параметр указывает, с какой моделью будет создаваться связь, - в нашем случае это модель Company.
Второй параметр on_delete задает опцию удаления объекта текущей модели при удалении связанного объекта главной модели.
В частности, в приведенном коде задано каскадное удаление models.CASCADE. То есть если из БД будет удалена компания, то автоматически из БД будет удалена и вся продукция, которая выпускается этой компанией.
Всего для параметра on_delete могут использовать следующие значения:
models.CASCADE: автоматически удаляет строку из зависимой таблицы, если удаляется связанная строка из главной таблицы
models.PROTECT: блокирует удаление строки из главной таблицы, если с ней связаны какие-либо строки из зависимой таблицы
models.SET_NULL: устанавливаетNULLпри удалении связанной строки из главной таблицы
models.SET_DEFAULT: устанавливает значение по умолчанию для внешнего ключа в зависимой таблице. В этом случае для этого столбца должно быть задано значение по умолчанию
models.DO_NOTHING: при удалении связанной строки из главной таблицы не производится никаких действий в зависимой таблице
При этом указано, что в таблице со сведениями о продуктах компании поле company является связующим с главной таблицей, и предусмотрено каскадное удаление всех записей для случая, если компания будет удалена из главной таблицы.
Теперь если мы внесем эти изменения в саму базу данных, для чего в окне терминала выполним команды миграций.
python manage.py makemigrations
python manage.py migrate
И в результате миграции на основе моделей Company и Product в базе данных SQLite автоматически будут созданы таблицы: onetomany_company и onetomany_product.
В сформированных таблицах автоматически созданы ключевые поля для идентификации записей id, а также в таблице onetomany_product создано поле company_id для связи этой дочерней таблицы с родительской таблицей onetomany_company:
На уровне таблиц в БД мы видим, что таблица модели Product связана с таблицей модели Company через столбец company_id. Однако в самом определении модели Product нет поля company_id, а есть только поле company, и именно через него в программном коде можно получать связанные данные.
Чтобы проверить связи запустим шелл:
python manage.py shell
И не забываем перед началом работы импортировать нужные модели:
from onetomany.models import *
Для начала создадим новую компанию с именем Nestle:
c = Company(name='Nestle')
c.save()
Далее создадим два продукта и присвоим их в компанию Nestle:
e = Product(company=c, name='Chocolate', price=100)
e.save()
e = Product(company=c, name='Dragees', price=200)
e.save()
Попробуем получить доступ к Company объекту из Product объекта:
e.company
# получим <Company: Company object (1)>
e.company.name
# получим 'Nestle'
Например, если требуется получить идентификатор компании, которая производит этот продукт, нужно воспользоваться командой:
Product.objects.get(id=1).company.id
# получим 1
Если требуется получить название компании, которая производит этот продукт, нужно воспользоваться командой:
Product.objects.get(id=2).company.name
# получим 'Nestle'
Так, если требуется получить перечень товаров, которые производятся компанией Nestle, следует воспользоваться командой:
Product.objects.filter(company__name='Nestle')
# получим <QuerySet [<Product: Product object (1)>, <Product: Product object (2)>]>
Здесь нужно обратить особое внимание на выражение company__name. С помощью выражения модель__свойство (обратите внимание: два подчеркивания!) можно использовать свойство главной модели для фильтрации объектов (записей в таблице БД) зависимой модели.
С точки зрения модели Company (родительская таблица в БД) она не имеет никаких свойств, которые бы связывали ее с моделью Product (дочерняя таблица в БД).
Но с помощью команды, имеющей следующий синтаксис:
"главная_модель"."зависимая_модель"_set
можно изменить направление связи. То есть на основании записи из главной модели вы сможете получать связанные записи из зависимой (подчинённой) модели.
Давайте продолжим вводить команды в шелле:
comp = Company.objects.get(name='Nestle')
# получение всех товаров фирмы "Nestle"
# <Company: Company object (1)>
tovar = comp.product_set.all()
# получение всех товаров этой компании <QuerySet [<Product: Product object (1)>, <Product: Product object (2)>]>
# получение количества товаров фирмы "Nestle"
count_tovar = comp.product_set.count()
# получим ответ 2
# получение товаров, название которых начинается на "Choc"
tovar = comp.product_set.filter(name__startswith='Choc')
# получим <QuerySet [<Product: Product object (1)>]>
Здесь в переменную comp из объекта Company (а фактически из таблицы БД onetomany_company) мы получаем сведения о компании с именем Nestle.
Затем в переменную tovar считываем сведения обо всех продуктах фирмы Nestle (по сути, из таблицы БД onetomany_product).
Причем с помощью выражения _set можно выполнять операции добавления, изменения, удаления объектов зависимой модели из главной модели.
Рассмотрим следующий программный код:
# создаем объект Company с именем Apple
firma = Company.objects.create(name="Apple")
# создание товара компании
firma.product_set.create(name="IPhone 15 Pro", price=150000)
# отдельное создание объекта с последующим добавлением в БД
ipad = Product(name="iPad", price=34200)
# при добавлении необходимо указать параметр bulk=False
firma.product_set.add(ipad, bulk=False)
# исключает из компании все товары,
# при этом товары остаются в БД и не привязаны к компании
# работает, если в зависимой модели ForeignKey(Company, null=True)
firma.product_set.clear()
# тоже самое, только в отношении одного объекта
ipad = Product.objects.get(name="iPad")
firma.product_set.remove(ipad)
Стоит отметить три метода, которые могут быть использованы в сочетании с выражением _set:
add()- добавляет как саму запись в дочернюю таблицу, так и связь между объектом зависимой модели и объектом главной модели. По своей сути методadd()вызывает для модели еще и методupdate()для добавления связи. Однако это требует, чтобы обе модели уже были в базе данных. И здесь применяется параметрbulk=False- чтобы объект зависимой модели сразу был добавлен в БД и для него была установлена связь;
clear()- удаляет связь между всеми объектами зависимой модели и объектом главной модели. При этом сами объекты зависимой модели (дочерней таблицы) остаются в базе данных, и для их внешнего ключа устанавливается значениеNULL. Поэтому методclear()будет работать, если в самой зависимой модели при установке связи использовался параметрnull=True, т. е.ForeignKey(Company, null=True);
remove()- так же, как иclear(), удаляет связь, только между одним объектом зависимой модели и объектом главной модели. При этом также все объекты дочерней таблицы остаются в БД. И также в самой зависимой модели при установке связи должен использоваться параметрnull=True.