JavaRush /Курси /Модуль 3: Django /Використання select_related() та prefetch_related()

Використання select_related() та prefetch_related()

Модуль 3: Django
Рівень 9 , Лекція 6
Відкрита

Робота з реляційними базами даних — це часто балансування на межі між зручністю програмування та продуктивністю. Припустимо, у нас є дві моделі: Author та Book. Один автор може написати багато книг. Якби ми просто завантажували об'єкти без оптимізації, кожне звернення до пов'язаного об'єкта могло б створювати окремий запит до бази даних. Це не проблема, якщо у нас пара записів. Але спробуйте завантажити одразу тисячу книг з їхніми авторами, і ви почуєте, як ваша база даних жалібно стогне.

1. Оптимізація запитів за допомогою select_related()

select_related() використовується для оптимізації SQL-запитів при роботі з полями ForeignKey або OneToOne. Цей метод дозволяє об'єднати дві таблиці в один SQL-запит за допомогою операції JOIN, тим самим скорочуючи кількість запитів до бази.

Звучить складно? Давай розберемо на прикладі.

Приклад без select_related()

# models.py
class Author(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

# views.py
books = Book.objects.all()
for book in books:
    print(book.title, book.author.name)

У цьому прикладі для кожного об'єкта book Django відправляє окремий запит до бази даних, щоб отримати дані про автора. Якщо у нас 100 книг, це означає 101 запит (1 для книг + 100 для кожного автора).

Приклад з select_related()

books = Book.objects.select_related('author')
for book in books:
    print(book.title, book.author.name)

Тепер Django виконає один запит з використанням SQL JOIN, витягуючи дані про книги та їх авторів одночасно. Виглядає це приблизно так:

SELECT book.id, book.title, author.id, author.name
FROM book
INNER JOIN author ON book.author_id = author.id;

Коли використовувати select_related()?

  • Якщо у вас зв'язок ForeignKey або OneToOne.
  • Якщо ви багаторазово звертаєтесь до пов'язаних об'єктів у циклі або інших операціях.
  • Якщо ваші дані завжди потрібні разом (наприклад, книга завжди відображається зі своїм автором).

2. Робота з prefetch_related()

Якщо select_related() зручний для зв’язків один-до-багатьох ForeignKey і OneToOne, то для зв’язків багато-до-багатьох (ManyToMany) або складних запитів на множину пов’язаних об’єктів краще підходить prefetch_related().

Цей метод виконує два окремих запити, але кешує результат, щоб Django могла зв’язати об’єкти на рівні Python. Це дозволяє уникнути надмірної кількості запитів при складних зв’язках.

Приклад без prefetch_related()

class Tag(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    tags = models.ManyToManyField(Tag)

# views.py
books = Book.objects.all()
for book in books:
    print(book.title, [tag.name for tag in book.tags.all()])

Для кожної книги Django виконає окремий запит, щоб завантажити пов’язані теги. Якщо у нас 50 книг і кожна має 5 тегів, ми отримаємо 51 запит (1 для книг + 50 для тегів).

Приклад з prefetch_related()

books = Book.objects.prefetch_related('tags')
for book in books:
    print(book.title, [tag.name for tag in book.tags.all()])

Тепер виконується лише два запити: один для завантаження книг, інший для завантаження всіх тегів, пов’язаних із цими книгами.

Приклад SQL-запитів:

  1. Завантаження книг:
       SELECT * FROM book;
    
  1. Завантаження тегів:
    SELECT * FROM tag
    INNER JOIN book_tags ON tag.id = book_tags.tag_id
    WHERE book_tags.book_id IN (1, 2, 3, ..., 50);
    

Коли використовувати prefetch_related()?

  • Якщо у вас зв’язок ManyToMany або зворотний зв’язок ForeignKey.
  • Якщо ви хочете уникнути великої кількості "дрібних" запитів.
  • Якщо вам потрібно об’єднати дані з пов’язаних таблиць на рівні Python.

3. Порівняння select_related() і prefetch_related()

Метод Тип зв'язку Кількість запитів Переваги Недоліки
select_related() ForeignKey, OneToOne 1 запит Об'єднує дані на рівні бази даних за допомогою SQL JOIN Не підтримує ManyToMany
prefetch_related()| ManyToMany, зворотний ForeignKey 2 запити (мінімум) Завантажує дані через окремі запити і зв'язує їх на рівні Python Виконує більше запитів, ніж select_related()

Використовуйте select_related(), якщо вам потрібно просто підтягнути дані для полів ForeignKey або OneToOne, а prefetch_related() — для більш складних сценаріїв, пов'язаних з ManyToMany або зворотним зв'язком.

4. Комбінування select_related() та prefetch_related()

Іноді одне без іншого не обходиться. Наприклад, якщо у нас є зв'язок виду:

  • Книга (Book) -> Автор (Author) через ForeignKey.
  • Книга також може мати теги (ManyToMany).

Ми можемо використовувати обидва методи:

books = Book.objects.select_related('author').prefetch_related('tags')
for book in books:
    print(f"Книга: {book.title}, Автор: {book.author.name}, Теги: {[tag.name for tag in book.tags.all()]}")

5. Практика

Давай створимо невеликий приклад. У нас є три моделі: Author, Book та Tag.

Моделі

class Author(models.Model):
    name = models.CharField(max_length=255)

class Tag(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag)

Завантаження даних без оптимізації

books = Book.objects.all()
for book in books:
    print(book.title, book.author.name, [tag.name for tag in book.tags.all()])

Результат: величезна кількість запитів (один для книг, по одному для кожного автора і для тегів).

Оптимізований варіант

books = Book.objects.select_related('author').prefetch_related('tags')
for book in books:
    print(book.title, book.author.name, [tag.name for tag in book.tags.all()])

Результат: всього три запити. Дуже елегантно і швидко!

Підсумки

Методи select_related() та prefetch_related() — це ключові інструменти в оптимізації запитів Django ORM. Вони допоможуть вам уникнути типових помилок початківців розробників, таких як "N+1 запит". Використовуйте їх правильно, і ваш сервер (а, можливо, і фронтенд) буде вам вдячний.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