JpaRepository Spring Data предоставляет огромный набор методов, упрощающих реализацию операций над базой данных. С их помощью вы можете сохранять, удалять и считывать объект сущности (entity object). Однако одна из немногих проблем, являющаяся следствием изобилия возможностей, которые дают нам эти интерфейсы, — это выбор правильного метода для вашего конкретного случая. И иногда это совсем не так просто, как могло бы показаться с первого взгляда. Хорошим примером этой проблемы являются методы findById, getOne, getById, и findOne. Судя по их именам, все они делают одно и то же. Так когда и какой из них вы должны использовать?

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

Как посмотреть реализации методов репозиториев Spring Data JPA

Прежде чем я перейду к деталям реализации этих четырех методов, я хочу быстро показать вам, как вам самим посмотреть реализацию любого из методов репозиториев Spring Data JPA. Вы можете прибегать к этому всякий раз, когда вы не до конца уверены в том, как работает метод или в чем разница между методами с похожими именами.

Одним из ключевых преимуществ Spring Data JPA является то, что он предоставляет реализации по умолчанию для всех своих репозиториев. Вы можете легко найти их с помощью инструментов, предоставляемых вашей IDE. Вам всего лишь нужно открыть один из интерфейсов ваших собственных репозиториев. Он будет наследоваться как минимум от одного из стандартных интерфейсов Spring Data JPA.

public interface ChessPlayerRepository extends JpaRepository<ChessPlayer, Long>{}

В данном случае это интерфейс JpaRepository. Вы можете посмотреть все реализации этого интерфейса с помощью встроенных функций вашей IDE. Если вы сделаете это для интерфейса JpaRepository, то вы найдете класс под названием SimpleJpaRepository. Это стандартная реализация интерфейса JpaRepository из Spring Data JPA со всеми его методами. Мы еще несколько раз вернемся к ней в этой статье.

4 метода, которые на первый взгляд делают одно и то же

Когда ваш репозиторий наследуется от JpaRepository из Spring Data JPA, он получает методы findById, getOne, getById, и findOne. Исходя из их имен, вы можете подумать, что они делают одно и то же.

Но Spring Data явно не стала бы давать нам 4 совершенно одинаковых метода под разными именами. Итак, давайте подробнее рассмотрим эти методы и найдем их отличия.

Метод findById

Интерфейс Spring Data JPA CrudRepository является родительским интерфейсом для JpaRepository, и именно в нем определен метод Optional findById(ID id). Интерфейс CrudRepository не привязан к JPA. Его определяет родительский проект Spring Data. Благодаря этому вы можете найти его различные реализации во всех модулях Spring Data.

Класс SimpleJpaRepository предоставляет уже специфичную для JPA реализацию метода findById. Как видно из следующего фрагмента кода, эта реализация основана на методе find, определенном EntityManager JPA и оборачивает полученный объект сущности в Optional.

public Optional<T> findById(ID id) {
 
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
 
    Class<T> domainType = getDomainClass();
 
    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }
 
    LockModeType type = metadata.getLockModeType();
 
    Map<String, Object> hints = new HashMap<>();
 
    getQueryHints().withFetchGraphs(em).forEach(hints::put);
 
    if (metadata.getComment() != null && provider.getCommentHintKey() != null) {
        hints.put(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment()));
    }
 
    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

Источник: https://github.com/spring-projects/spring-data-jpa/blob/main/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java#L296

Вызов метода find EntityManager вместо создания и выполнения запроса позволяет Hibernate использовать свои кэши 1-го и 2-го уровня. Это может повысить производительность, если объект сущности уже был извлечен из базы данных в рамках текущего сеанса или если сущность является частью кэша 2-го уровня. Это делает метод findById вашим лучшим вариантом, если вы хотите получить объект сущности со всеми его атрибутами по его атрибуту первичного ключа.

Основное отличие между методом findById и простым вызовом метода EntityManager.find заключается в том, что он учитывает LockModeType, который вы настроили для своего репозитория, и любой EntityGraph, связанный с методом репозитория.

LockModeType позволяет настроить оптимистическую или пессимистическую блокировку для обработки одновременных изменений извлеченных сущностей.

А EntityGraph позволяет вам определить ассоциации, которые вы хотите инициализировать при получении объекта сущности. Это предотвращает LazyInitializationExpections и помогает избежать проблемы n+1.

Методы getOne и getById

В Spring Data JPA 3 методы getOne и getById считаются устаревшими. Внутри них происходит вызов метода getReferenceById. Как мы можем понять из названия, этот метод возвращает ссылку на объект сущности, а не на сам объект сущности.

public T getReferenceById(ID id) {
 
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    return em.getReference(getDomainClass(), id);
}

Источник: https://github.com/spring-projects/spring-data-jpa/blob/main/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java#L351

Как я объяснил в своем руководстве по методу getReference JPA, Hibernate не выполняет SQL-запрос, когда вы вызываете метод getReference. Если сущность не является управляемой, Hibernate инстанцирует прокси-объект и инициализирует атрибут первичного ключа.

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

Отложенное выполнение SQL-запроса может вызвать проблемы и не дает никаких преимуществ, если вам обязательно нужно получить доступ к каким-либо атрибутам, не являющимся первичными ключами. Поэтому методы getOne и getById не очень подходят для сценариев, где вам нужно работать с информацией, представленной объектом сущности. Но они обеспечивают очень эффективный способ получить ссылку на объект, если вы хотите инициализировать ассоциацию. В таких случаях ссылка предоставляет всю информацию, необходимую Hibernate.

