Orm django что это
Перейти к содержимому

Orm django что это

  • автор:

Django ORM и QuerySet

В этой главе ты узнаешь, как Django подключается к базе данных и сохраняет в неё информацию. Давай начнём!

Что такое QuerySet?

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

Проще научиться на примере. Давай попробуем, согласна?

Интерактивная консоль Django

Открой свой локальный терминал (не на PythonAnywhere) и набери следующую команду:

(myvenv) ~/djangogirls$ python manage.py shell 

Результат должен быть таким:

(InteractiveConsole) >>> 

Ты находишься в интерактивной консоли Django. По сути, это та же интерактивная консоль Python, но с магией Django 🙂 Ты можешь использовать весь синтаксис Python, разумеется.

Все объекты

Давай попробуем вывести на экран все записи в нашем блоге. Ты можешь сделать это следующей командой:

>>> Post.objects.all() Traceback (most recent call last): File "", line 1, in NameError: name 'Post' is not defined 

Упс! Ошибка. Она говорит, что не существует объекта с именем Post. И это верно — мы забыли импортировать его!

>>> from blog.models import Post 

Всё просто: мы импортируем модель Post из blog.models . Давай попробуем получить все записи блога ещё раз:

>>> Post.objects.all() , ]> 

Это список записей, с которыми мы работали до этого! Мы создали их через панель администратора Django. Теперь же мы хотим создавать записи с помощью Python, так как же мы этого добьёмся?

Создаём объект

Создать объект Post в базе данных можно следующим образом:

>>> Post.objects.create(author=me, title='Sample title', text='Test') 

Но у нас есть один недочёт: me . Мы должны передать этой переменной экземпляр модели User , который будет отвечать за автора записи. Как это сделать?

Давай для начала импортируем модель User:

>>> from django.contrib.auth.models import User 

Какие пользователи есть в нашей базе данных? Попробуй эту команду:

>>> User.objects.all() ]> 

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

>>> me = User.objects.get(username='ola') 

Как ты можешь заметить, мы получили ( get ) пользователя ( User ) с именем username ‘ola’. Шикарно! В твоём случае имя, конечно, может отличаться.

Теперь мы, наконец, можем создать наш пост:

>>> Post.objects.create(author=me, title='Sample title', text='Test')

Ура! Хочешь проверить, что всё работает?

>>> Post.objects.all() , , ]> 

Есть, ещё один пост в списке!

Добавляем записи

Можешь повеселиться и добавить ещё записей. 2-3 будет достаточно.

Фильтрация объектов

Важной особенностью QuerySets является возможность фильтровать объекты. Предположим, нам нужно найти все записи пользователя ola. Мы используем метод filter вместо метода all в Post.objects.all() . В скобках мы укажем условия, по которым будет построена выборка записей. В нашей ситуации условием будет являться равенство поля author переменной me . В Django мы можем написать это следующим образом: author=me . Теперь наш код выглядит следующим образом:

>>> Post.objects.filter(author=me) , 2>, , ]> 

А может быть мы хотим получить все записи со словом ‘title’ в поле title ?

>>> Post.objects.filter(title__contains='title') , ]> 

Примечание: обрати внимание на два символа нижнего подчёркивания ( _ ) между title и contains . Django ORM использует этот синтаксис для разделения имён полей («title») и операций или фильтров («contains»). Если ты используешь только один символ нижнего подчёркивания, то получишь ошибку «FieldError: Cannot resolve keyword title_contains».

Ты также можешь получить список всех опубликованных записей. Мы просто отфильтруем записи по полю published_date :

>>> from django.utils import timezone >>> Post.objects.filter(published_date__lte=timezone.now())

К сожалению, пост, который мы добавили в консоли Python, ещё не опубликован. Мы можем изменить это! Сначала выберем запись, которую мы хотим опубликовать:

>>> post = Post.objects.get(title="Sample title") 

Дальше мы опубликуем её с помощью метода publish !

>>> post.publish() 

Теперь попробуй получить список опубликованных сообщений снова (нажми стрелку вверх 3 раза и затем enter ):

>>> Post.objects.filter(published_date__lte=timezone.now()) ]> 

Сортировка объектов

