Spring Data JPA: запросы, генерируемые по имени метода

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

Все запросы так не напишешь, но простые можно. Ниже рассмотрим примеры.

Модель

Класс User:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String name;
    @OneToMany(mappedBy = "user")
    private Set<Account> accounts;
    
    // getters/setters/constructors
}

У пользователя может быть несколько счетов: они хранятся в коллекции accounts.

Класс Account:

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String name;
    private long amount;

    @ManyToOne
    private User user;

   // getters/setters/constructors
}

Заполним таблицы данными.

Данные

Данные находятся в файле data.sql. Скрипт data.sqlschema.sql) запускается благодаря настройке в файле application.properties:

spring.datasource.initialization-mode=always

Итак, с помощью файла data.sql добавим 5 пользователей (два с именем John). Они получат последовательные автоматически сгенерированные id с 1 по 5. 

insert into user (name) values ('Ivan');
insert into user (name) values ('John');
insert into user (name) values ('Petr');
insert into user (name) values ('John');
insert into user (name) values ('Artem');

Только для пользователей с id=1 и id=2 добавим счета:

insert into account (name, amount, user_id) values ('ac1Iv', 10, 1);
insert into account (name, amount, user_id) values ('ac2Iv', 11, 1);
insert into account (name, amount, user_id) values ('ac3Iv', 120, 1);
insert into account (name, amount,  user_id) values ('ac4Iv', 0, 1);

insert into account (name, amount,  user_id) values ('ac1J', 50, 2);
insert into account (name, amount,  user_id) values ('ac2J', 20, 2);
insert into account (name, amount,  user_id) values ('ac3J', 100, 2);

Простейший пример — поиск пользователей по имени — find

Чтобы декларировать методы, необходимо расширить CrudRepository (или JpaRepository).

Сначала найдем пользователей по имени:

public interface UserRepository extends JpaRepository<User, Long> {

    List<User> findByName(String str);
    // другие методы
}

В консоли отобразится запрос, который сгенерировал Spring Data:

select user0_.id as id1_1_, user0_.name as name2_1_ 
   from user user0_ 
where user0_.name=?

Сгенерированный SQL отображается благодаря настройке:

spring.jpa.show-sql=true

Помимо find…, есть также ключевые слова getquery…, read… — все они имеют один и тот же смысл.

Можно также писать findAll…, findUsers.., findUser..

То есть наш метод можно записать любым из возможных вариантов:

List<User> getByName(String name);
List<User> queryByName(String name);
List<User> readByName(String name);

List<User> findAllByName(String name);
List<User> findUserByName(String name);
List<User> findUsersByName(String name);

Вышеперечисленные методы генерируют тот же самый запрос.

Возвращаемый тип

Кроме того, тип возвращаемого значения можно сменить на просто User или Optional<User>:

Optional<User> findByName(String name);
или
User findByName(String name);

Правда, в этом случае при нахождении более одного User будет выброшено NonUniqueResultException.

Все возможные возвращаемые типы есть в документации.

Ограничение количества: findFirst

Зато можно выбрать только первую запись:

Optional<User> findFirstByName(String name);

И вышеприведенный метод не выбросит исключений.

Выбрать первые N записей (например, 2) можно так:

List<User> findFirst2ByName(String name);

Генерируемый SQL:

select user0_.id as id1_1_, user0_.name as name2_1_
    from user user0_ 
where user0_.name=? limit ?

Обращение к полям вложенной сущности: знак «_»

Можно в запросе учесть не только поля самого User, но и accounts. Например, следующий запрос ищет всех пользователей, у которых в названии одного из счетов есть заданная строка:

List<User> findDistinctUserByAccounts_NameContaining(String str);

Генерируемый SQL:

select distinct user0_.id as id1_1_, user0_.name as name2_1_ 
   from user user0_ 
   left outer join account accounts1_ 
   on user0_.id=accounts1_.user_id 
where accounts1_.name like ? escape ?

Знак подчеркивания перед Name в данном случае можно и не писать — оно необходимо в случае неоднозначности (отделения названия полей родительской от вложенной сущности). Но для читаемости в любом случае полезно.

Обратите внимание, что мы употребили также ключевое слово Distinct, потому что join может сформировать дубликаты. А повторяющиеся User в списке нам не нужны.

Сравнение: containing, startsWith, greaterThan

Мы уже использовали ключевое слово containing, что означало «поле содержит строку». Есть также startingWith:

List<User>  findByNameIsStartingWith(String str);

А для чисел есть GreaterThan. Найдем пользователей, у которых сумма счета превышает заданное число:

