ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA 쿼리메서드
    백엔드 2023. 8. 18. 23:45
    728x90
    반응형
    SMALL

    이전 시간에 Java의 ORM 표준 스펙인 JPA을 활용할 수 있도록 해주는 JpaRepository의 내부 코드를 살펴보았습니다. 이번에는 좀 더 복잡한 쿼리들을 repository 내부에 정의하여 사용하는 방법에 대해 배워보도록 하겠습니다.

     

     

     

    쿼리 메서드 선언 위치

    JpaRepository를 상속받고 있는 repository interface 내부에 Spring Data Jpa에서 명시하고 있는 키워드를 이용해 쿼리메서드를 정의하고 사용할 수 있습니다.

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        // 선언 위치
    }

     

     

    리턴 타입

    네이밍 규칙으로 만들어진 메서드들에 의해 반환되는 값들의 타입을 개발자가 정의할 수 있습니다.

    쿼리 메서드를 통해 반환될 수 있는 값이 List라면 List로 매핑된 Entity를 리턴 타입을 정의할 수도 있고, Set이라면 Set으로 매핑된 Entity를 리턴 타입으로 정의할 수 있습니다.

    개발 스택에 맞춰 리턴 타입을 정해주면 Spring Data JPA에서 자동으로 데이터를 매핑해서 반환해줍니다.

     

    아래 Spring Data JPA Document에서 Return Types 종류를 확인할 수 있습니다.

     

    Spring Data JPA - Reference Documentation

    Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

    docs.spring.io

     

     

    네이밍 규칙

    Spring Data JPA Document 상에 쿼리메서드의 네이밍 규칙들에 대해 정의해주고 있습니다. 이를 이용해 쿼리메서드들을 정의할 수 있습니다.

    쿼리 메서드 키워드들
    쿼리 메서드 키워드들

    기본 예시

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        User findByName(String name);
        
        User findByByName(String name); //에러!
    }

    위의 키워드들의 경우 직관적으로 읽을 경우 그 쿼리가 무엇인지 판단하기 용이하게 제작되어 있고 메서드 제작에 어느정도 자유도가 주어졌습니다. 단, 자유도가 주어진만큼 키워드가 원하는 방식으로 동작하도록 네이밍을 잘 해주어야합니다.

    findByByName의 경우 네이밍 규칙에 오류가 있지만 실제 오류를 나타내어주지는 않습니다. 컴파일 시 문제가 없고 실제 runtime 상에서 오류를 포착하게 되는 경우가 생기므로 주의해야합니다.

     

    Top, First 키워드

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        User findTop1ByName(String name);
        
        User findFirst1ByName(String name);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findTop1ByName("user1");
            System.out.println(userRepository.findFirst1ByName("user1");
        }
    }

    위의 top, first 키워드의 경우 해당 쿼리를 통해 얻어온 데이터의 첫 번째 데이터를 얻어오게 됩니다.

    top, first 키워드 뒤에 숫자가 붙을 경우 해당 개수만큼 데이터를 얻어오게 됩니다.

     

    top, first의 키워드를 보고 마지막 데이터의 경우 last를 넣으면 되지 않을까 생각할 수 있습니다. 하지만, last와 같은 키워드는 없으므로 만약 마지막 원소를 알고 싶다면, 현재 쿼리의 정렬을 반대로 뒤집은 것에 top, first를 붙이면 됩니다.

    (findLast1ByName()과 같은 메서드를 만든다면 이 메서드는 그냥 findByName()과 동일한 데이터를 반환하게 됩니다.)

     

     

    and, or

    where 문에서 원하는 조건을 추가해줄 때 사용합니다.

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        User findByNameAndEmail(String name,String email);
        User findByNameOrEmail(String name, String email);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByNameAndEmail("user1","user1@gmail.com"));
            System.out.println(userRepository.findByNameOrEmail("user2","user2@gmail.com"));
        }
    }

    and 키워드의 경우 해당 컬럼 명에 들어온 인자들의 조건에 모두 맞는 데이터를 반환해줍니다.

    or 키워드의 경우 해당 컬럼 명에 들어온 인자들의 조건 중 하나라도 맞는 데이터를 반환해줍니다.

     

     

    대소 구분

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findByIdAfter(Long id);
        List<User> findByIdBefore(Long id);
        
        List<User> findByIdGreaterThan(Long id);
        List<User> findByIdGreaterThanEqual(Long id);
        
        List<User> findByIdLessThan(Long id);
        List<User> findByIdLessThanEqual(Long id);
        
        List<User> findByIdBetween(Long id1, Long id2);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByIdAfter(1L);
            System.out.println(userRepository.findByIdBefore(3L);
            
            System.out.println(userRepository.findByIdGreaterThan(1L);
            System.out.println(userRepository.findByIdGreaterThanEqual(1L);
            
            System.out.println(userRepository.findByIdLessThan(3L);
            System.out.println(userRepository.findByIdLessThanEqual(3L);
            
            System.out.println(userRepository.findByIdBetween(1L,3L);
        }
    }

    before, lessThan의 경우 모두 인자로 들어온 값보다 작은 값을 반환합니다. 시간의 경우 인자로 들어온 값보다 이전의 데이터들이 반환되게 됩니다.

    after, greaterThan의 경우 모두 인자로 들어온 값보다 큰 값을 반환합니다. 시간의 경우 인자로 들어온 값보다 이후의 데이터들이 반환되게 됩니다.

    before,after과 lessThan, greaterThan의 차이점은 ==을 붙일 수 있다는 점입니다.

    between 키워드의 경우 인자로 들어온 값을 포함한 범위의 데이터를 반환합니다. 즉 위의 예시에서는 1L <= id <= 3L인 데이터가 반환되게 됩니다. (findByIdbetween 키워드는 findByIdgreaterThanAndIdLessThanEqual과 동일합니다.)

     

    위의 키워드들은 논리상의 오류(컴파일 상의 오류는 없으나 의도치 않게 동작될 경우)가 발생할 확률이 높으므로 ==의 포함 여부와 같은 것들을 잘 파악하고 있는 것이 중요할 것 같습니다.

     

     

    isNotNull, isNotEmpty

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findByEmailIsNotNull();
        
        List<User> findByEmailIsNotEmpty();
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByIdIsNotNull());
            System.out.println(userRepository.findByIdIsNotEmpty()); // 에러
        }
    }

    isNotNUll의 경우 해당 컬럼의 값이 Null인 데이터를 반환해줍니다.

    isNotEmpty의 경우 해당 컬럼이 collection 타입인 경우에만 사용 가능합니다. collection type의 컬럼이 not empty인 데이터만 반환해줍니다. 릴레이션 관계에 있는 컬럼의 데이터에 대해 파악할 경우 사용됩니다.

     

     

    in

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findByNameIn(List<String> name);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByNameIn(Lists.newArrayList("user1","user2"));
        }
    }

    in은 인자로 들어온 List 형태의 값에 존재하는 데이터를 반환해줍니다.

    위와 같은 경우 2개밖에 되지 않지만 실제 사용할 경우 어떤 쿼리에 대해 반환된 값들을 넣어서 사용할 수도 있습니다. 만약, 인자로 들어오는 값들이 많을 경우 성능 이슈가 발생할 수 있기 때문에 인자로 들어올 수 있는 데이터의 개수를 미리 파악하고 사용하는 것이 좋을 것입니다.

     

     

    startingWith, endingWith, contains, like

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findByNameStartingWith(String name);
        
        List<User> findByNameEndingWith(String name);
        
        List<User> findByNameContains(String name);
        
        List<User> findByNameLike(String name);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByNameStartingWith("user"));
            System.out.println(userRepository.findByNameEndingWith("er3"));
            System.out.println(userRepository.findByNameContains("ser"));
            System.out.println(userRepository.findByNameLike("%ser%"));
        }
    }

    startingWith, endingWith, contains 키워드의 메서드는 like 검색이 실행됩니다.

    like의 경우 %를 붙여 어느 방향으로 like 검색을 할 거인지 알려주어야 합니다.

     

     

    is, equals

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findByName(String name);
        List<User> findByNameIs(String name);
        List<User> findByNameEquals(String name);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByName("user1"));
            System.out.println(userRepository.findByNameIs("user1"));
            System.out.println(userRepository.findByNameEquals("user1"));
        }
    }

    위의 세 쿼리메서드는 모두 같은 결과를 가져옵니다.

    어떨 때에 어떤 네이밍 규칙을 가져가는 것이 가독성을 높일 것인지 생각해서 사용할 수 있습니다.

     

     

    orderBy

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        User findTop1ByNameOrderByIdDesc(String name);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findTop1ByNameOrderByIdDesc("user"));
        }
    }

    orderBy를 통해 반환되는 데이터를 정렬시켜줄 수 있습니다.

    Desc의 경우 내림차순, Asc의 경우 오름차순으로 정렬해줍니다.

    위의 예시의 경우 name컬럼이 "user"인 데이터를 Id에 대해 내림차순으로 정렬한 값 중 첫번째 값이 반환됩니다.

     

     

    중첩 정렬

    만약 두 개의 컬럼에 대한 정렬을 하게 될 경우 이어서 붙여주면 됩니다.

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findByNameOrderByIdDescEmailAsc(String name);
    }

    위의 예시는 Name 컬럼에 인자로 들어온 값과 동일한 데이터들 중 id 내림차순으로, email의 오름차순으로 정렬된 데이터를 반환하게 됩니다.

     

    Sort 인자 사용 정렬

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findByName(String name, Sort sort);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByName("user1",Sort.by(Order.desc("id"))));
        }
    }

    Sort인자에 어떤 컬럼에 대한 정렬을 할 것인지 넣어주면 됩니다.

    Order.desc() 는 내림차순, Order.asc() 는 오름차순 정렬이 됩니다.

    각 desc(), asc() 인자값에 정렬시킬 컬럼 명을 넣어주면 됩니다.

     

    Sort.by의 경우 반복되는 인자도 받을 수 있게 되므로 중첩 정렬을 사용할 경우 여러 개의 Order 인자를 넣어주면 됩니다.

    (예: Sort.by(Order.desc("id"), Order.asc("email")) )

     

    OrderBy 키워드 vs Sort 클래스 매개 변수

    OrderBy 키워드를 사용한 메서드의 경우 메서드 명이 길어질 수 있으므로 가독성이 떨어지는 경우가 있습니다.

    Sort 클래스를 사용하는 경우 Sort 클래스를 인자로 받을 수 있는 findByXxx 메서드 한개를 선언해주고 정렬을 할지 안 할지 선택해서 사용할 수 있게 해주므로 자유도가 높아질 수 있고 코드의 가독성 또한 높아질 수 있다는 장점이 있습니다. 하지만, 이런 자유도가 주어진만큼 하나의 정렬 방식이 다른 여러 코드에서도 사용될 경우 계속해서 선언해서 사용해야 된다는 단점 또한 생길 수 있습니다.

     

     

    Page

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        Page<User> findByName(string name, Pageable pageable);
    }
    
    
    @SpringBootTest
    class UserRepositoryTest {
        @Autowired
        UserRepository userRepository;
        
        @Test
        void test(){
            System.out.println(userRepository.findByName("user1", PageRequest.of(<page>,<size>,<sort>)).getContent());
        }
    }

    리턴 타입인 Page 인터페이스는 페이징에 대한 응답값입니다. Page 인터페이스는 데이터 묶음의 부분 집합에 대한 정보를 나타내는 Slice를 상속받고 있습니다. 이 Slice의 getContent() 메서드를 통해 해당 페이지의 데이터들을 받을 수도 있고 전체 페이지에 대한 정보를 제공해줍니다.

     

    Pageable 인터페이스는 페이징에 대한 요청값입니다. 페이징 요청에 대한 정보들을 가지고 있습니다. PageRequest.of를 통해 ( 현재 페이지, 한 페이지의 크기, 정렬 ) 페이징 요청을 할 수 있습니다.

     

     

     

     

     

     

    오늘은 이전의 기본 제공된 메서드들에 비해 복잡한 쿼리를 작성하는 방법에 대해 알아보았습니다. 네이밍 규칙들을 활용해서 가독성있는 코드를 작성할 수 있도록 해야겠습니다.

     

    반응형
    LIST

    '백엔드' 카테고리의 다른 글

    JPA 연관관계 살펴보기 (1:1, 1:N, N:1)  (4) 2023.08.21
    Entity annotation, Listener  (1) 2023.08.19
    JPA 살펴보기  (0) 2023.08.17
    Spring boot todo list 만들기  (2) 2023.07.21
    mysql cli 모음 (+ docker)  (0) 2022.12.09

    댓글

Designed by Tistory.