QuerySets позволяет сортировать объекты. Давай попробуем сортировку по полю created_date :

>>> Post.objects.order_by('created_date') , 2>, , ]> 

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

>>> Post.objects.order_by('-created_date') , , 2>, ]> 

Соединение QuerySets

QuerySets можно сцеплять, создавая цепочки:

>>> Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date') , , , ]> 

Это мощный и удобный инструмент, позволяющий писать сложные запросы.

Отлично! Теперь ты готова к следующей части! Чтобы закрыть интерактивную консоль, набери:

>>> exit() $ 

Django ORM и его самые популярные фичи

Django ORM — это инструмент фреймворка Django, который позволяет взаимодействовать с базами данных, используя высокоуровневые методы Python, а не SQL-запросы. Он относится к типу ORM, который реализует шаблон Active Record. Общая суть шаблона в том, что каждой таблице в приложении соответствует одна модель.

Стратегічний курс від laba: Business English для фінансистів.
Завоюйте світовий ринок.

В этой статье мы рассмотрим несколько интересных фич Django ORM, которые помогают эффективно управлять данными в проектах Django.

Django ORM

1. Основы Django ORM

Вот пример модели пользователя:

from django.db import models class User(models.Model): email = models.EmailField(unique=True) nickname = models.CharField(max_length=100, null=True)

Каждый объект соответствует записи в таблице. ORM отвечает за преобразование табличных данных в объекты и обратно. Разработчику остается только выбирать подходящие методы.

Професійний курс від mate.academy: Java.
Погрузьтеся у світ програмування.

Например, можно найти пользователя по идентификатору:

user = User.objects.get(id=2)

Или обновить его никнейм:

user.nickname = 'High and load'

Изменения сохраняются в базе данных после использования метода save :

user.save()

Можно посчитать количество пользователей в БД:

User.objects.count()

Главный плюс Django ORM — существенное упрощение запросов. Это возможно благодаря Query Builder — абстракции поверх SQL.

users = User.objects.order_by('email')[:20]
SELECT "user"."id", . FROM "user" ORDER BY "user"."email" ASC LIMIT 20

В Django ORM существует возможность делать SQL-запросы напрямую. Однако это лучше оставить для тех крайних случаев, с которыми Query Builder не справляется.

2. Плюсы и минусы Django ORM

Главные плюсы использования Django ORM — миграция и транзакции.

Ефективний курс від skvot: Основи 3D-моделювання в ZBrush.
Звільніть свою творчість.

  1. Можно без труда менять таблицы и обновлять модели. Django автоматически генерирует порядок миграции для внесений изменений в БД.
  2. В рамках одной транзакции можно сделать несколько обновлений БД. Если что-то пойдет не так, всегда остается возможность откатиться к предыдущему состоянию.

Недостатки у Django ORM тоже есть. Главным источником проблем становится чрезмерная простота инструмента. Разработчикам не обязательно знать, какие SQL-запросы генерируются. Из-за этого может значительно увеличиваться нагрузка на сервер.

3. Проверка запросов

Чтобы грамотно пользоваться возможностями Django ORM, нужно понимать, что происходит «под капотом». Сделать это можно несколькими способами.

Connection.queries

Если в настройках Django-проекта установлен параметр debug = True , то можно смотреть выполненные запросы с помощью connection.queries . Пример:

from django.db import connection ost.objects.all() connection.queries [ < 'sql': 'SELECT "blogposts_post"."id", "blogposts_post"."title", ' '"blogposts_post"."content", "blogposts_post"."blog_id", ' '"blogposts_post"."published" FROM "blogposts_post" LIMIT 21', 'time': '0.000' >]

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

from django.db import reset_queries reset_queries()

Shell_plus –print-sql

Shell_plus — одна из функций библиотеки расширений для Django. При вызове с параметром — print-sql она отображает SQL-запросы по мере их выполнения. Например:

manage.py shell_plus --print-sql post = Post.objects.get(id=1) SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1

Django Silk

Silk — инструмент профилирования Django. Он записывает и визуализирует выполненные SQL-запросы. Это позволяет разработчику видеть, какие запросы отработали, изучать подробности по каждому обращению к БД, в том числе контролировать, какая строка кода инициировала запрос.

