스프링데이터 + JPA/스프링 데이터 JPA

9. 메소드 이름으로 쿼리 생성

sdafdq 2023. 11. 20. 08:25

Spring Data JPA 에서 제공하는 공통 기능 말고도, 예를 들어 username으로 조회를 해 온다던지, 이런 도메인에 특화된 조회 등 공통되지 않은 부분들을 만들어야 할 필요성이 있다.

 

Spring Data JPA는 이러 한 공통적인 부분이 아닌 쿼리들을 어떻게 만들도록 지원해 주는지 알아볼 것이다.

 

Spring Data JPA에서 쿼리를 만들도록 지원해 주는 것은 총 3가지 방법이 있는데,

1. 메소드 이름으로 쿼리 생성

2. JPA NamedQuery 호출

3. @Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의

 

먼저 메소드 이름으로 쿼리 생성하는 것에대해 알아볼 거임.

 

그 전에, Jpa로만 먼저 짜보면

username이 같고 age가 특정 값 보다 큰거

public List<Member> findByUsernameAndAgeGraterThen(String username, int age){
    return em.createQuery("select m from Member m where m.username = :username and m.age > :age", Member.class)
            .setParameter("username", username)
            .setParameter("age", age)
            .getResultList();
}

이렇게 된다.

 

@Test
public void findByUsernameAndAgeGraterThen(){
    Member member1 = new Member("AAA",10);
    Member member2 = new Member("AAA",20);

    memberJpaRepository.save(member1);
    memberJpaRepository.save(member2);

    List<Member> findMembers = memberJpaRepository.findByUsernameAndAgeGraterThen("AAA", 15);

    findMembers.forEach((Member member)->{
        assertThat(member.getUsername()).isEqualTo("AAA");
        assertThat(member.getAge()).isGreaterThan(15);
    });
    assertThat(findMembers.size()).isGreaterThanOrEqualTo(1);
}

테스트 잘 된다.

 

나가는 쿼리

    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.username=? 
        and m1_0.age>?
2023-11-20T07:58:14.924+09:00  INFO 17176 --- [           main] p6spy                                    : #1700434694924 | took 0ms | statement | connection 4| url jdbc:h2:tcp://localhost/./datajpa
select m1_0.member_id,m1_0.age,m1_0.team_id,m1_0.username from member m1_0 where m1_0.username=? and m1_0.age>?
select m1_0.member_id,m1_0.age,m1_0.team_id,m1_0.username from member m1_0 where m1_0.username='AAA' and m1_0.age>15;
2023-11-20T07:58:15.006+09:00  INFO 17176 --- [           main] p6spy                                    : #1700434695006 | took 0ms | commit | connection 4| url jdbc:h2:tcp://localhost/./datajpa

;

 

 

이제 이거를 메소드 이름으로 쿼리를 생성하는 것을 해 볼것이다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

이렇게 했다.

따로 구현한 게 없다.

 

그냥 이름만 약속에 맞게 잘 적어줬다. 그러면,

@Test
public void findByUsernameAndAgeGraterThen(){
    Member member1 = new Member("AAA",10);
    Member member2 = new Member("AAA",20);

    memberRepository.save(member1);
    memberRepository.save(member2);

    List<Member> findMembers = memberRepository.findByUsernameAndAgeGreaterThan("AAA", 15);

    findMembers.forEach((Member member)->{
        assertThat(member.getUsername()).isEqualTo("AAA");
        assertThat(member.getAge()).isGreaterThan(15);
    });
    assertThat(findMembers.size()).isGreaterThanOrEqualTo(1);
}

 

리포지토리를 바꿔서 실험을 해봐도, 똑같은 결과가 나온다.

    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.username=? 
        and m1_0.age>?

나가는 쿼리도 같다.

 

진짜 메소드 명으로 자동으로 만들어 진 것이다.

 

 

만약

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> hi(String username, int age);
}

그냥 이렇게 대충하면

 

Caused by: org.springframework.data.mapping.PropertyReferenceException: No property 'hi' found for type 'Member'; Did you mean 'id'

뭐 Member에서 hi를 찾을 수 없다고 뜬다. 필드로 읽어보려고 시도해 본 모양이다.

 

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

천천히 읽어보면,

findBy 찾다

Username 유저네임으로

And 그리고

AgeGreaterThen 나이가 더 큰걸로

 

