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.sql (и schema.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…, есть также ключевые слова get…query…, 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 в данном случае можно и не писать — оно необходимо в случае неоднозначности (отделения названия полей родительской от вложенной сущности). Но для читаемости в любом случае полезно.
Сравнение: 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()); }
Другие ключевые слова в документации.
Логические условия 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.
Обращение в полям вложенной сущности: знак “_” — ошибка в тексте. «Обращение к полям…»
спасибо, исправлено.
Было бы интересно почитать про запросы через аннотацию Query, особенно для каких-нибудь многоуровневых джоинов. Например, есть три сущности: доска, колонки и таски (аля трелло), как правильнее было бы написать код, чтобы сразу заджоинить все три таблицы, если доска содержит много колонок, а колонка содержит много тасок.