Django ORM (Object Relational Mapping) является одной из самых мощных особенностей Django. Это позволяет нам взаимодействовать с базой данных, используя код Python, а не SQL.
Для демонстрации опишу такую модель:
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=250)
url = models.URLField()
def __str__(self):
return self.name
class Author(models.Model):
name = models.CharField(max_length=250)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=250)
content = models.TextField()
published = models.BooleanField(default=True)
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
authors = models.ManyToManyField(Author, related_name="posts")
Я буду использовать 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]
<QuerySet [<Post: Post object (1)>]>
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]
<Blog: Django tutorials>
Тем не менее, это вызвало новый запрос, чтобы получить информацию из блога. Так что используйте 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
<Blog: Django tutorials>
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]
<Blog: Django tutorials>
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]
<QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>, <Author: Guido van Rossum>]>
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]
<QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>]>
Похоже, мы получили запрос для каждого объекта поста. По этому, мы должны использовать 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]
<QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>, <Author: Guido van Rossum>]>
<QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>]>
Что только что произошло??? Мы сократили количество запросов с 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]
<QuerySet [<Post: Post object (1)>, <Post: Post object (2)>]>
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]
<QuerySet [<Post: Post object (1)>, <Post: Post object (2)>]>
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]
<QuerySet [<Post: Post object (1)>]>
То есть, мы использовали 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]
[<Post: Post object (1)>, <Post: Post object (2)>]
[<Post: Post object (1)>, <Post: Post object (2)>]
[<Post: Post object (1)>]
Мы использовали определенный запрос для получения постов через параметр запроса и сохранили отфильтрованные сообщения в новом атрибуте. Как мы видим, теперь у нас есть только 2 запроса в базу данных.
onegreyonewhite
В п.3 наверное было бы проще сразу всех авторов получить у которых post__id в qs. На практике это обычно быстрее, потому что полностью одним запросом к СУБД делается, пусть и с подзапросом.
parfeniukink Автор
Можете пример привести. Я не до конца понял.
onegreyonewhite
Он сделает обычный join или подзапрос — уже и не помню. Но движок бд всяко быстрее отработает + там скорее всего будет пагинатор использоваться, а значит такой вариант больше подойдёт. Ну и список авторов будет без всяких distinct выдавать только уникальные записи.
danilovmy
Либо я не понимаю гениальности этих решений, либо автор и комментатор чего-то не понимают в джанго:
Authors.objects.filter(posts__id__isnull=False).distinct() — все авторы
Authors.objects.filter(posts__title__icontains=«кодзима гений»)
.delete()— выборка авторов по определенному тексту в заголовке поста.onegreyonewhite
Либо вам чуждо понятие примера. Вместо
.all()
может быть любой фильтр и тогда первый пример сразу не подходит. Если параметров для поста довольно много, то можно получить не очень читабельный код, да и оптимизации это не прибавит — SQL будет либо таким же, либо около того.Но если запрос тупо на всех авторов, которые имеют хотя бы один пост, то первый ваш пример самый читабельный. Если нужно отобрать только по содержимому заголовка, то второй тоже неплох. Но если говорить о реальном приложении, то очень редко происходит, что нужно получить данные не с уже имеющегося объекта qs.
Хотя конечно же нужно было мне написать пояснение к моему ходу мыли, чтобы неокрепшие умы не сломались. И вообще, оптимизировать надо исходя из задачи и т.д.
parfeniukink Автор
Хороший пример)
Но Вы же понимаете, что это исключительно в качестве примера)