๐Spring Data JPA
Spring JPA
Transaction
- ์คํ๋ง ๋ฐ์ดํฐ JPA๋
@Transaction์์ด๋ ๋ฑ๋ก, ์์ , ์ญ์ ๋ฅผ ์ฒ๋ฆฌํ๋ค. ์๋น์ค ๊ณ์ธต(์ธ๋ถ)์์ Transaction์ด ์์๋๋ฉด ํด๋น Transaction์ ์ ํ๋ฐ์์ ์ฌ์ฉํ๋ค.- ๊ทธ๋ ์ง ์๋ค๋ฉด
Repository(๋ด๋ถ)์์ ์์ฒด์ ์ผ๋ก Transaction์ ์์ํ๋ค.
์ฟผ๋ฆฌ ๋ฉ์๋
๋ฉ์๋ ์ด๋ฆ์ผ๋ก ์ฟผ๋ฆฌ ์์ฑ
- ์์ JPA
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
.setParameter("username", username)
.setParameter("age", age) .getResultList();
}
- ์คํ๋ง ๋ฐ์ดํฐ JPA
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
์กฐํ
findโฆByreadโฆByqueryโฆBygetโฆByfindHelloBy์ฒ๋ผ โฆ์ ์๋ณํ๊ธฐ ์ํ ๋ด์ฉ(์ค๋ช )์ด ๋ค์ด๊ฐ๋ ๋๋ค.
COUNT
long countโฆBy()
EXISTS
boolean existsโฆBy()
DELETE
long deleteโฆBy()long removeโฆBy()
DISTINCT
findDistinctfindMemberDistinctBy
LIMIT
findFirst3findFirstfindTopfindTop3
@Query๋ฅผ ํตํด Repository ๋ฉ์๋์ ์ฟผ๋ฆฌ ์ ์ํ๊ธฐ
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๋ฅผ ์์ฃผ ์ฌ์ฉํ๊ฒ ๋๋ค. - ์ฟผ๋ฆฌ๋ฌธ์ JPQL์ ํตํด ์์ฑํ๋ค.
@Param์ ํตํด ์ฟผ๋ฆฌ๋ฌธ์ ํ๋ผ๋ฉํฐ๋ฅผ ๋ฐ์ธ๋ฉํ๋ค.- ์๋์ ๊ฐ์ด ์ปฌ๋ ์
์ ๋ํด์๋
in์ ์์ ํ๋ผ๋ฉํฐ๋ฅผ ๋ฐ์ธ๋ฉ ํ ์ ์๋ค.
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
๊ฐ ํ์ ์กฐํํ๊ธฐ
@Query("select m.username from Member m")
List<String> findUsernameList();
DTO๋ก ์ง์ ์กฐํํ๊ธฐ
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
์กฐํ ๊ฒฐ๊ณผ
- ๋ค์ํ ํ์ ์ผ๋ก ์ง์ ์ด ๊ฐ๋ฅํ๋ค.
์ปฌ๋ ์
- ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด ๋น ์ปฌ๋ ์ ์ ๋ฐํํ๋ค.
List<Member> findByUsername(String name);
๋จ๊ฑด
- Optional์ ๋ถ์ผ ์๋ ์๋ค.
- ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด null, 2๊ฑด ์ด์์ผ ๊ฒฝ์ฐ
javax.persistence.NonUniqueResultException์ผ ๋ฐ์๋๋ค.
Member findByUsername(String name);
Optional<Member> findByUsername(String name);
๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ
- ๊ฐ๊ธ์ ์ฌ์ฉํ์ง ๋ง์.
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
ํ์ด์ง๊ณผ ์ ๋ ฌ
ํ๋ผ๋ฉํฐ
org.springframework.data.domain.Sort: ์ ๋ ฌ ๊ธฐ๋ฅorg.springframework.data.domain.Pageable: ํ์ด์ง ๊ธฐ๋ฅ (๋ด๋ถ์ Sort ํฌํจ) ํน๋ณํ ๋ฐํ ํ์
๋ฐํํ์
org.springframework.data.domain.Page: ์ถ๊ฐ count ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ํฌํจํ๋ ํ์ด์งorg.springframework.data.domain.Slice: ์ถ๊ฐ count ์ฟผ๋ฆฌ ์์ด ๋ค์ ํ์ด์ง๋ง ํ์ธ ๊ฐ๋ฅ (๋ด๋ถ์ ์ผ๋ก limit + 1์กฐํ)List (์๋ฐ ์ปฌ๋ ์ ): ์ถ๊ฐ count ์ฟผ๋ฆฌ ์์ด ๊ฒฐ๊ณผ๋ง ๋ฐํ
count ์ฟผ๋ฆฌ๋ ๋งค์ฐ ๋ฌด๊ฒ๋ค. ๋ณ๋๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ถ๋ฆฌํด์ ๋ง๋๋ ๊ฒ์ ์ถ์ฒํ๋ค.
์์
- Page๋ 1์ด ์๋๋ผ 0๋ถํฐ ์์ํ๋ ๊ฒ์ ์ฃผ์ํ์
Page<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์ฌ์ฉ
Slice<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์ฌ์ฉ ์ํจ
List<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์ฌ์ฉ ์ํจ
List<Member> findByUsername(String name, Sort sort);
// PageRequest๋ Pagable ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด์ด๋ค
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); Page page = memberRepository.findByAge(10, pageRequest);
List<Member> content = page.getContent(); //์กฐํ๋ ๋ฐ์ดํฐ
assertThat(content.size()).isEqualTo(3); //์กฐํ๋ ๋ฐ์ดํฐ ์
assertThat(page.getTotalElements()).isEqualTo(5); //์ ์ฒด ๋ฐ์ดํฐ ์
assertThat(page.getNumber()).isEqualTo(0); //ํ์ด์ง ๋ฒํธ
assertThat(page.getTotalPages()).isEqualTo(2); //์ ์ฒด ํ์ด์ง ๋ฒํธ
assertThat(page.isFirst()).isTrue(); //์ฒซ๋ฒ์งธ ํญ๋ชฉ์ธ๊ฐ?
assertThat(page.hasNext()).isTrue(); //๋ค์ ํ์ด์ง๊ฐ ์๋๊ฐ?
DTO ๋ณํ
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
Controller์์์ ํ์ฉ
- ๊ฐ๋จํ ๊ฒฝ์ฐ์ ๋งค์ฐ ํธ๋ฆฌํ๊ฒ Paging API๋ฅผ ๋ง๋ค ์ ์๋ค.
// ๊ธฐ๋ณธ ํ์ด์ง ์ฌ์ด์ฆ: spring.data.web.pageable.default-page-size=20
// ์ต๋ ํ์ด์ง ์ฌ์ด์ฆ: spring.data.web.pageable.max-page-size=2000
// "/members?page=0&size=3&sort=id,desc&sort=username,desc"
@GetMapping("/members")
public Page list(Pageable pageable) {
Page page = memberRepository.findAll(pageable);
return page;
}
// ์ง์ ์ค์ ๊ฐ์ ์ง์ ํ๋ ๊ฒฝ์ฐ
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username", direction = Sort.Direction.DESC) Pageable pageable) {
Page page = memberRepository.findAll(pageable);
return page;
}
// Dto๋ก ๋ฐํํ๋ ๊ฒฝ์ฐ
@GetMapping("/members")
public Page list(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}
๋๋ฉ์ธ ํด๋์ค ์ปจ๋ฒํฐ
- HTTP ํ๋ผ๋ฉํฐ๋ก ๋์ด์จ Entity์ id๋ก ๊ฐ์ฒด๋ฅผ ์ฐพ์์ ๋ฐ์ธ๋ฉํด์ค๋ค.
- ์ฃผ์ํ ์ ์ ๋จ์ ์กฐํ์ฉ์ผ๋ก Entity๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค๋ ์ ์ด๋ค. ๋ณ๊ฒฝํ๋๋ผ๋ DB์ ๋ฐ์๋์ง ์๋๋ค.
@RestController
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
}
Bulk Update ์ฟผ๋ฆฌ
- ๋ฒํฌ์ฑ ์์ ๋ฐ ์ญ์ ๋
@Modifying์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ค. - ๋ฒํฌ ์ฐ์ฐ์ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ๋ฌด์ํ๊ณ ์คํ๋๋ค. ๋ฐ๋ผ์ ๋๊ธฐํ๋ฅผ ์ํด ์์์ฑ ์ปจํ ์คํธ๋ฅผ ์ด๊ธฐํํด์ผ ํ๋ค.
- ๋ฒํฌ์ฑ ์ฟผ๋ฆฌ ํ ์์์ฑ ์ปจํ
์คํธ๋ฅผ ์ด๊ธฐํํ๋ ค๋ฉด
@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);
@EntityGraph
- ์ง์ฐ๋ก๋ฉ์ ํ๋ฅญํ ๊ธฐ๋ฅ์ด์ง๋ง
N+1๋ฌธ์ ๋ฅผ ๋ฐ์์ํค๊ธฐ๋ ํ๋ค. - ํด๊ฒฐ์ ์ํด
fetch join์ด๋ผ๋ ์ข์ ๋ฐฉ๋ฒ์ด ์์ง๋ง JPQL์ ์จ์ผํ๋ ๋ฒ๊ฑฐ๋ก์์ด ์๋ค. - ๊ฐํธํ ์ฌ์ฉ์ ์ํด
@EntityGraph๊ฐ ์ ๊ณต๋๊ณ ๋ด๋ถ์ ์ผ๋กLEFT OUTER JOIN์ด ์ฌ์ฉ๋๋ค.
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username);
@NamedEntityGraph
- Entity๋ฅผ ์ ์ํ ๋, EntityGraph์ ์ด๋ฆ์ ๋ถ์ฌ ํ์ฉํ ์ ์๋ค.
// Entity ํ์ผ
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team")) @Entity
public class Member {
}
// Repository ํ์ผ
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
JPA Hint์ Lock
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username); // update ์ฟผ๋ฆฌ ๋ฑ์ด ์คํ๋์ง ์๋๋ค.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
Projections
- ์ํ๋ ํํ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ค.
- ๋ณต์กํ ๊ฒฝ์ฐ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ๋ ์ด๋ ค์ฐ๋ฏ๋ก ๊ทธ๋ฐ ๊ฒฝ์ฐ์๋
QueryDSL์ ์ฌ์ฉํ์. - ๊ทธ๋๋ ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ๋ณด๋ค ๋ซ๋ค! ๊ทธ๋ฐ ๊ฒฝ์ฐ ์ฌ์ฉํ์.
// Getter ํํ๋ฅผ ์ฌ์ฉ
public interface UsernameOnly {
String getUsername();
}
// SpEL ๋ฌธ๋ฒ ์ฌ์ฉ(๋ค๋ง ๋ชจ๋ ๊ฐ๋ณ๋ก ์กฐํํ ํ ์กฐํฉํ๋ค)
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
String getUsername();
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<UsernameOnly> findProjectionsByUsername(String username);
}