Django Debug Toolbar

Debug Toolbar добавляет в браузер отладочную панель. Она предоставляет много возможностей для проверки проекта и исправления ошибок, в том числе отображает выполненные SQL-запросы. Можно проверить каждый запрос, посмотреть порядок их выполнения, а также затраченное время (профилирование).

4. Фильтрация данных

Аргумент filter появился еще в версии Django 2.0. Это заметно упростило получение данных по нескольким условиям. Например, вот так просто можно увидеть общее количество пользователей и общее количество активных пользователей.

Експертний курс від robotdreams: Product Manager PRO.
Керуйте продуктом на високому рівні.

from django.contrib.auth.models import User from django.db.models import Count, F User.objects.aggregate( total_users=Count('id'), total_active_users=Count('id', filter=F('is_active')), )

Сравните, насколько больше кода нужно написать для решения той же задачи без использования filter :

from django.contrib.auth.models import User from django.db.models import ( Count, Sum, Case, When, Value, IntegerField, ) User.objects.aggregate( total_users=Count('id'), total_active_users=Sum(Case( When(is_active=True, then=Value(1)), default=Value(0), output_field=IntegerField(), )), )

В PostgreSQL та же операция выглядит следующим образом:

SELECT COUNT(id) AS total_users, SUM(CASE WHEN is_active THEN 1 ELSE 0 END) AS total_active_users FROM auth_users; SELECT COUNT(id) AS total_users, COUNT(id) FILTER (WHERE is_active) AS total_active_users FROM auth_users;

Ничего сложного, но с ORM все равно намного удобнее.

5. QuerySet – результаты в виде именованного кортежа

Еще один полезный атрибут — named . Если он равен True , то QuerySet отображается в виде именованного кортежа:

user.objects.values_list( 'first_name', 'last_name', )[0] (‘High’, ‘Load’) user_names = User.objects.values_list( 'first_name', 'last_name', named=True, ) user_names[0] Row(first_name='High', last_name='Load') user_names[0].first_name 'High' user_names[0].last_name 'Load'

6. Пользовательские функции ORM Django

В Django ORM доступно добавление пользовательских функций. Это помогает расширить его возможности и не дожидаться обновлений от вендоров баз данных.

Например, нужно найти среднюю продолжительность. Сделать это легко:

from django.db.models import Avg Report.objects.aggregate(avg_duration=Avg(‘duration’))

Но само по себе среднее значение ничего не дает. Допустим, для анализа требуется еще среднеквадратичное отклонение:

from django.db.models import Avg, StdDev Report.objects.aggregate( avg_duration=Avg('duration'), std_duration=StdDev('duration'), ) ProgrammingError: function stddev_pop(interval) does not exist LINE 1: SELECT STDDEV_POP("report"."duration") AS "std_dura. HINT: No function matches the given name and argument types. You might need to add explicit type casts.

Здесь PostgreSQL сообщает об ошибке. Stddev не поддерживается на поле типа interval . Сначала нужно привести interval к числу. Можно сделать это с помощью функции Extract :

SELECT AVG(duration), STDDEV_POP(EXTRACT(EPOCH FROM duration)) FROM report; avg | stddev_pop ----------------+------------------ 00:00:00.55432 | 1.06310113695549 (1 row)

То же самое можно реализовать в Django с помощью пользовательских функций:

# common/db.py from django.db.models import Func class Epoch(Func): function = 'EXTRACT' template = "%(function)s('epoch' from %(expressions)s)"

В итоге функция для определения среднеарифметического значения и среднеквадратичного отклонения будет выглядеть так:

from django.db.models import Avg, StdDev, F from common.db import Epoch Report.objects.aggregate( avg_duration=Avg(‘duration’), std_duration=StdDev(Epoch(F(‘duration’))), )

7. Ограничение времени выполнения запроса

В Django используются синхронные процессы. Пока пользователь выполняет длительную операцию, рабочий процесс приостанавливается. В большинстве случаев время тратится на запросы к БД. Поэтому будет нелишним ограничить время выполнения запросов.

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

