JPA QueryMethod 2 (where 절 조건 추가)
지난 포스팅에서는 쿼리메소드의 기본 키워드에 대해 정리했다면
이번에는 where절에 조건을 추가하는 방법에 대해 포스팅.
where 절에 조건을 추가할 수 있는 키워드들은 Jpa Document에서
Appendix C: Repository query keywords -> Supported query method predicate keywords and modifiers
에서 확인할 수 있다.
일단 테스트 환경은 다음과 같다.
- Intelli J
- SpringBoot 2.6.2
- Lombok
- Gradle 7.3.2
제일 처음에 해볼건 And와 Or다.
테스트는 아래와 같이 진행.
@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String name;
@NonNull
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByEmailAndName(String email, String name);
List<User> findByEmailOrName(String email, String name);
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println(userRepository.findByEmailAndName("coco@gmail.com", "coco"));
System.out.println(userRepository.findByEmailOrName("coco@gmail.com", "coco"));
}
현재 데이터는
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`)
values(1, 'coco', 'coco@gmail.com', now(), now());
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`)
values(2, 'mozzi', 'mozzi@gmail.com', now(), now());
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`)
values(3, 'cococo', 'cococo@gmail.com', now(), now());
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`)
values(4, 'cocococo', 'cocococo@gmail.com', now(), now());
insert into user(`id`, `name`, `email`, `created_at`, `updated_at`)
values(5, 'coco', 'coco2@gmail.com', now(), now());
이렇게 들어가있다.
그럼 테스트를 실행했을 때 And 조건에 맞는 1번 데이터만 출력하게 되고 or에서는 1,5번 데이터가 출력되게 된다.
로그에서 쿼리문을 확인해보면 where 절에서 email = ? and name = ? 과 email = ? or name = ? 이렇게 실행되고 있는것을 볼 수 있다.
어렵지 않은 부분이라 빠르게 패스.
다음은 after 와 before다.
위 데이터들을 보면 created_at과 updated_at으로 시간을 받고있는것을 볼 수 있다.
after와 before는 의미 그대로 이후 이전을 의미하고 보통 시간에 대한 조건으로 사용한다고 한다.
그래서 created_at을 통해 조회한다.
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByCreatedAtAfter(LocalDateTime yesterday);
List<User> findByCreatedAtBefore(LocalDateTime tommorow);
List<User> findByIdAfter(Long id);
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByCreatedAtAfter : " + userRepository.findByCreatedAtAfter(LocalDateTime.now().minusDays(1L)));
System.out.println("findByCreatedAtBefore : " + userRepository.findByCreatedAtBefore(LocalDateTime.now().plusDays(1L)));
System.out.println("findByIdAfter : " + userRepository.findByIdAfter(4L));
}
이렇게 테스트를 하게 되면 어제날짜의 현재 시간 이후에 저장된 데이터들이 모두 출력되고
before에서는 내일날짜의 현재시간 이전에 저장된 데이터들이 모두 출력된다.
쿼리문에서는 after의 경우 created_at > ? 형태로 실행하고 before는 반대로 created_at < ? 형태로 실행된다.
마지막에 findByIdAfter의 경우 id 값이 4보다 큰 데이터만 출력한다.
after와 before는 시간에 대한 조건으로 사용한다고 했는데 결과를 보면 다른 형태로도 사용이 가능하다.
하지만 after와 before라는 의미의 가독성을 위해 보통 시간에 대한 조건에서만 사용한다고 한다.
다음은 greaterThan과 LessThan이다.
after, before와 동일한 형태의 쿼리가 실행된다.
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByCreatedAtGreaterThan(LocalDateTime yesterday);
List<User> findByCreatedAtLessThan(LocalDateTime tommorow);
List<User> findByCreatedAtGreaterThanEqual(LocalDateTime yesterday);
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByCreatedAtGreaterThan : " + userRepository.findByCreatedAtGreaterThan(LocalDateTime.now().minusDays(1L)));
System.out.println("findByCreatedAtLessThan : " + userRepository.findByCreatedAtLessThan(LocalDateTime.now().plusDays(1L)));
System.out.println("findByCreatedAtGreaterThanEqual : " + userRepository.findByCreatedAtGreaterThanEqual(LocalDateTime.now().minusDays(1L)));
}
테스트를 실행해보면 이전 after와 before테스트때의 쿼리와 완전 동일한것을 볼 수 있다.
단, GreaterThan와 LessThan은 동일하게 > < 형태지만 GreaterThanEqual의 경우 >= 형태로 동일한 값도 포함한 데이터를 조회하는것을 볼 수 있다.
의미 그대로 보면 큰것, 작은것이기 때문에 after와 before 처럼 똑같이 사용할 수 있지만 가독성을 위해
after와 before는 시간, GreaterThan, LessThan은 그 외 필드에 사용하는것이 좋다고 한다.
가독성을 생각안하면 어떻게 쓰던 상관은 없다는것.
그리고 after와 before에서는 Equals를 포함하지 않는다.
다음은 between이다.
범위 내의 데이터 조회를 하기 위해 사용하기 때문에 변수가 두가지 필요하다.
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByCreatedAtBetween(LocalDateTime yesterday, LocalDateTime tomorrow);
List<User> findByIdBetween(Long id1, Long id2);
List<User> findByIdGreaterThanEqualAndIdLessThanEqual(Long id1, Long id2);
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByCreatedAtBetween : " + userRepository.findByCreatedAtBetween(LocalDateTime.now().minusDays(1L), LocalDateTime.now().plusDays(1L)));
System.out.println("findByIdBetween : " + userRepository.findByIdBetween(2L, 4L));
System.out.println("findByIdGreaterThanEqualAndIdLessThanEqual : " + userRepository.findByIdGreaterThanEqualAndIdLessThanEqual(2L, 4L));
}
이렇게 테스트를 진행하게 되면 날짜는 어제의 현재 시간 이후에 등록된 데이터라면 모두 출력하게 된다.
첫번째 테스트는 사실상 그냥 날짜도 가능하다 라는것을 보여주기 위함이고
두번째 테스트부터 보면 id 값이 2, 3, 4에 해당하는 데이터를 가져오게 된다.
between은 지정한 범위의 데이터를 모두 가져오기 때문에 지정 범위까지 모두 포함해 가져오게 된다.
로그에서 쿼리문을 보게 되면 id between ? and ? 형태가 된다.
마지막 3번째 테스트는 between을 사용하지 않고 풀어서 처리하는 경우다.
GreaterThanEqual은 >= ? 를 의미하고 LessThanEqual은 <= ? 를 의미하기 때문에 두개의 조건을 만족한다는것은
해당 범위를 포함한 그 안에 위치하는 데이터들을 의미하는것과 마찬가지다.
그래서 between과 동일한 결과를 출력하게 된다.
이렇게 사용하는경우는 Equal이 꼭 붙어있어야 이상, 이하로 조회되기 때문에 붙여줘야 하고 이부분은 잠재적인 오류 포인트로 자주 나타나는 부분이라고 한다.
개발툴이 많이 발달했어도 아직 쿼리상의 논리적인 오류는 잘 찾지 못하기 때문에 조심해야 한다고 한다.
어차피 같은 결과를 출력해야 한다면 between이 당연히 더 편하다!!!!!
이렇게도 사용이 가능하구나 정도만 알아두면 될듯!
다음은 Null값에 대한 조회다.
제일먼저 볼것은 isNotNull
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByIdIsNotNull();
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByIdIsNotNull : " + userRepository.findByIdIsNotNull());
}
isNotNull은 의미 그대로 null이 아닌것을 조회한다.
primary key인 Id를 기준으로 조회했기 때문에 당연히 모든 데이터가 출력된다.
쿼리문만 잘 보면 되는데 로그에서 보면
where
user0_.id is not null
이렇게 조회하는것을 볼 수 있다.
다음은 isNotEmpty다.
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByIdIsNotEmpty();
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByIdIsNotEmpty : " + userRepository.findByIdIsNotEmpty());
}
이렇게 그냥 테스트를 실행하면 오류가 발생한다.
오류 내용을 보면
IsEmpty / IsNotEmpty can only be used on collection properties!
IsEmpty와 isNotEmpty는 Collection 속성에서만 사용할 수 있다는 것이다.
Collection은 객체의 모음, 그룹이라 볼 수 있다.
아직 collection 포스팅은 안해서 좀 더 자세한건 제일 아래에 Reference에서 확인.
Collection을 이해해야 좀 더 보기 편하긴 하다..
Relation이 있어야 가능하다고 보면 될듯.
근데 지금 테스트에서는 문자열이 아닌 long타입이기 때문에 괜찮지만 만약 문자열을 사용한 경우에는 오해의 소지가 좀 생길 수 있다.
일반적인 not empty는 notNull이면서 NotEmpty인 경우이기 때문이다. 문자열에서는 ""가 empty를 의미하기때문에 NotEmpty를 사용했을 때 실수할 수도 있다.
중요한건 IsNotEmpty 에서의 NotEmpty는 문자열에서의 NotEmpty가 아닌 Collection에서의 NotEmpty를 의미한다.
즉, 비어있지 않으면 가져와! 라는것이 아니라 collection이 비어있지 않으면 가져오라는것이다.
그래서 쿼리를 좀 보기 위해 테스트를 하나 더 진행.
그리고 User Etity 필드 하나와 Entity하나를 추가해준다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String name;
@NonNull
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@OneToMany(fetch = FatchType.EAGER)
private List<Address> address;
}
@Entity
public class Address {
@Id
private Long id;
}
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByAddressIsNotEmpty();
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByAddressIsNotEmpty : " + userRepository.findByAddressIsNotEmpty());
}
Address는 데이터를 따로 넣지 않는다.
테스트를 실행하면 아무것도 출력되지 않는다.
Address에 데이터가 없으니까.
쿼리문만 보면 되니까 로그에서 쿼리문을 확인한다.
select
user0_.id as id1_2_,
user0_.created_at as created_2_2_,
user0_.updated_at as updated_3_2_,
user0_.email as email4_2_,
user0_.name as name6_2_
from
user user0_
where
exists (
select
address2_.id
from
user_address address1_,
address address2_
where
user0_.id=address1_.user_id
and address1_.address_id=address2_.id
)
isNotEmpty라는 네이밍을 보면 Empty. 즉, 비어있지 않은 값을 조회한다고 생각하기 딱 좋다.
그럼 name을 기준으로 조회한다면 name is Not Null and name != ' ' 이런 형태를 생각하기 쉬운데
쿼리문을 보면 전혀 그런것이 아니라는것을 볼 수 있다.
where 절에서 exists를 통해 조회하는데 User안에 있는 address와 Address의 id를 비교해 동일한 값만 가져오도록 한다.
그럼 쿼리를 봤을때 결과가 나오는것은 isNotNull이랑 전혀 다른 결과가 나온다는것을 예측할 수 있다.
다음은 In와 NotIn.
public interface UserRepository extends JpaRepository<User, Long>{
List<User> findByNameIn(List<String> names);
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByNameIn : " + userRepository.findByNameIn(Lists.newArrayList("coco", "mozzi")));
}
In의 경우는 iterator타입인 List가 들어간다. 그리고 조회하는 타입이 제네릭으로 들어가야 하기 때문에
List<String> names 로 인자를 받는다.
테스트에서는 그냥 Lists로 테스트했지만 이렇게 하기 보다는 복수의 결과값을 리턴하는 다른 쿼리의 결과값을 이용해 조회한다.
In을 사용할때 조심해야하는 점은 Lists로 넣지 않고 쿼리 결과를 넘기도록 하게 되면 얼마나 많은 데이터가 in절로 넘어가는지 알수가 없다. in절안에 List의 길이가 너무 길어지게 되면 성능이슈가 발생할 수 있기 때문에 데이터가 어느정도 들어갈것인지를 사전에 검토하고 사용하는것이 좋다.
NotIn은 동일하게 사용하기 때문에 패스~!
다음은 containing, ending_with, starting_with.
이 세가지는 한번에 테스트.
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
private UserRepository userRepository;
@Test
void select(){
System.out.println("findByNameStartingWith : " + userRepository.findByNameStartingWith("mo"));
System.out.println("findByNameEndingWith : " + userRepository.findByNameEndingWith("zi"));
System.out.println("findByNameContains : " + userRepository.findByNameContains("zz"));
System.out.println("findByNameLike : " + userRepository.findByNameLike("%oz%"));
}
Like를 추가해서 테스트를 진행했는데
일단 위 세가지만 좀 보자면 로그에서 쿼리를 봤을때는 셋다 동일하게
name like ? escape ? 로 출력되는것을 볼 수 있다.
그럼 차이를 보자면 출력되는 데이터의 name은 mozzi다.
그럼 메소드 네이밍이랑 같이 예측해보면 StartingWith는 mo%, EndingWith는 %zi, contating은 %zz% 형태라는것을
알 수 있다.
Like를 추가해서 작성한 이유는 차이를 좀 보기 위해서다.
나머지 세개의 메소드는 원하는 검색 키워드만 딱 입력해주면 되지만 Like는 그렇지 않다는것을 볼 수 있다.
이렇게 직접 입력하는 경우는 크게 어렵지 않을 수 있지만 만약 값을 받아서 검색을 한다고 하면
다른 메소드들은 (keyword) 형태로 작성해주면 되지만 Like의 경우는
("%" + keyword + "%") 이런 형태로 사용해야 하기때문에 아무래도 가독성이 떨어진다.
Reference
- 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA
- Collection
[JAVA] Java 컬렉션(Collection) 정리
[JAVA] Java 컬렉션(Collection) 정리 ■ Java Collections Framework(JCF) Java에서 컬렉션(Collection)이란 데이터의 집합, 그룹을 의미하며 JCF(Java Collections Framework)는 이러한 데이터, 자료구조인 컬..
gangnam-americano.tistory.com