Стратегии извлечения
От автора
Это первая статья из трех или четырех, в которых я попробую разобрать возможные причины возникновения и предложу варианты решения проблемы N+1. Первые две статьи, должны раскрыть причины, которые могут приводить к проблеме N+1. В третьей и возможно четвертой статье я рассмотрю варианты выявления этой проблемы и предложу рабочие варианты её решения.
Проблемма N+1, если её специально не отлавливать, почти наверняка проявится и будет тихо и незаметно увеличивать время отклика вашего приложения. Коварство N+1, в том, что она может быть совсем незаметна. При постепенном заполнении базы, она может увеличивать задержку всего на доли секунд, так что при постоянной работе с приложением, заметить увеличение времени отклика, будет очень сложно.
Введение
При работе с базой данных, нужно стремиться к тому, чтоб с одной стороны не усложнять запрос, а с другой всегда получать исчерпывающие данные, то есть, запрос не должен доставать из базы больше или меньше информации. Следует помнить, что избыточная сложность запросов, так же как их количество, замедляют работу приложения.
Стратегия извлечения данных
Это довольно простая тема, и тем не менее, я попробую подробно разобрать вопрос.
Стратегия извлечения данных объясняет, как должны извлекаться из СУБД, связанные данные. Допустим, у нас есть сущность Книга, у которой помимо полей со стандартными типами данных, есть дополнительные поля со связанными или ассоциированными сущностями Автор, Жанр и Отзывы. Может возникнуть вопрос, нужно ли доставать из базы связанного Автора, например когда необходимо показать список книг? Или у нас есть сущность Пользователь у которого есть Друзья, Фотографии, Увлечения. Нужно ли при каждом обращении к базе отдавать пользователю связанные фотографии? Стратегия извлечения данных должна ответить на эти вопросы.
Фактически, стратегия извлечения данных, отвечает только на один простой вопрос: ОБЯЗАТЕЛЬНО или НЕ ОБЯЗАТЕЛЬНО доставать из СУБД связанные сущности?
Так, если существует требование, что вместе с книгой, пользователь всегда должен видеть её автора, нам ОБЯЗАТЕЛЬНО, при каждом запросе книги, придется достать из базы и её автора. Или, если фотографии или увлечения пользователя, нужны только на какой-то одной странице, НЕ ОБЯЗАТЕЛЬНО усложнять ими запрос при каждом обращении к базе.
Существуют две стратегии извлечения данных: EAGER и LAZY.
название | описание |
FetchType.LAZY | в переводе ЛЕНИВАЯ, означает, что данные НЕ ОБЯЗАТЕЛЬНО доставать из базы вместе с основной сущностью |
FetchType.EAGER | можно перевести как ЖАДНАЯ, предполагает, что данные ОБЯЗАТЕЛЬНО доставать из базы вместе с основной сущностью |
Обе стратегии указываются при объявлении типа связи:
Поскольку для всех типов есть значения по умолчанию, не обязательно явно указывать соответствующую стратегию.
Ниже приведена таблица, в которой указаны значения по умолчанию.
Существует, не настойчивая рекомендация, всегда явно указывать стратегию, даже если она, не отличается от значения по умолчанию. Это сделает код более понятным.
Итак, EAGER стратегия показывает, что связанные данные должны быть извлечены вместе с с основной сущностью, а LAZY стратегия показыват, что связанные данные не будут извлечены вместе с основной сущностью.
Примеры
Допустим у нас есть класс:
@Entity
@Table
@Data
public class Book {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "author_id")
private Author author;
@OneToMany(fetch = FetchType.LAZY)
private List comments;
}
Для запроса на получение книги будем использовать следующий метод:
public interface BookRepository extends CrudRepository {
@Query("select b from Book b where b.id = ?1")
Book getBookById(Long id);
}
Так же, необходимо отметить, что мы не будем обращаться к полям объекта, а только сделаем запрос в базу и присвоим объект ссылке. О том, почему это важно, будет рассказано в статье про proxy.
Код на создание объекта выглядит так:
//...
System.out.println("до...");
Book book = bookRepository.getBookById(1L);
System.out.println("после...");
//...
Теперь, если мы попробуем получить книгу по id, в базу данных улетят следующие запросы:
до...
Hibernate:
select
book0_.id as id1_1_,
book0_.author_id as author_i2_1_
from
book book0_
Hibernate:
select
author0_.id as id1_0_0_,
author0_.name as name2_0_0_
from
author author0_
where
author0_.id=?
после...
Как видно из этих запросов, в базу данных вначале был отправлен запрос на получение книги из таблицы book, а затем благодаря тому, что для поля author
мы указали стратегию EAGER, был выполнен еще один запрос в таблицу author, для получения связанного с книгой автора. Дополнительных запросов на получение комментариев небыло, поскольку для отношения comments, выбрана стратегия LAZY.
Попробуем для полей author и comments поменять местами стратегии. Для поля comments укажем стратегию EAGER, а для поля author укажем стратегию LAZY:
//...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
@OneToMany(fetch = FetchType.EAGER)
private List comments;
//...
Если мы выполнем тот же запрос на получение книги по id, мы увидем уже другие запросы в базу:
до...
Hibernate:
select
book0_.id as id1_1_,
book0_.author_id as author_i3_1_,
book0_.name as name2_1_
from
book book0_
Hibernate:
select
comments0_.book_id as book_id1_2_0_,
comments0_.comments_id as comments2_2_0_,
comment1_.id as id1_3_1_,
comment1_.comment as comment2_3_1_,
comment1_.name as name3_3_1_
from
book_comments comments0_
inner join
comment comment1_
on comments0_.comments_id=comment1_.id
where
comments0_.book_id=?
после...
Можно увидеть, что в следствие изменения стратегии для полей, после запроса в таблицу book, был выполнен еще один select, на получение комментариев, в то время как запроса на получение автора не было.
Продолжая логику, если мы, для обоих полей, укажем стратегию LAZY, будет выполнен только один запрос в таблицу book, если же укажем EAGER, в базу данных будет отправлено 3 запроса.
Стратегия извлечения относится к полю класса, поэтому будет применяться при любом извлечении основной сущности из базы. Например получение списка объектов, приведет к такому же результату.
Давайте, удалим явное указание стратегии и оставим для полей, стратегии по умолчанию. Напомним, для @ManyToOne это EAGER, а для @OneToMany — LAZY.
Поля будут выглядеть следующим образом:
//...
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
@OneToMany
private List comments;
//...
Теперь попробуем сделать запрос на получение всех книг из базы:
//...
System.out.println("до...");
List books = bookRepository.getBooks();
System.out.println("после...");
//...
В консоль будут выведены следующие запросы:
до...
Hibernate:
select
book0_.id as id1_1_,
book0_.author_id as author_i3_1_,
book0_.cover_id as cover_id4_1_,
book0_.name as name2_1_
from
book book0_
Hibernate:
select
author0_.id as id1_0_0_,
author0_.name as name2_0_0_
from
author author0_
where
author0_.id=?
Hibernate:
select
author0_.id as id1_0_0_,
author0_.name as name2_0_0_
from
author author0_
where
author0_.id=?
после...
Как видно из лога выше, для списка из двух книг, будет сделан один запрос на получение книг, и два запроса на получение авторов.
Стратегия извлечения данных, это простая и понятная концепция и на этом можно было бы закончить, но это в идеальном мире.
Реальность
Хорошие новости в том, что концепцию стратегии извлечения данных достаточно просто понять. Плохие новости, что эти стратегии работают не всегда, не везде, а иногда и совсем не так как должны. В частности, на приведенных примерах можно увидеть пример классической проблемы N+1. Мы пытаемся получить какое-то количество книг из базы, но с каждой книгой получаем минимум один дополнительный запрос на получение автора или отзывов.
Кроме того, и значения по умолчанию и явное указание стратегии не гарантируют, что она будет применена, то есть существует вероятность, получить прямо противоположный результат. В случае EAGER вы можете не увидеть дополнительный запрос или словить LazyInitializationException, а в случае использования стратегии LAZY, вы можете все же увидеть дополнительный запрос в базу.
Если вы попытаетесь использовать стратегию EAGER, для отношений @ManyToMany, то вероятно словите LazyInitializationException.
Для отношений @OneToOneв дочерней сущности, то есть, когда внешний ключ ссылается из родительской таблицы на текущую таблицу, указание стратегии LAZY не имеет смысла, поскольку дополнительный запрос все равно будет выполнен. Так, если б в приведенном выше примере, в таблице author был внешний ключ на таблицу book, таблица book являлась бы для таблицы author дочерней. В этом случае, указание стратегии LAZY для поля author, накак не повлияло бы на результат запроса в базу.
Данные особенности связаны с концепцией proxy, о которой будет рассказано в следующей статье.
Лучшие практики
Рекомендуется всегда, явно указывать стратегию, даже если она, не отличается от значения по умолчанию. Это сделает код более понятным.
Будьте внимательны к выбору стратегии. Неправильное указание стратегии извлечения данных, может значительно замедлить работу вашей программы.
Стремитесь к тому, чтоб перенести ответственность за извлечение данных, с уровня сущности на уровень бизнес логики. Для этого, рекомендуется для всех отношений указывать стратегию LAZY, а наличие необходимых данных обеспечивать запросами в базу.
Использование стратегии EAGER, нежелательно по ряду причин. Во-первых, разные запросы пользователя, как правило, предполагают отображение различных данных. Если вы всегда будете доставать из базы EAGER отношения, они могут подгружаться и туда где они не нужны. Это приведет к усложнению запроса и соответственно замедлению работы. Во-вторых, создание ненужных объектов будет занимать память. И в-третьих, использование, стратегии EAGER, может приветси к проблемме N+1.
Заключение
Концептуально, стратегия извлечения данных, тема достаточно несложная, однако относиться к ней нужно очень внимательно, поскольку неправильный выбор стратегии, может оказать сильное влияние на скорость работы вашей программы. Кроме того, неправильно выбранная стратегия, может стать первой причиной, которая приведет к проблемме N+1.