๐ŸƒSpring Data JPA

Spring JPA

3 minute read

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โ€ฆBy
  • readโ€ฆBy
  • queryโ€ฆBy
  • getโ€ฆBy
  • findHelloBy์ฒ˜๋Ÿผ โ€ฆ์— ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ๋‚ด์šฉ(์„ค๋ช…)์ด ๋“ค์–ด๊ฐ€๋„ ๋œ๋‹ค.

COUNT

  • long countโ€ฆBy()

EXISTS

  • boolean existsโ€ฆBy()

DELETE

  • long deleteโ€ฆBy()
  • long removeโ€ฆBy()

DISTINCT

  • findDistinct
  • findMemberDistinctBy

LIMIT

  • findFirst3
  • findFirst
  • findTop
  • findTop3

@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);  
}