List<User>  findDistinctByAccounts_AmountIsGreaterThan(long n);

Генерируемый SQL:

select distinct user0_.id as id1_1_, user0_.name as name2_1_ 
    from user user0_ 
left outer join account accounts1_ 
on user0_.id=accounts1_.user_id 
where accounts1_.amount>?
Тест:
@Test
void t6() {

    List<User> users = userDao.findDistinctByAccounts_AmountIsGreaterThan(15);
    Assertions.assertEquals(2, users.size());

}
Без ключевого слова Distinct в этом тесте появляются дубликаты, потому что join генерирует одну запись для Ivan и целых 3 записи для John (у которого три аккаунта более 15).

Другие ключевые слова в документации.

Логические условия or, and

Можно соединять условия для полей символами or, and. Например, найдем пользователей с заданным именем, либо (еще) тех у которых имеется счет с  суммой больше заданной:

List<User> findDistinctByNameOrAccounts_AmountIsGreaterThan(String name, long amount);

Генерируемый SQL:

select distinct user0_.id as id1_1_, user0_.name as name2_1_ 
   from user user0_ 
   left outer join account accounts1_
   on user0_.id=accounts1_.user_id 
where user0_.name=? or accounts1_.amount>?

Iterable как параметр

Можно  задать список в качестве параметра:

List<User> findByNameIn(Iterable name);

Пример использования:

@Test
void t8() {
    List<User> users = userDao.findByNameIn(Arrays.asList("Ivan", "Petr"));
    Assertions.assertEquals(2, users.size());
}

Желательно параметр делать именно Iterable, а не Set и List, чтобы при вызове метода не требовалось лишних преобразований.

Pagination

Можно создавать запросы для постраничного извлечения сущности:

Page<User> findByName(String name, Pageable pageable);

Ниже пример использования: в запросе передается PageRequest.of(0,2) с номером страницы и количеством элементов на странице:

@Test
void t10() {
    Page<User> userPage = userDao.findByName("John",PageRequest.of(0,2));
    Assertions.assertEquals(2, userPage.getTotalElements());
}

Страницы нумеруются с 0.

Генерируемый SQL:

Hibernate: select user0_.id as id1_1_, user0_.name as name2_1_ from user user0_ where user0_.name=? limit ?
Hibernate: select count(user0_.id) as col_0_0_ from user user0_ where user0_.name=?

Стоит отметить, что метод постраничного получения всех подряд сущностей уже есть в PagingAndSortingRepository:

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
...
      Page<T> findAll(Pageable pageable);
...
}

Сортировка: orderBy и Sort

Найти пользователей, что имя содержит заданную строку и упорядочить результат по имени:

List<User> findByNameContainingOrderByNameAsc(String str);

Или передать в параметре объект Sort:

List<User> findByNameContaining(String str, Sort sort);

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

@Test
void t12() {
    Sort sort = Sort.by(Sort.Direction.ASC, "name");
    List<User> users= userDao.findByNameContaining("e", sort);
    Assertions.assertEquals(2,users.size());
    Assertions.assertEquals("Artem",users.get(0).getName());
}

Оба метода генерируют одинаковый SQL:

select user0_.id as id1_1_, user0_.name as name2_1_ 
from user user0_ 
where user0_.name like ? escape ? 
order by user0_.name asc

Подсчет количества: count

Есть еще ключевое слово count для подсчета строк. Например, найти количество пользователей с заданным именем:

int countAllByName(String name);

Spring Data генерирует следующий SQL:

select count(user0_.id) as col_0_0_ 
   from user user0_ 
where user0_.name=?

Найти количество пользователей с названием аккаунта, содержащим строку:

int countDistinctUserByAccounts_NameContaining(String str);

Удаление сущности: remove и delete

Наконец, можно писать и методы удаления сущности с помощью ключевых слов remove и delete:

int deleteByName(String name);

void removeUserByName(String name);

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

Генерируемый SQL:

select user0_.id as id1_1_, user0_.name as name2_1_ from user user0_ where user0_.name=?

delete from user where id=?

Исходный код

Код примеров есть на GitHub.

Spring Data JPA: запросы, генерируемые по имени метода: 3 комментария

  1. Обращение в полям вложенной сущности: знак “_” — ошибка в тексте. «Обращение к полям…»

  2. Было бы интересно почитать про запросы через аннотацию Query, особенно для каких-нибудь многоуровневых джоинов. Например, есть три сущности: доска, колонки и таски (аля трелло), как правильнее было бы написать код, чтобы сразу заджоинить все три таблицы, если доска содержит много колонок, а колонка содержит много тасок.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *