Spring Data JPA
JPA를 사용해도 마찬가지로 DB의 기본적인 CRUD는 어찌됐든 사용하기 마련이다.
근데 이것을 가지고 똑같이 계속 사용해서 엔티티마다 만들어 주는 개념이 아니라
인터페이스에 제네릭 타입을 사용해서 엔티티를 매칭해주어 구현을 하는것이 편리하다.
이 repository를 개발할 때 인터페이스만 정의주면 실행 시점에 Spring Data JPA
가 구현 객체를 동적으로 생성해서 주입해준다.
그래서 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발이 가능 하다.
public class MemeberRepository extends JpaRepository<Member, Long> {
List<Member> findAll();
}
이런식으로 구현을 해준다.
MemberRepository
에는 @Repository
를 넣어주지 않는다.
왜냐면
JpaRepository
를 상속받은 JpaRepositoryImplementation
를 구현한
SimpleJpaRepository
가
{: text-center}
그림과 같이 @Repository
를 가지고 있다.
그래서 인터페이스만 잘 작성해주면 된다.
제네릭에는 JpaRepository<엔티티, 식별자타입>
이렇게 만들어준다.
JpaRepository의 주요 메서드
save()
: 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
delete()
: 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()
를 호출한다.
findOne()
: 엔티티 하나를 조회한다. 내부에서 EntityManager.find()
를 호출한다.
getOne()
: 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()
를 호출한다.
findAll()
: 모든 엔티티를 조회한다. 정렬이나 페이징 조건을 파라미터로 전달할 수 있다.
findBy()
: 엔티티 하나를 조회한다. Optional
타입의 객체를 반환한다.
반환 타입
Spring Data JPA는 만약 조회 결과가 없는 경우 컬렉션을 반환하는 메소드에서는 null을 반환한다.
단건으로 두 건 이상 겹쳐서 조회가 될 때 다음의 예제를 보자.
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByName(String name);
}
@DataJpaTest
class MemberTest {
@Test
void 두건_조회_테스트() {
Address address = Address.builder()
.city("city")
.street("street")
.zipcode("zipcode")
.build();
Address comAddress = Address.builder()
.city("cocity")
.street("costreet")
.zipcode("cozipcode")
.build();
Period period = Period.of("20210714", "20210714");
Member member = Member.builder()
.name("name")
.age(26)
.period(period)
.homeAddress(address)
.companyAddress(comAddress)
.build();
Member member2 = Member.builder()
.name("name")
.age(26)
.period(period)
.homeAddress(address)
.companyAddress(comAddress)
.build();
memberRepository.save(member);
memberRepository.save(member2);
memberRepository.findByName("name");
}
}
같은 삽입 코드를 두개 복사했다. 감안해서 보자.
일단 Member
엔티티인 member
, member2
를 저장하고 Name 컬럼의 값으로 하나를 가져오는 것인데, 복사해서 붙여넣었으니 같은 값이 즉 2개 이다.
하나의 데이터만 가져오지 못한다면,
{: text-center}
NonUniqueResultException
을 발생시킨다.
정확하게 1개 반환하는 것이 아니라면 List로 받는게 오히려 좋을 수도 있겠다.
findById()와 findAll()의 차이를 알고 싶다면
쿼리 메서드
Spring Data JPA가 제공하는 기능이다. 메소드 이름만으로 쿼리를 생성하는 기능이 있는데 인터페이스에 메소드만 선언해주면 해당 메소드 이름으로의 적절한 JPQL 쿼리를 생성해서 날려준다.
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
@Query
사용하여 인터페이스에 직접 정의
쿼리 메소드의 규칙에 따라서 정의를 해주어야 동작한다.
쿼리 메소드 규칙
키워드 |
예 |
JQPL 예 |
And |
findByNameAndAddress |
where x.Name = ?1 and x.Address = ?2 |
Or |
findByNameOrAddress |
where x.Name = ?1 or x.Address = ?2 |
Distinct |
findDistinctByLastnameAndFirstname |
select distinct … where x.lastname = ?1 and 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 ( %가 뒤에 추가된 매개 변수) |
EndingWith |
findByFirstnameEndingWith |
… where x.firstname like ?1 ( %가 앞에 추가된 매개 변수) |
Containing |
findByFirstnameContaining |
… where x.firstname like ?1 ( %가 래핑된 매개 변수) |
OrderBy |
findByAgeOrderByLastnameDesc |
… where x.age = ?1 order by x.lastname desc |
Not |
findByLastnameNot |
… where x.lastname <> ?1 |
In |
findByAgeIn(Collection ages) |
… where x.age in ?1 |
NotIn |
findByAgeNotIn(Collection 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) |
NamedQuery
보통 @NamedQuery
로 정의하는 방식인데, 이거는 MyBatis를 쓰는 방식같은 느낌이 많이 든다.
이것은 전의 포스팅을 보면 이해가 쉬울 수 있다.
@Query, Repository 메소드에 쿼리 정의
이 방법이 더 간단하고 편리하게 정의할 수 있는 느낌이 더 강하다.
이름없는 NamedQuery 라고 생각하면 좋을듯? 싶다 😁
장점이라고 하면 애플리케이션 실행 시점에 문법 오류를 발견할 수 있어서 좋다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Memer m where m.name = ?1")
Member findByName(String name);
}
사용은 이런식으로 하면 되겠다.
Native SQL을 사용하려면 @Query("sql", nativeQuery=true)
로 설정하면 되고 이 네이티브 쿼리에서는 파라미터 바인딩 숫자가 0부터 시작한다.
0부터 시작하면 뭐하나 인덱스 밀려서 쓰면 값 에러 날 수 있으니 권장하지 않는다.
위치 기준 파리미터 주의점
위치(순서) 기준보다는 이름 기준의 바인딩을 추천한다. 위치 기준의 바인딩을 사용하면 추후에 요소가 추가되면 밀리거나 땡겨질 수가 있기 때문❗️
그래서 아래와 같은 방법을 더 많이 쓴다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Memer m where m.name = :name")
Member findByName(@Param("name") String name);
}
정렬
Spring Data JPA에는 쿼리 메소드에 페이징 그리고 정렬기능을 사용하도록 2가지 파라미터를 제공한다.
org.springframework.data.domain.Sort
: 정렬 기능
org.springframework.data.domain.Pageable
: 페이징 기능 (내부에 Sort) 포함
여기서 정렬 실습에 대한 에러도 같이 남긴다.
우선 sql DDL 그리고 DML 이다.
member.sql
CREATE TABLE IF NOT EXISTS MEMBER
( ID BIGINT GENERATED BY DEFAULT AS IDENTITY,
NAME VARCHAR(255),
AGE INTEGER NOT NULL,
COMPANY_CITY VARCHAR(255),
COMPANY_STREET VARCHAR(255),
COMPANY_ZIPCODE VARCHAR(255),
CITY VARCHAR(255),
STREET VARCHAR(255),
ZIPCODE VARCHAR(255),
END_DATE VARCHAR(255),
START_DATE VARCHAR(255),
PRIMARY KEY (ID)
);
INSERT INTO MEMBER (name, age, company_city, company_street, company_zipcode, city, street, zipcode, end_date, start_date) VALUES ('테스트', 33, 'city1', 'street', 'zipcode', 'cocity1', 'costreet', 'cozipcode', '20210714', '20210714');
같은 쿼리 여러개 넣었다고 생각하자.
...
...
...
처음에 이렇게 쿼리메소드를 구현하였다.🙂
public interface MemberRepository extends JpaRepository {
//기존 로직
...
Page<Member> findByName(String name, Pageable pageable);
}
그리고 테스트코드에서는...
Page<Member> members = memberRepository.findByName("테스트", PageRequest.of(0, 3, Sort.by(Order.desc("age"))));
이렇게 Page<T>
를 사용하면 페이징 처리를 할 수있다.
첫 페이지는 0부터 시작한다.
이슈 발생
여기서 생긴 이슈는 분명 Sort.by(Order.desc("정렬할 컬럼"));
으로 정렬을 넣어줬음에도 쿼리에서는 정렬을 해주지 않았고 근데 정렬방식은 DESC로 나오는 경우였다.
{: text-center}
{: text-center}
이렇게 페이징을 위해 count
쿼리는 나온다. 근데 where
이후로 order by
가 없는 것이다.
스터디를 하는 분과 같이 의논을 Code With Me
로 같이 삽질을 좀 했다.
p.s 시간 내주셔서 감사합니다!
결과 알아낸 것은 내가 쿼리 메소드로 구현한건 findBy
로 시작하는 쿼리메소드를 오버로딩해서 Pageable
만 추가해줬다.
이렇게 하니까 결국 기본으로 Spring Data JPA에서 정의된 쿼리메소드 참조를 하기 때문에 우선순위가 그쪽으로 배정되어서 조회는 ASC로 되는가보다.
해결책
public interface MemberRepository extend JpaRepository {
Page<Member> findMembersByName (String name, Pageable pageable);
}
보다시피 메서드 이름을 findMembersByName
으로 바꿨고,
@DataJpaTest
@ActiveProfiles("test")
public class DbTest {
@Autowired
MemberRepository memberRepository;
@Test
@Sql("classpath:member.sql")
void dbTest() throws Exception {
Page<Member> members = memberRepository.findMembersByName("테스트", PageRequest.of(0, 3, Sort.by(Order.desc("age"))));
Pageable pageable = members.getPageable();
System.out.println("pageable = " + pageable);
System.out.println("pageable.getSort() = " + pageable.getSort());
}
}
이렇게 해주니까
{: text-center}
잘 나오게 된다.
2021-08-22 수정본
위 부분은 사실 단순한 해결이었고, 진정한 해결책은 우선순위는 @NamedQuery
에 있다.
실습때문에 Member
엔티티에 NamedQuery
를 추가했었어서 이 부분이 먼저 구현되어 비정상적으로 정렬이 됐던 것이다. 스터디원들 다 같이 모여 해결했다. 이럴때 집단지성이 굉장히 좋은것 같다👍
아무튼 구문자체에는 오류가 없었고 네임드 쿼리 쓰지말자! Mybatis를 보면 볼 수록 생각나게 한다. 이제 정을 떼는것이 좋을것 같다.
좀 더 편리하게 사용하려면 interface
는 다중상속
이 가능하기 때문에
이렇게 상속해서 사용해도 될 것 같다.
이상으로 12장 포스팅을 마치도록 하겠다.