# wsgi.py from django.db.backends.signals import connection_created from django.dispatch import receiver @receiver(connection_created) def setup_postgres(connection, **kwargs): if connection.vendor != 'postgresql': return # Таймаут через 40 секунд. with connection.cursor() as cursor: cursor.execute(""" SET statement_timeout TO 40000; """)

Используется файл wsgi.py , чтобы ограничить только рабочие процессы.

Таймаут также можно настроить на уровне пользователя:

postgresql=#> alter user app_user set statement_timeout TO 40000; ALTER ROLE

Еще одна хорошая практика — устанавливать таймаут на вызов удаленной службы, чтобы не ждать бесконечно ответа:

import requests response = requests.get( 'https://api.very-slow.com', timeout=4000, )

8. Установка лимита

Еще один способ оптимизации запросов — установка лимитов.
Например, пользователю нужно получить список всех продаж с начала работы компании. Разработчик понимает, что такая ситуация может случиться, и поэтому устанавливает лимит: не более 100 записей.

data = Sale.objects.all()[:100]

Оператор limit гарантирует, что пользователь получит только 100 записей. Но здесь возникает проблема. Пользователь хотел получить все записи. Получил 100. Он может подумать, что в базе данных всего 100 записей. Но что если это не так? Как об этом сообщить пользователю?

Логичное решение — выбрасывать исключение, если записей больше, чем разрешено лимитом:

LIMIT = 100 if Sales.objects.count() > LIMIT: raise ExceededLimit(LIMIT) return Sale.objects.all()[:LIMIT]

Можно сделать еще удобнее.

LIMIT = 100 data = Sale.objects.all()[:(LIMIT + 1)] if len(data) > LIMIT: raise ExceededLimit(LIMIT) return data

Вместо того чтобы запрашивать первые 100 записей, код запрашивает 100 + 1 запись. Если есть запись 101, то всего записей уже точно больше 100. Значит, будет выброшено исключение.

9. Использование кэшированных внешних ключей

Если необходимо получить идентификатор внешнего ключа, можно использовать кэшированный ID с помощью _id .
Например, пусть будет такой запрос:

Post.objects.first().blog.id

Вот какие SQL-запросы выполняются:

SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1 Execution time: 0.001668s [Database: default] SELECT "blogposts_blog"."id", "blogposts_blog"."name", "blogposts_blog"."url" FROM "blogposts_blog" WHERE "blogposts_blog"."id" = 1 LIMIT 21 Execution time: 0.000197s [Database: default]

После обращения к id объекта blog создается еще один запрос, который возвращает весь объект blog . Но если не требуется доступ к другим атрибутам объекта blog , то и возвращать его целиком нет смысла.

Использование кэшированного id :

Post.objects.first().blog_id

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

SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1 Execution time: 0.000165s [Database: default]

10. Использование select_related

Еще одна хорошая практика — заранее рассказывать Django, что нужно делать. Например, для этого есть функция select_related . Она позволяет точно указать, какие связанные модели потребуются, чтобы Django мог выполнить JOIN .

Например, есть модель Post . Каждый Post принадлежит определенному Blog, а отношения эти выражены в базе данных через внешние ключи.

Допустим, нужно получить определенный Post :

post = Post.objects.get(id=1)
SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1

А теперь нужно получить доступ к Blog из Post :

post.blog

SELECT «blogposts_blog».»id», «blogposts_blog».»name», «blogposts_blog».»url» FROM «blogposts_blog» WHERE «blogposts_blog».»id» = 1 LIMIT 21 Execution time: 0.000602s [Database: default]

Чтобы получить информацию из Blog , ORM выполнил новый запрос. Этого можно избежать, используя select_related :

post = Post.objects.select_related("blog").get(id=1)
SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published", "blogposts_blog"."id", "blogposts_blog"."name", "blogposts_blog"."url" FROM "blogposts_post" INNER JOIN "blogposts_blog" ON ("blogposts_post"."blog_id" = "blogposts_blog"."id") WHERE "blogposts_post"."id" = 1 LIMIT 21 Execution time: 0.000150s [Database: default]

