Мы рассматривали запросы, генерируемые из имени метода. Так удобно писать простые запросы. Но если в запросе более 1-2 параметров или несколько join, то удобнее использовать аннотацию @Query.
Для этого в репозитории создаем метод, аннотированный @Query, и внутри аннотации прописываем запрос — можно как JPQL, так и native SQL.
Ниже приведен пример.
Использование @Query
public interface PostRepository extends JpaRepository<Post, Long> { @Transactional(readOnly = true) @Query("select p from Post p where p.user.id=:id and p.title like :title") List<Post> findMySuperPosts(String title, long id); }
Запрос выбирает посты данного пользователя по заголовку.
Посты и пользователи находятся в отношении @ManyToOne. Класс Post:
@Entity @Data @NoArgsConstructor public class Post { @Id @GeneratedValue(generator = "sequence") private Long id; private String title; private String text; @ManyToOne private User user; }
Класс User:
@Data @Entity @NoArgsConstructor public class User { @Id @GeneratedValue(generator = "sequence") private Long id; private String email; private String nickname; private String password; private String role = "ROLE_USER"; private boolean locked = false; }
Native Query
Такой же запрос можно написать на SQL, указав nativeQuery = true:
@Transactional(readOnly = true) @Query(nativeQuery = true, value = "select * from post p where p.user_id=:id and p.title like :title") List<Post> findMySuperPostsNative(String title, long id);
Pagination и Sorting
Можно получать результат постранично, указав в качестве параметра Pagable:
@Transactional(readOnly = true) @Query("select p from Post p where p.title like :title") List<Post> findSuperPosts(String title, Pageable pageable);
Создавать запрос страницы можно так:
Pageable pageable= PageRequest.of(0, 2, Sort.by("title"));
Выше приведен запрос первой страницы (страницы нумеруются с 0), при этом на странице должно быть два элемента. Задано упорядочивание выборки по title. Подробнее о постраничном выводе и сортировке тут.
Формируемый SQL такой:
select post0_.id as id1_0_, post0_.text as text2_0_, post0_.title as title3_0_, post0_.user_id as user_id4_0_ from post post0_ where post0_.title like ? order by post0_.title asc limit ? select user0_.id as id1_1_0_, user0_.email as email2_1_0_, user0_.locked as locked3_1_0_, user0_.nickname as nickname4_1_0_, user0_.password as password5_1_0_, user0_.role as role6_1_0_ from user user0_ where user0_.id=?
Аннотация @Modifying
С помощью @Query можно не только читать, но и обновлять базу. Но в таких запросах необходима дополнительная к @Query аннотация @Modifying.
update
Пример обновления заголовков всех постов пользователя:
@Modifying @Transactional @Query("update Post p set p.title=:title where p.id=:id") int setFixedPostTitleFor(String title, long id);
delete
Пример удаления всех постов пользователя:
@Modifying @Transactional @Query("delete from Post p where p.user.id=:id") int deleteInBulkByUserId(long id);
Стоит отметить, что так посты удаляются пакетно, поскольку генерируется один SQL :
delete from post where user_id=?
Если же использовать генерируемый по имени метод, то посты удаляются по одному:
@Transactional int deleteByUserId(long id);
SQL отправляется свой для каждого поста:
Hibernate: delete from post where id=? Hibernate: delete from post where id=? Hibernate: delete from post where id=?
@Transactional
Обратите внимание, что методы мы аннотировали @Transactional, поскольку писали их сами. (Если бы использовали стандартные методы репозитория, унаследованные от SimpleJpaRepository, то они уже @Transactional.)
Все методы должны быть в рамках транзакции, другое дело что @Transactional может быть на уровне сервиса или теста. Но в данном случае тест у нас не @Transactional, класс аннотирован @SpringBootTest, а не @DataJpaTest (внутри которого есть @Transactional). Это потому, что @DataJpaTest откатывает транзакции и некоторые SQL не логируются (возможно, потому что не выполняются?). А хотелось бы видеть все сгенерированные SQL.
@SpringBootTest public class PostRepositoryTest { @Autowired private PostRepository postRepository; @DirtiesContext @DisplayName("посты должны удаляться по user.id единым запросом в @Query") @Test void titlesShouldBeDeletedByIdInBulk() { int n = postRepository.deleteInBulkByUserId(1l); Assertions.assertEquals(3, n); } .... }
А для того, чтобы подтвержденные транзакции не портили базу после каждого теста, каждый тест аннотирован @DirtiesContext — это пересоздает H2 базу. Не оптимально, зато наглядная консоль с выводом всех SQL-запросов.
Итоги
Исходный код есть на GitHub.