Методы findOne

Spring Data JPA предоставляет 2 версии метода findOne.

Одна из версий определяется интерфейсом QueryByExampleExecutor. Вы можете вызвать ее, чтобы найти объект сущности, соответствующий предоставленному вами примеру. После чего Spring Data JPA сгенерирует оператор WHERE на основе предоставленного объекта сущности и примера конфигурации.

// Sample player
ChessPlayer examplePlayer = new ChessPlayer();
examplePlayer.setFirstName("Magnus");
examplePlayer.setLastName("Carlsen");
 
Example<ChessPlayer> example = Example.of(examplePlayer);
Optional<ChessPlayer> player = playerRepo.findOne(example);

Как я уже объяснял в курсе по Spring Data JPA на Persistence Hub, вы можете указать, как и для каких атрибутов сущности Spring будет генерировать предикаты для вашего оператора WHERE. По умолчанию он создает запрос, который выбирает объект сущности и включает предикат равенства для каждого предоставленного атрибута.

2023-02-01 15:48:11.370 DEBUG 27840 --- [           main] org.hibernate.SQL                        : 
    select
        chessplaye0_.id as id1_1_,
        chessplaye0_.birth_date as birth_da2_1_,
        chessplaye0_.first_name as first_na3_1_,
        chessplaye0_.last_name as last_nam4_1_,
        chessplaye0_.version as version5_1_ 
    from
        chess_player chessplaye0_ 
    where
        chessplaye0_.first_name=? 
        and chessplaye0_.last_name=? 
        and chessplaye0_.version=0

Вторая версия ожидает объект Specification (спецификацию), определяющий оператор where запроса. Спецификация — это концепция, определенная в Domain Driven Design. Это слишком сложно, чтобы объяснять это все в рамках этой статьи, и вам не нужно разбираться в этом, чтобы понять различия между рассматриваемыми методами интерфейса JpaRepository. Если вы хотите узнать больше об этой фичи, я рекомендую вам пройти курс Spring Data JPA на Persistence Hub.

Основная идея концепции спецификации заключается в том, что каждая спецификация определяет базовое бизнес-правило, которые можно объединить в сложный набор правил. При переложении этой концепции на Spring Data JPA — каждая спецификация определяет небольшую часть оператора WHERE.

public class ChessPlayerSpecs  {
 
    public static Specification<ChessPlayer> playedInTournament(String tournamentName) {
        return (root, query, builder) -> {
            SetJoin<ChessPlayer, ChessTournament> tournament = root.join(ChessPlayer_.tournaments);
            return builder.equal(tournament.get(ChessTournament_.name), tournamentName);
        };
    }
 
    public static Specification<ChessPlayer> hasFirstName(String firstName) {
        return (root, query, builder) -> {
            return builder.equal(root.get(ChessPlayer_.firstName), firstName);
        };
    }
}

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

Specification<ChessPlayer> spec = ChessPlayerSpecs.playedInTournament("Tata Steel Chess Tournament 2023")
                                                  .and(ChessPlayerSpecs.hasFirstName("Magnus"));
Optional<ChessPlayer> player = playerRepo.findOne(spec);

Краткие описания обоих вариантов методов findOne и два примера кода уже раскрывают разницу между этими методами и ранее рассмотренными методами findById, getOne, and getById. Обсуждавшиеся ранее методы получают объект сущности или ссылку только по его первичному ключу. Обе версии метода findOne позволяют находить объекты сущностей по атрибутам, не являющимся первичными ключами, и определять запросы любой сложности.

Заключение

Стандартные интерфейсы репозиториев Spring Data JPA определяют различные методы, которые вы можете использовать для реализации слоя хранения данных. Некоторые из них могут иметь похожие имена, но их функциональность отличается. При использовании любого из этих методов в своем коде вы должны знать, как они работают под капотом и при каких обстоятельствах вы должны их использовать.

В этой статье мы подробно рассмотрели несколько методов, которые возвращают  объект сущности. Итак, вы должны использовать:

  • метод findById для получения объекта сущности по его первичному ключу и инициализации его атрибутов.

  • метод getReferenceById для получения ссылки на объект сущности, который можно использовать для инициализации ассоциаций. Методы getOne и getById устарели и под капотом вызывают метод getReferenceById.

  • метод findOne, если вы хотите использовать запрос Spring Data JPA на основе примера или спецификации, которая определяет запрос, возвращающий один объект сущности.

И это лишь пара методов, предлагаемых JpaRepository, которые имеют похожие имена, но представляют разные функции. Другими примерами являются методы save(), saveAndFlush(), and saveAll(), о которых мы поговорили в предыдущей статье.


Приглашаем на открытое занятие «Создание игры на Java и LibGDX с нуля».

На вебинаре посмотрим, как можно за несколько часов с нуля сделать небольшую двумерную игру на Java. Изучать принципы написания кода и продумывать логику игры будем прямо на ходу. При работе на Java вы вряд ли столкнетесь с подобными задачами (создание игр), однако подобная тема очень хорошо показывает, как изменение кода приводит к изменению поведения программ. В результате урока вы:
- увидите, как пишется код и создаются программы на языке Java;
- узнаете, из каких базовых блоков строятся программы.

Записаться на открытый урок можно на странице специализации "Java Developer".

Комментарии (0)