Кроме того, что количество запросов уменьшилось, было выполнено кэширование. Дополнительный запрос теперь не нужен. Функция select_related работает также для набора запросов. Не нужно каждый раз обращаться к базе данных, чтобы проверить связь — достаточно сделать это один раз.

11. Индексы внешних ключей

Django создает B-Tree индексы для внешних ключей в модели. Они не всегда нужны и при этом занимают много места. Типичный пример — модель с отношением многие-ко-многим:

class Membership(Model): group = ForeignKey(Group) user = ForeignKey(User)

Здесь будет два внешних индекса — для user и group .

Еще одна стандартная ситуация — уникальные ограничения. Например, один пользователь может быть членом одной группы только один раз:

class Membership(Model): group = ForeignKey(Group) user = ForeignKey(User) class Meta: unique_together = ( 'group', 'user', )

Unique_together создает индекс для обоих полей: group и user . В итоге есть одна модель, два поля и целых три индекса.

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

class Membership(Model): group = ForeignKey(Group, db_index=False) user = ForeignKey(User, db_index=False) class Meta: unique_together = ( 'group', 'user', )

Цель этих манипуляций — оптимизация. Без лишних индексов вставка и обновление данных будут проходить быстрее, а база данных будет весить меньше.

12. Порядок столбцов в составном индексе

Если в индексе более одного столбца, то он называется составным. В таких индексах первый столбец индексируется с использованием древовидной структуры. Листья первого уровня формируют деревья второго уровня и так далее.

Порядок столбцов важен. В примере выше сначала создавалось бы дерево для групп и только затем — для пользователей. Скорее всего, пользователей будет больше, чем групп. Поэтому столбец с ними должен быть выше.

class Membership(Model): group = ForeignKey(Group, db_index=False) user = ForeignKey(User, db_index=False) class Meta: unique_together = ( 'user', 'group', )

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

13. BRIN-индексы

Основная проблема индексов B-Tree в том, что они занимают много места. Выше мы рассмотрели, как можно их оптимизировать. Но есть и альтернативные способы — например, в PostgreSQL можно использовать BRIN (Block Range Index). В некоторых случаях этот тип индексов эффективнее, чем B-Tree .

BRIN подходит для обработки огромных таблиц. Важно, чтобы значение индексируемого столбца имело естественную корреляцию с физическим расположением строки в таблице.

Фактически BRIN создает мини-индекс, используя ряд соседних блоков в таблице. Каждый такой мини-индекс может сказать, находится определенное значение в диапазоне блоков или нет.

Например, есть девять блоков:

1, 2, 3, 4, 5, 6, 7, 8, 9

Их можно объединить по три:

[1,2,3], [4,5,6], [7,8,9]

Для простоты в каждом диапазоне будет храниться минимальное и максимальное значения:

[1–3], [4–6], [7–9]

Например, нужно найти блок 8. Процесс будет выглядеть следующим образом:

[1–3] — здесь такого точно нет;

[4–6] — здесь такого точно нет;

[7–9] — возможно, здесь.

Благодаря такому разделению поиск ограничивается одним диапазоном.

Но все ломается, если значения не отсортированы. Например,

[2,8, 4], [3,5,9], [1,7,6]

Диапазоны с минимальными и максимальными значениями будут выглядеть так:

[2–8], [3–9], [1–7]

Например, нужно найти блок 5.

[2–8] — возможно, здесь;

[3–9] — возможно, здесь;

[1–7] — возможно, здесь.

Разделение на диапазоны становится не только бесполезным, но и вредным. Одну и ту же работу приходится выполнять несколько раз.
Для максимально полезного использования BRIN данные должны быть отсортированы или сгруппированы. Например, можно использовать поле auto_now_add :

class SomeModel(Model): created = DatetimeField( auto_now_add=True, )

Теперь при добавлении данных Django будет автоматически записывать время. Это значит, что можно использовать BRIN -индекс.

from django.contrib.postgres.indexes import BrinIndex class SomeModel(Model): created = DatetimeField( auto_now_add=True, ) class Meta: indexes = ( BrinIndex(fields=['created']), )

При небольшом количестве записей разница в размере БД будет незаметной. Но если добавить в таблицу два миллиона записей и отсортировать их по дате, то разница будет значительная:

