Перейти к основному содержанию

Производительность и оптимизация в TypeORM

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

1. Введение в оптимизацию производительности

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

  • Типичные проблемы при использовании ORM включают избыточное извлечение данных, проблемы N+1 запросов и неиспользование инструментов оптимизации, таких как индексы или кэширование.

  • Основные цели оптимизации включают:

    • Сокращение количества SQL-запросов к базе данных.
    • Оптимизацию сложных запросов для ускорения их выполнения.
    • Использование кэширования и индексации для ускорения доступа к данным.
    • Обеспечение эффективного извлечения данных через правильный выбор стратегий загрузки (Lazy vs. Eager).

2. Эффективное использование Query Builder

2.1. Избегание проблемы N+1 запросов

  • Проблема N+1 запросов возникает, когда система выполняет слишком много подзапросов для каждой извлечённой строки данных.

  • Чтобы избежать этого, используйте leftJoinAndSelect или innerJoinAndSelect для объединения таблиц в одном запросе вместо выполнения множества отдельных запросов.

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.posts", "post")
.getMany()
  • В этом случае leftJoinAndSelect позволяет извлечь все посты пользователя в рамках одного запроса вместо множества мелких запросов.

2.2. Использование getRawMany() для сырых данных

  • Если полные объекты не требуются, используйте getRawMany() для получения сырых данных, избегая избыточной обработки информации в TypeORM.
const rawPosts = await dataSource
.getRepository(Post)
.createQueryBuilder("post")
.select("post.title, post.createdAt")
.getRawMany()

2.3. Ограничение полей через select

  • Для оптимизации использования памяти и сокращения избыточных данных выбирайте только необходимые поля через select.
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select(["user.name", "user.email"])
.getMany()

3. Использование индексов

  • Индексы ускоряют выполнение запросов в базе данных, сокращая объём сканируемых данных. TypeORM поддерживает создание индексов на столбцах таблиц через декоратор @Index.

3.1. Создание индекса

  • Индексы можно создавать непосредственно в сущностях через декоратор @Index.
import { Entity, Column, Index } from "typeorm"

@Entity()
@Index(["firstName", "lastName"]) // Composite index
export class User {
@Column()
firstName: string

@Column()
lastName: string
}

3.2. Уникальный индекс

  • Вы можете создавать уникальные индексы для гарантии отсутствия дубликатов значений в столбце.
@Index(["email"], { unique: true })

4. Lazy loading и Eager Loading

TypeORM предоставляет два основных метода загрузки связей: Lazy Loading (отложенная) и Eager Loading (жадная). Каждый по-разному влияет на производительность приложения.

4.1. Lazy loading (отложенная загрузка)

  • Отложенная загрузка (Lazy loading) загружает данные связи только по требованию, снижая нагрузку на БД, когда связанные данные не всегда необходимы.
@Entity()
export class User {
@OneToMany(() => Post, (post) => post.user, { lazy: true })
posts: Promise<Post[]>
}
  • Когда потребуется получить данные, просто вызовите
const user = await userRepository.findOne(userId)
const posts = await user.posts
  • Преимущества:

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

    • Усложнение запросов: каждое обращение к связанным данным инициирует дополнительный запрос к БД, что может увеличить задержку при неправильном управлении.
    • Сложность отслеживания: может привести к проблеме N+1 запросов при неосторожном использовании.

4.2. Eager Loading (жадная загрузка)

  • Жадная загрузка (Eager loading) автоматически извлекает все связанные данные при выполнении основного запроса. Это удобно, но может вызвать проблемы с производительностью при наличии множества сложных связей.
@Entity()
export class User {
@OneToMany(() => Post, (post) => post.user, { eager: true })
posts: Post[]
}
  • В этом случае посты будут загружены сразу после получения данных пользователя.

  • Преимущества:

    • Автоматически загружает связанные данные, упрощая доступ к связям без дополнительных запросов.
    • Избегает проблемы N+1 запросов: поскольку все данные извлекаются одним запросом, нет риска генерации множества ненужных запросов.
  • Недостатки:

    • Получение всех связанных данных разом может привести к громоздким запросам, даже если часть данных не нужна.
    • Не подходит для сценариев, где требуется только подмножество связанных данных, так как ведёт к неэффективному использованию данных.
  • Для подробного изучения примеров настройки и использования отложенных и жадных связей посетите официальную документацию TypeORM: Жадные и отложенные связи

5. Продвинутые методы оптимизации

5.1. Использование подсказок запросов (Query Hints)

  • Подсказки запросов (Query Hints) — это инструкции, передаваемые вместе с SQL-запросами, которые помогают СУБД выбрать более эффективную стратегию выполнения.

  • Разные реляционные СУБД поддерживают различные типы подсказок, например, рекомендации по использованию индексов или выбору типа JOIN.

await dataSource.query(`
SELECT /*+ MAX_EXECUTION_TIME(1000) */ *
FROM user
WHERE email = 'example@example.com'
`)
  • В примере выше MAX_EXECUTION_TIME(1000) указывает MySQL остановить запрос, если его выполнение превышает 1 секунду.

5.2. Пагинация

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

  • В TypeORM для пагинации используйте limit и offset.

const users = await userRepository
.createQueryBuilder("user")
.limit(10) // Number of records to fetch per page
.offset(20) // Skip the first 20 records
.getMany()
  • Пагинация помогает избежать извлечения больших объёмов данных за раз, снижая задержки и оптимизируя использование памяти. При реализации пагинации рассмотрите использование курсоров для более эффективной работы с динамическими данными.

5.3. Кэширование

  • Кэширование — это техника временного хранения результатов запросов или данных для использования в будущих запросах без необходимости каждый раз обращаться к базе данных.

  • TypeORM имеет встроенную поддержку кэширования, и вы можете настроить его использование.

const users = await userRepository
.createQueryBuilder("user")
.cache(true) // Enable caching
.getMany()
  • Дополнительно вы можете настроить срок действия кэша или использовать внешние инструменты кэширования, такие как Redis, для повышения эффективности.
const dataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "test",
password: "test",
database: "test",
cache: {
type: "redis",
options: {
host: "localhost",
port: 6379
}
}
});