그럼 그 규칙이 뭐냐?

https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html

 

JPA Query Methods :: Spring Data JPA

By default, Spring Data JPA uses position-based parameter binding, as described in all the preceding examples. This makes query methods a little error-prone when refactoring regarding the parameter position. To solve this issue, you can use @Param annotati

docs.spring.io

여기에 나온다.

 

예시와 만들어지는 이름까지 잘 설명해 놨다.

 

아무래도 다 조회에 대한 것 밖에 없다.

하긴 CUD는 아예 JPQL 쿼리를 직접 안 써도 제공해 주니, 조회만 있어도 될 거 같다.

 

여기 중 하나에

In findByAgeIn(Collection<Age> ages) … where x.age in ?1

이런 것도 있다.

in 해서 in에 들어갈 것을 컬렉션으로 그냥 넘기면 된다.

 

KeywordSampleJPQL snippet

Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

 

name이나 age는 그냥 예시일 뿐이고, 당연히 다른 것으로 해도 된다.

 

위의 것들은 거의 조건? 에 관한 것 들이고,

 

countBy : long타입으로 몇개 있는지 반환

existsBy : 존재하는지 bool 타입으로 반환

deleteBy 또는 removeBy : 삭제. 반환타입 long. 몇개의 row가 영향을 받았는지 그거일 듯.

findMemberDistictBy : 이렇게 distinct도 가능. 

 

또 이름으로 페이징도 가능

top이나 first로

findTop3ByAge() : 제일 위에서 3개

findFirst3ByAge() : 제일 첫번째부터 3개

 

List<Member> findTop3ByAge(int age);
    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.age=? 
    fetch
        first ? rows only

근데 Top이나 First나 나가는 쿼리는 같음. 그냥 findBy를 readBy라고도 할 수 있고 getBy, queryBy라고도 할 수 있는 그런 비슷한 거 인듯.

 

orderBy도 할 수 있음

findFirst3ByOrderByAgeAsc()

age를 Asc순으로 한거 처음 3개만.

 

참고로, findFirstBy 그냥 이렇게 할 수 있는데

이러면 하나만 가져오는 거임. 가정 First 하나 아니면 가장 Top 하나 등.

 

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

예시들

 

 

그리고

findBy할 때 저 find와 By 사이에 자유롭게 넣을 수 있음.

나는 뭐 find 다음 가져올 필드 이런 거 넣는 거 인 줄 알았는데 아님.

그냥 식별? 할수 있게끔 정말 자유롭게 쓸 수 있는거임.

예를들어 findRankerBy~~~

그냥 저렇게 개발자가 식별할 수 있게끔.

findHelloBy 이런 것도 됨.

페이징(Top, First) 제외하고, 만들어지는 쿼리에 영향이 없음. 그냥 순전히 개발자가 보고 식별? 메모하는 용도임.

 

 

 

참고로,

List<Member> findBy();

그냥 이렇게도 가능.

이렇게 하면 그냥 전체 조회임.

    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0

그냥 select * from 근원

 

find...By()
// ...에 페이징 가능

findFirst3By()
// 처음 3개 가져옴.

findFirst3ByOrderByAgeAsc()
// 처음 3개. 그런데 order By Age asc 된거에서


findByAge(int age)
//where age = :age, 즉, age에 의해 찾기.

findByNameAndAge(String name, int age)
// select m from Member m where name = :name and age = :age
//findBy 뒤에 필드 붙으면 조건.
//orderBy등 명령 붙으면 그 명령.

 

 

findFirst3ByOrderByAgeAsc()
    select
        m1_0.member_id,
        m1_0.age,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    order by
        m1_0.age 
    fetch
        first ? rows only

order by age asc

asc가 기본이니까 굳이 그렇게 안나간거고,

first 3 이니까 limit(first)으로 3개

 

얘는 기본적으로 Dto나 특정값들이 아니라,

엔티티 자체를. 엔티티 전체 자체를 가져오는 게 기본인 듯.

 

 

되게 짧은 간단한 쿼리들 만들 때 좋은 듯.

 

 

 

참고로, 중요한데

엔티티의 필드명이 변경되면 메소드명도 변경되어야 함.

메소드명을 보고 쿼리를 만들 때 엔티티의 필드명을 참조하는 거니..

JPQL이니 엔티티 참조니까.