B-Tree -индекс — 37 MB

BRIN -индекс — 49 KB

Заключение

В этой статье мы разобрали основы, плюсы и минусы Django ORM, научились проверять запросы, а также познакомились с некоторыми полезными функциями.

Чтобы закрепить материал и узнать еще больше о работе с Django ORM, посмотрите этот часовой видеокурс:

Основные концепции — Python: Django ORM

ORM (Object-Relation Mapping) – общее название для фреймворков или библиотек, позволяющих автоматически связать базу данных с кодом. Они стараются скрыть существование базы данных настолько, насколько это возможно. Взамен программисту дают возможность оперировать данными в базе через специальный интерфейс. Вместо построения SQL-запросов, программист вызывает простые методы, а всю остальную работу берёт на себя ORM.

user = User() user.first_name = 'Peter' user.save() # сохранение пользователя в базу 

Несмотря на общую цель, ORM бывают очень разными. Django ORM относится к наиболее распространённому и простому типу ORM, реализующему шаблон проектирования Active Record. Этот шаблон базируется на идее, что каждой таблице в приложении соответствует один класс (модель). Этот класс отвечает как за реализацию бизнес логики, так и за взаимодействие с базой данных. Последнее обычно появляется в модели за счёт наследования от базового класса ORM.

Кроме Active Record существует шаблон Data Mapper, он например реализован в SQLAlchemy . Этот подход разделяет сущности и код, связанный с базой данных, на два независимых слоя. Такой подход гибче, но при этом сложнее в работе.

Модель

from django.db import models class User(models.Model): email = models.EmailField(unique=True) nickname = models.CharField(max_length=100, null=True) 

Каждый объект данного класса соответствует одной записи в таблице. За преобразования данных в объекты и обратно, отвечает ORM. Программисту лишь нужно использовать правильные методы для извлечения объектов и их модификации.

# Поиск пользователя по идентификатору user = User.objects.get(id=1) # Обновление пользователя user.nickname = 'Crash and Burn' # Сохранение в базу user.save() # Удаление записи в базе данных user.delete() # Общее число пользователей User.objects.count() # => 0 

Выборки

Важная часть любой ORM — это Query Builder (построитель запросов). Это абстракция поверх SQL, которая упрощает генерацию запросов. Она обычно выглядит как цепочка функций, каждая из которых отвечает за конкретную часть SQL, например: ORDER, SELECT или WHERE.

users = User.objects.order_by('email')[:10] # SELECT "blog_user"."id", # . # FROM "blog_user" # ORDER BY "blog_user"."email" ASC # LIMIT 10 

Django ORM позволяет описывать достаточно сложные запросы, и большинство задач вы сможете решать именно с помощью Query Builder. Однако существует и возможность делать запросы напрямую:

first_user = User.objects.raw('SELECT * FROM blog_user')[0] # SELECT * # FROM blog_user Execution time: 0.002150s [Database: default] 

Мы не рекомендуем по умолчанию использовать запросы, написанные вручную. Оставьте эту возможность для тех немногих случаев, когда Query Builder не может сформировать эффективный запрос.

Схема

Ещё одна обязанность ORM – изменение схемы базы данных: добавление, удаление и модификация таблиц. Делается это, как правило, не на чистом SQL, а с помощью специального языка. Это позволяет работать с ORM, не отвлекаясь на особенности конкретных баз данных.

В Django ORM первостепенными являются модели — вы изменяете код, а потом используете средства для приведения схемы базы данных в соответствие новому состоянию кода. Такой подход называется Code First.

Также существует подход Database First: вы сначала меняете базу данных, выполняя SQL-запросы или формируя запросы с помощью средств ORM, а затем ORM уже сама подхватывает изменения и адаптирует модели. Например для добавления нового свойства достаточно добавить новую колонку. В коде ничего менять не нужно, она автоматически начинает работать.

Миграции

Любая база данных в процессе жизни приложения изменяется. В неё добавляются новые таблицы, удаляются старые, какие-то меняются. Этот процесс никогда не заканчивается. Изменения в базе данных выполняются с помощью механизма миграций.

