Мы рассматривали запросы, генерируемые из имени метода. Так удобно писать простые запросы. Но если в запросе более 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.