Django 5 для начинающих

Прогресс по курсу:  9/1004

3.4 Организация связей между таблицами
3 из 11 шагов пройдено
0 из 53 баллов  получено

Организация связей между таблицами "ОДИН-КО-МНОГИМ"

Рассмотрим организацию связи между таблицами БД «один-ко-многим» или «многие-к-одному», при которой одна главная сущность может быть связаны с несколькими зависимыми сущностями.

Приведем несколько примеров таких связей:

  • одна компания, которая выпускает множество видов товаров;
  • один автомобиль, который состоит из множества составных частей;
  • одна гостиница, которая имеет множество комнат с разными характеристиками;
  • одна книга, у которой несколько авторов;
  • один город, который имеет множество улиц;
  • одна улица, которая имеет множество домов, и т. п.

Создадим новое приложение в нашем проекте:

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.

  • Комментариев
Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий

Ранее при создании модели Product, поле price было задано через IntegerField, вероятнее всего тут ошибка, цену надо было указать числом, а не строкой?)

 

@Нарбеков_Марсель, так допустимо, строка будет автоматически конвертирована в число, хотя лучше наверно исправить, чтоб не смущать учащихся. Спасибо за замечание, исправил.

У меня в моделях onetomany не работают методы RelatedManager clear() и remove():

Аналогичная ошибка с remove()

UPDATE: дочитал урок до конца, помогло )) Пока не добавишь в модель null=True, будет ошибка

Изменен ilya kutaev

@ilya_kutaev, Спасибо за комментарий, я бы еще добавил к нему, что после изменения модели (добавление null=True) необходимо повторно выполнить миграции и при последующей работе в shell не забыть импортировать измененную модель (на чем собственно я немного подзавис ))) )

@Максим_Михеев, Конечно, как и при любых изменениях модели ))

А вот с shell я не мучаюсь, использую shell_plus из django-extensions, он импортирует модели сам

@ilya_kutaev, Спасибо за инфу, возьму на вооружение...

На уровне таблиц в БД мы видим, что таблица Product связана с таблицей Company через столбец company_id. Однако в самом определении модели Product нет поля company_id, а есть только поле company, и именно через него в программном коде можно получать связанные данные.

Здесь наверно в первом абзаце тоже речь о таблицах, а не о моделях. Думаю верно будет "На уровне таблиц в БД мы видим, что таблица onetomany_product связана с таблицей onetomany_company через столбец company_id"

@Евгений_Епишкин, Тут тогда правильнее так: "На уровне таблиц в БД мы видим, что таблица модели  Product связана с таблицей модели Company через столбец company_id. Однако в самом определении модели Product нет поля company_id, а есть только поле company, и именно через него в программном коде можно получать связанные данные."

@Илья_Перминов, так тоже отлично!

В сформированных таблицах автоматически созданы ключевые поля для идентификации записей id, а также в таблице onetomany_product создано поле company_id для связи этой дочерней таблицы с родительской таблицей Company:

 

с родительской таблицей onetomany_company а не Company (такой таблицы судя по скрину нет)

@Евгений_Епишкин, Спасибо, поправил.

Здравствуйте, столкнулся с ошибкой и не знаю, как правильно решить. Я шел по этому топику и в конце, где надо было ввести эту команду firma.product_set.clear() , я добавил в модели null=True, потом выполнил миграцию и вроде как всё должно было заработать, но нет, оно не работало. Я думал, что проще всего будет удалить эти таблицы в Database и приложение и создать его заново(выполнить миграцию и тд), но вот в чем прикол, я прописал заново модели, но миграция не выполняется, видимо где-то остались данные о прошлых миграциях и моя нынешняя миграция не воспринимается, как что-то новое.

@Шамбер_Егор, попробуйте удалить файл БД(db.sqlite3) и заново применить миграции(migrate).

@Шамбер_Егор@Дмитрий_Селезнев,  здравствуйте, помогло, только пришлось все таблицы заново воссоздавать ибо данные из них удалились

и вообще нужно ли мне воссоздавать их заново или можно не переделывать заново таблицы из этого топика про организацию связей, я просто не знаю понадобятся ли конкретно эти таблицы нам потом 

Изменен Шамбер Егор

@Шамбер_Егор, Если не ошибаюсь, то они не понадобятся. В Django ORM мы будем работать над своими моделями.

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

@Руслан_Гаджиев, Первым делом в голову приходит что то подобное:

class Product(models.Model):
    # поля товара
    # имя, цена и тд


class Order(models.Model):
    buyer = models.ForeignKey(User)
    # поля заказа
    # например дата создания, адресс доставки, статус оплаты


class Order_Item(models.Model):
    order = models.ForeignKey(Order)
    product = models.ForeignKey(Product)
    # поля цены, количества позиции товара

1. Здесь в переменную comp из объекта Company (а фактически из  таблицы БД Company) мы получаем сведения о компании с именем «Nestle».

здесь тоже таблицу из БД необходимо поменять на onetomany_company в скобках

2. # создаем объект Company с именем Электрон

firma = Company.objects.create(name="Apple")

 

Думаю верно будет: создаем объект Company с именем "Apple"

@Евгений_Епишкин, спасибо, исправил.

Предполагаю что тут тоже модель должна писаться с большой буквы:

@Максим_Михеев, спасибо, поправил.

тут майкрософт, и нестле? что из этого правильно?

@Vlad_Kirnats, Nestle разумеется, спасибо за замечание, исправил.