В Django ORM миграция — это Python модуль, который содержит описание действий, изменяющих схему базы данных. Учебный проект содержит пример миграции, но если вы этот файл откроете в редакторе, количество кода может вас смутить. К счастью, этот код не написан вручную — миграции Django генерирует самостоятельно. Модифицировать же вручную модули миграций приходится достаточно редко.

Попробуем добавить поле «nickname» к полям модели User из учебного проекта:

# python_django_orm_blog/blog/models.py class User(models.TimestampedModel): """A blog user.""" email = models.EmailField(unique=True) first_name = models.CharField(max_length=100, null=True) last_name = models.CharField(max_length=100, null=True) # это поле - новое! nickname = models.CharField(max_length=100, null=True) 

Модель изменилась, нужно сгенерировать миграцию:

for 'blog': python_django_orm_blog/blog/migrations/0002_user_nickname.py - Add field nickname to user 

Теперь миграцию нужно применить:

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Django ORM для начинающих | Оптимизируем запросы

Я буду использовать django-extentions, чтобы получить полезную информацию с помощью:

python manage.py shell_plus --print-sql 
>>> post = Post.objects.all() >>> post SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id" FROM "blog_post" LIMIT 21 Execution time: 0.000172s [Database: default] ]> 
1. Используем ForeignKey значения непосредственно
>>> Post.objects.first().blog.id SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id" FROM "blog_post" ORDER BY "blog_post"."id" ASC LIMIT 1 Execution time: 0.000225s [Database: default] SELECT "blog_blog"."id", "blog_blog"."name", "blog_blog"."url" FROM "blog_blog" WHERE "blog_blog"."id" = 1 LIMIT 21 Execution time: 0.000144s [Database: default] 1 

А так получаем 1 запрос в БД:

>>> Post.objects.first().blog_id SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id" FROM "blog_post" ORDER BY "blog_post"."id" ASC LIMIT 1 Execution time: 0.000155s [Database: default] 1 
2. OneToMany Relations

Если мы используем OneToMany отношения мы используем ForeignKey поля и запрос выглядит примерно так:

>>> post = Post.objects.get(id=1) SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id" FROM "blog_post" WHERE "blog_post"."id" = 1 LIMIT 21 Execution time: 0.000161s [Database: default] 

И если мы хотим получить доступ к объекту блога из объекта поста, мы можем сделать:

>>> post.blog SELECT "blog_blog"."id", "blog_blog"."name", "blog_blog"."url" FROM "blog_blog" WHERE "blog_blog"."id" = 1 LIMIT 21 Execution time: 0.000211s [Database: default]

Тем не менее, это вызвало новый запрос, чтобы получить информацию из блога. Так что используйте select_related, чтобы избежать этого. Чтобы использовать его, мы можем обновить наш оригинальный запрос:

>>> post = Post.objects.select_related("blog").get(id=1) SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id", "blog_blog"."id", "blog_blog"."name", "blog_blog"."url" FROM "blog_post" INNER JOIN "blog_blog" ON ("blog_post"."blog_id" = "blog_blog"."id") WHERE "blog_post"."id" = 1 LIMIT 21 Execution time: 0.000159s [Database: default] 

Обратите внимание, что Django использует JOIN сейчас! И время выполнения запроса меньше, чем раньше. Кроме того, теперь post.blog будет кэширован!

>>> post.blog

select_related так же работает с QurySets:

>>> posts = Post.objects.select_related("blog").all() >>> for post in posts: . post.blog . SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id", "blog_blog"."id", "blog_blog"."name", "blog_blog"."url" FROM "blog_post" INNER JOIN "blog_blog" ON ("blog_post"."blog_id" = "blog_blog"."id") Execution time: 0.000241s [Database: default]
3. ManyToMany Relations

Чтобы получить авторов постов мы используем что-то вроде этого:

>>> for post in Post.objects.all(): . post.authors.all() . SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id" FROM "blog_post" Execution time: 0.000242s [Database: default] SELECT "blog_author"."id", "blog_author"."name" FROM "blog_author" INNER JOIN "blog_post_authors" ON ("blog_author"."id" = "blog_post_authors"."author_id") WHERE "blog_post_authors"."post_id" = 1 LIMIT 21 Execution time: 0.000125s [Database: default] , , ]> SELECT "blog_author"."id", "blog_author"."name" FROM "blog_author" INNER JOIN "blog_post_authors" ON ("blog_author"."id" = "blog_post_authors"."author_id") WHERE "blog_post_authors"."post_id" = 2 LIMIT 21 Execution time: 0.000109s [Database: default] , ]> 

