JPA -> 스프링 데이터 JPA 방식으로 진행!
세팅을 원래는 해줬어야 하나, Spring 부트가 알아서 해줌!
<MemberJpaRepository.java>
public class MemberJpaRepository {
private final EntityManager em;
public Member save(Member member){
em.persist(member);
return member;
}
public Member find(Long id){
return em.find(Member.class, id);
}
public void delete(Member member){
em.remove(member);
}
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class).getResultList();
}
public Optional<Member> findById(Long id){
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public long count(){
return em.createQuery("select count(m) from Member m", Long.class).getSingleResult();
}
}
<MemberJpaRepositoryTest.java>
@SpringBootTest
@Transactional
@Rollback(false)
class MemberJpaRepositoryTest {
@Autowired MemberJpaRepository memberJpaRepository;
@Test
public void testMember() throws Exception {
// given
Member member = new Member("memberA", 10);
Member saveMember = memberJpaRepository.save(member);
// when
Member findMember = memberJpaRepository.find(saveMember.getId());
// then
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
assertThat(findMember).isEqualTo(member);
}
@Test
public void basicCRUD(){
Member memberA = new Member("memberA", 10);
Member memberB = new Member("memberB", 12);
memberJpaRepository.save(memberA);
memberJpaRepository.save(memberB);
Member findMember1 = memberJpaRepository.findById(memberA.getId()).get();
Member findMember2 = memberJpaRepository.findById(memberB.getId()).get();
assertThat(findMember1).isEqualTo(memberA);
assertThat(findMember2).isEqualTo(memberB);
List<Member> all = memberJpaRepository.findAll();
assertThat(all.size()).isEqualTo(2);
long count = memberJpaRepository.count();
assertThat(count).isEqualTo(2);
memberJpaRepository.delete(memberA);
memberJpaRepository.delete(memberB);
long afterCount = memberJpaRepository.count();
assertThat(afterCount).isEqualTo(0);
}
}
<MemberRepository.java>
public interface MemberRepository extends JpaRepository<Member, Long> {
}
Test 쪽 코드에서, memberRepository
를
MemberJpaRepository => MemberRepository 로 바꿔주면 똑같이 작동함!
interface 만 있고, 구현한 적이 없는데 대체 어떻게?
Spring Data JPA가 로딩 시점에 알아서 프록시 객체로 구현을 해줌!
<TeamJpaRepository.java>
@Repository
@RequiredArgsConstructor
public class TeamJpaRepository {
private final EntityManager em;
public Team save(Team team){
em.persist(team);
return team;
}
public Team find(Long id){
return em.find(Team.class, id);
}
public void delete(Team team){
em.remove(team);
}
public List<Team> findAll(){
return em.createQuery("select t from Team t", Team.class).getResultList();
}
public Optional<Team> findById(Long id){
Team team = em.find(Team.class, id);
return Optional.ofNullable(team);
}
public long count(){
return em.createQuery("select count(t) from Team t", Long.class).getSingleResult();
}
}
<TeamRepository.java>
public interface TeamRepository extends JpaRepository<Team, Long> {
}
Team 의 경우도 바꿔줄 수 있다!
JpaRepository<___, ___>
Type 이 중요하다! 그것에 따라 내부 동작 메소드들이 결정되니깐!
기본적으로 사용할 것 같은 메소드들은 다 구현되어 있으니, CRUD 코드 노가다를 엄청 줄일 수 있음!! 최고!!
save, delete, findById, getOne, findAll, paging, sort 등의 메소드를 제공해준다!!
getOne의 경우 getReference 프록시 객체로 받음!!
개발자로서 상상할 수 있는 공통기능은 전부다 제공해줌!!!
공통기능 외에 다른 기능이 필요하다면!!
ex. field 값에 따른 find
List<Member> findByUsername(String username)
이걸 해결해주기 위해!! query method 를 제공해줌!
순수하게 했다고 치면
public List<Member> findByUsernameAndAgeGreaterThen(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();
}
뭐 그닥 어렵지 않기는 한데, 이런거 좀 알아서 해주면 안되나?
정말 웃기게도, Spring Data JPA 가 알아서 메소드를 연결해준다.
public interface MemberRepository extends JpaRepository<Member, Long> {
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
(IntelliJ 유료 버전에서는 이렇게 추천도 해줌!)
메소드 이름으로 쿼리를 생성!!!
https://docs.spring.io/spring-data/jpa/docs/2.7.11/reference/html/#jpa.query-methods.query-creation
엔티티 필드명이 변경된다거나, 많은 양의 쿼리 조건이 들어간다면!!
아무래도 메소드 명도 길어지고, 뭔가 유지보수가 애매해진다!!
그래서 다른 방안들이 여러 개 존재한다!!
김영한 개발자님은 한두개 정도 필드의 쿼리는 메소드 쿼리로 생성한다고 하심!
나머지는 다른 방법으로!!
덧. 오류의 순위
- 가장 좋은 거 : 컴파일 단에서
- 그 다음 : Tdd 돌려서 알 수 있는
- 그 다음 : 실행을 하면서 세팅단에서 알 수 있는
- 최악 : 고객이 버튼을 클릭해야 나오는
...
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username"
)
public class Member {
...}
public List<Member> findByUsername(String username) {
return em.createNamedQuery("Member.findByUsername", Member.class).setParameter("username", "aaa").getResultList();
}
createNamedQuery 를 활용해서 사용 !!
이 방식을 Spring Data JPA 는 어떻게 도와준다는 걸까
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
}
@Query
를 활용하여 매칭해주고, @Param
으로 매개변수 지정!!웃긴건, @Query
어노테이션을 지워도 동작을 하게 해줌! (Spring boot)
이런 관례가 있다고함!
메소드 쿼리를 생성할때,
JpaRepository 에서 Type으로 받은 Member
로 Member.____
이름의 NamedQuery 가 있는지 먼저 탐색!
없으면 메소드 쿼리로 생성!
좋아 보이지만... 사실 실무에서 많이 사용되지는 않음!!
뭔가 엔티티에 쿼리가 들어가 있는게 좀 어색...? 위의 장점을 가진 다른 기능!!!
public interface MemberRepository extends JpaRepository<Member, Long> {
...
@Query("select m from Member m where m.username = :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") int age);
@Query("select m.username from Member m")
List<String> findUsernameList();
}
기본 CRUD 말고 필요한 쿼리들은 @Query
를 바로 적용하여 메소드 생성!
쿼리문도 똑같이 들어가는 것을 알 수 있다!!
위의 NamedQuery
처럼 어플리케이션 로딩 시점에 에러를 체크함!!
미리 에러를 발견하고 넘길 수 있다!
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
test code
@Test
public void findMemberDto() throws Exception {
// given
Member m1 = new Member("aaa", 10);
Member m2 = new Member("bbb", 20);
memberRepository.save(m1);
memberRepository.save(m2);
Team team = new Team("teamA");
teamRepository.save(team);
m1.changeTeam(team);
em.flush();
em.clear();
// when
List<MemberDto> memberDtoList = memberRepository.findMemberDto();
// then
assertThat(memberDtoList.get(0).getTeamName()).isEqualTo("teamA");
assertThat(memberDtoList.get(0).getUsername()).isEqualTo("aaa");
assertThat(memberDtoList.size()).isEqualTo(1);
}
동적 Query 의 경우는? QueryDSL 로 해야한다!!
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
컬렉션
,단건
,단건 Optional
public interface MemberRepository extends JpaRepository<Member, Long> { ... List<Member> findListByUsername(String username); Member findMemberByUsername(String username); Optional<Member> findOptionalByUsername(String username); }
find...ByUsername
형태의 모든 naming이 가능함!
위 형태는 반환값만 다르게 지정한 것임!
JPA 의 경우 SingleResult 메소드를 사용하면, 값이 없거나 2개 이상일때는 Exception 을 띄움
Spring Data JPA 의 경우 findMemberByUsername 의 경우
@Test
public void returnType() throws Exception {
// given
Member m1 = new Member("aaa", 10);
Member m2 = new Member("aaa", 20);
memberRepository.save(m1);
memberRepository.save(m2);
em.flush();
em.clear();
// when
List<Member> memberList = memberRepository.findListByUsername("aaa");
Member oneMember = memberRepository.findMemberByUsername("bbb");
Optional<Member> optionalMember = memberRepository.findOptionalByUsername("bbb");
// then
assertThat(memberList.size()).isEqualTo(2);
assertThat(oneMember).isEqualTo(null);
assertThat(optionalMember.isPresent()).isEqualTo(false);
}
다양한 반환 타입을 지원한다!
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m where m.age = :age order by m.username desc", Member.class)
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
.setParameter("age", age)
.getSingleResult();
}
위와 같이
public interface MemberRepository extends JpaRepository<Member, Long> {
...
Page<Member> findByAge(int age, Pageable pageable);
}
반환타입을 Page
로 지정해주고!
input 값으로 Pageable
을 받는다!!
주의 사항 : 기존에 offset, limit 를 넣었던 과는 다르게
pageNum, pageSize 를 넣는 것임!!
find, sorting, paging 쿼리를 날리고, 추가적으로 count 쿼리도 알아서 날림!
Page에 맞게 쿼리를 날려줌!!
test code
@Test
public void pagingTest() throws Exception {
// given
Member m1 = new Member("aaa", 10);
Member m2 = new Member("bbb", 20);
Member m3 = new Member("ccc", 10);
Member m4 = new Member("ddd", 10);
Member m5 = new Member("eee", 10);
Member m6 = new Member("fff", 10);
Member m7 = new Member("ggg", 10);
Member m8 = new Member("hhh", 10);
memberRepository.save(m1);
...
memberRepository.save(m8);
int age = 10;
int pageOffset = 0;
int onePageContent = 3;
PageRequest pageRequest = PageRequest.of(
pageOffset,
onePageContent,
Sort.by(Sort.Direction.DESC, "username"));
// when
Page<Member> memberPage = memberRepository.findByAge(age, pageRequest);
for (Member member : memberPage.getContent()) {
System.out.println("member = " + member);
}
// then
assertThat(memberPage.getContent().get(0).getUsername()).isEqualTo("hhh");
assertThat(memberPage.getContent().get(1).getUsername()).isEqualTo("ggg");
assertThat(memberPage.getContent().get(2).getUsername()).isEqualTo("fff");
assertThat(memberPage.getTotalElements()).isEqualTo(7);
assertThat(memberPage.getNumber()).isEqualTo(pageOffset);
assertThat(memberPage.getTotalPages()).isEqualTo(3);
assertThat(memberPage.isFirst()).isTrue();
assertThat(memberPage.hasNext()).isTrue();
}
Page 가 가지고 있는 기능들 중에, totalPage 나 totalElements 이런 것들은 좀 필요 없는데 좀 더 가벼운 같은 기능은 없나? (count 쿼리를 매번 호출하는게 약간 불만일때)
Page
가 상속받고 있는Slice
를 활용하면 됨!
Slice 의 경우 limit 보다 하나 더 들고와서,
hasNext
에 대한 대응을 한다!!
- Page 의 경우 3개를 불러오고, Slice 는 4개를 불러온다
기존의 여러 메소드들을 전혀 사용할 수 없음!!
total count 의 경우 복잡한 연관관계에 맞춰 성능이 저하될 수 있음.(join 등)
사실 테이블의 한 필드값만 접근해서 개수만 세어도 되는 건데!!
해당 쿼리를 따로 간단하게 제한하는 방법이 존재!!
Team
값을 함께 가져오는 join query 를 만들었다고 하자.
잘 보면, count를 하는데도 left join 이 들어간다!
counter query 가 table join 을 할 필요는 없음!
그래서, 이런식으로 추가해서 해줄 수 있다!
countQuery 항목에 새로 넣어서, count에 복잡한 쿼리가 나가지 않도록 할 수 있음!!
외부 API 로 반환할때는 이런식으로 mapping 해서 DTO 로 보낼 수 있도록!
public int bulkAgePlus(int age) {
return em.createQuery("update Member m" +
" set m.age = m.age + 1" +
" where m.age >= :age")
.setParameter("age", age).executeUpdate();
}
이런 쿼리를 날림!
@Modifying // 이게 있어야 executeUpdate() 를 실행함! 필수임!
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Modifying
필수!!테스트 코드
@Test
public void bulkUpdate() throws Exception {
// given
Member m1 = new Member("aaa", 10);
Member m2 = new Member("bbb", 20);
Member m3 = new Member("ccc", 10);
Member m4 = new Member("ddd", 10);
Member m5 = new Member("eee", 20);
Member m6 = new Member("fff", 10);
Member m7 = new Member("ggg", 10);
Member m8 = new Member("hhh", 20);
memberRepository.save(m1);
...
memberRepository.save(m8);
// when
int resultCount = memberRepository.bulkAgePlus(15);
System.out.println("resultCount : " + resultCount);
// then
assertThat(resultCount).isEqualTo(3);
}
...
Member m = new Member("hhh", 20);
...
memberRepository.save(m);
// when
int resultCount = memberRepository.bulkAgePlus(15);
System.out.println("resultCount : " + resultCount);
Member memberH1 = memberRepository.findMemberByUsername("hhh");
System.out.println("memberH1 : " + memberH1); // Member(id=8, username=hhh, age=20)
em.clear();
Member memberH2 = memberRepository.findMemberByUsername("hhh");
System.out.println("memberH2 : " + memberH2); // Member(id=8, username=hhh, age=21)
영속성 컨텍스트를 거치치 않고 DB 업데이트 쿼리를 날리기 때문에,
업데이트 된 값을 사용하고 싶으면, clear 를 해주어야함
- 가능하면 벌크연산 후에 EntityManager 를 flush, clear 해버려라!!
업데이트 함수에서 먼저 flush 를 실행함!!- 이걸 생략하고 싶으면,
@Modifying(clearAutomatically = true)
추가@Modifying(clearAutomatically = true) @Query("update Member m set m.age = m.age + 1 where m.age >= :age") int bulkAgePlus(@Param("age") int age);
// em.clear();
평범하게 findAll 쿼리메소드를 활용하면, lazy 같은 것들은 어떻게 해결 할까?
여기서 fetch 조인은 어떻게 해야할까!!
@xToOne
관계에 있는 요소들을 N+1 문제가 있다고, fetch 조인 하는 것은 아님 @Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
Query 어노테이션을 활용해서, 이와 같이 하면 된다!
그런데 이것마저 귀찮다!!!!!
@EntityGraph
로 해결!!!
public interface MemberRepository extends JpaRepository<Member, Long> {
...
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
}
findAll 자체에 fetch join 을 @Override
하면서, @EntityGraph
설정!!
@Query("select m from Member m left join m.team")
@EntityGraph(attributePaths = {"team"})
List<Member> findMemberEntityGraph();
이렇게 쓰는 것도 가능하다!!
쿼리 메소드의 경우도 @EntityGraph
를 추가하면서, fetch join 기능이 가능하도록 해줌!!
Transaction 이 끝나는 (flush 가 일어남) 시점에 update 쿼리가 실행됨.
하지만, @QueryHints
와 @QueryHint
를 통해 read only 라는 설정을 주게 되면
public interface MemberRepository extends JpaRepository<Member, Long> {
...
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
}
분명 setUsername을 하여, findMember 에 변경이 일어났는데 이를 체크하지 않고 업데이트 쿼리를 실행하지 않음!!
(각 엔티티마다 스냅샷을 지정해주고, 변경감지 등의 리소스를 사용하는 행위들을 하지 않도록 하여 성능을 향상 시킨다!!)
우리 서버 트래픽이 너무너무 많아서 최적화 시키는게 아닌 이상,
이 Read-only 기능은 다른 최적화들에 비해 미미하다!!
페치 조인, 페이징 등등이 훨씬 더 중요!
Lock 이라는 기능도 있는데, 동시 접근하여 수정을 하게 되면 안되므로, lock을 걸어 권한 있는 쪽만 수정하도록 하는 것!!
실무에서 꽤나 중요해 보임!!
https://velog.io/@backtony/JPA-Lock
꼭 따로 공부해야함!