Похоже, мы получили запрос для каждого объекта поста. По этому, мы должны использовать prefetch_related. Это похоже на select_related но используется с ManyToMany Fields:

>>> for post in Post.objects.prefetch_related("authors").all(): . post.authors.all() . SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."blog_id" FROM "blog_post" Execution time: 0.000300s [Database: default] SELECT ("blog_post_authors"."post_id") AS "_prefetch_related_val_post_id", "blog_author"."id", "blog_author"."name" FROM "blog_author" INNER JOIN "blog_post_authors" ON ("blog_author"."id" = "blog_post_authors"."author_id") WHERE "blog_post_authors"."post_id" IN (1, 2) Execution time: 0.000379s [Database: default] , , ]> , ]> 

Что только что произошло. Мы сократили количество запросов с 2 до 1, чтобы получить 2 QuerySet-a!

4. Prefetch object

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

>>> authors = Author.objects.prefetch_related("posts").all() >>> for author in authors: . print(author.posts.filter(published=True)) . SELECT "blog_author"."id", "blog_author"."name" FROM "blog_author" Execution time: 0.000580s [Database: default] SELECT ("blog_post_authors"."author_id") AS "_prefetch_related_val_author_id", "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."published", "blog_post"."blog_id" FROM "blog_post" INNER JOIN "blog_post_authors" ON ("blog_post"."id" = "blog_post_authors"."post_id") WHERE "blog_post_authors"."author_id" IN (1, 2, 3) Execution time: 0.000759s [Database: default] SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."published", "blog_post"."blog_id" FROM "blog_post" INNER JOIN "blog_post_authors" ON ("blog_post"."id" = "blog_post_authors"."post_id") WHERE ("blog_post_authors"."author_id" = 1 AND "blog_post"."published" = 1) LIMIT 21 Execution time: 0.000299s [Database: default] , ]> SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."published", "blog_post"."blog_id" FROM "blog_post" INNER JOIN "blog_post_authors" ON ("blog_post"."id" = "blog_post_authors"."post_id") WHERE ("blog_post_authors"."author_id" = 2 AND "blog_post"."published" = 1) LIMIT 21 Execution time: 0.000336s [Database: default] , ]> SELECT "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."published", "blog_post"."blog_id" FROM "blog_post" INNER JOIN "blog_post_authors" ON ("blog_post"."id" = "blog_post_authors"."post_id") WHERE ("blog_post_authors"."author_id" = 3 AND "blog_post"."published" = 1) LIMIT 21 Execution time: 0.000412s [Database: default] ]> 

То есть, мы использовали prefetch_related, чтобы уменьшить количество запросов, но мы фактически увеличили его. Чтобы этого избежать, мы можем настроить запрос с помощью объекта Prefetch:

>>> authors = Author.objects.prefetch_related( . Prefetch( . "posts", . queryset=Post.objects.filter(published=True), . to_attr="published_posts", . ) . ) >>> for author in authors: . print(author.published_posts) . SELECT "blog_author"."id", "blog_author"."name" FROM "blog_author" Execution time: 0.000183s [Database: default] SELECT ("blog_post_authors"."author_id") AS "_prefetch_related_val_author_id", "blog_post"."id", "blog_post"."title", "blog_post"."content", "blog_post"."published", "blog_post"."blog_id" FROM "blog_post" INNER JOIN "blog_post_authors" ON ("blog_post"."id" = "blog_post_authors"."post_id") WHERE ("blog_post"."published" = 1 AND "blog_post_authors"."author_id" IN (1, 2, 3)) Execution time: 0.000404s [Database: default] [, ] [, ] [] 

Мы использовали определенный запрос для получения постов через параметр запроса и сохранили отфильтрованные сообщения в новом атрибуте. Как мы видим, теперь у нас есть только 2 запроса в базу данных.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *