JPA를 사용하여 프로젝트를 진행하면서, 많은 코드들이 중복된다는 것을 깨달았고, 조금더 간편한 방식으로 데이터베이스 액세스 작업을 처리할 수 없을까에 대해서 고민하던 와중에 Spring Data JPA 를 알게되었고, Spring Data JPA 를 배우면서 중요하다고 생각 되는 내용들만 기록 하였다.
JavaConfig 설정- 스프링 부트 사용시 생략 가능
스프링 부트 사용시 @SpringBootApplication 위치를 지정(해당 패키지와 하위 패키지 인식)
만약 위치가 달라지면 @EnableJpaRepositories 필요
//@Repository 어노테이션 필요하지 않음 //<엔티티, 엔티티식별자>
public interface MemberRepository extends JpaRepository<Member, Long>{
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
//구현하지 않아도 된다
}
@SpringBootTest
@Transactional
public class MemberJpaRepositoryTest {
@Autowired
MemberRepository memberRepository; //JPA 가 proxy class를 생성해준다
}
조회: find…By ,read…By ,query…By get…By,
예:) findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.
COUNT: count…By 반환타입 long
EXISTS: exists…By 반환타입 boolean
삭제: delete…By, remove…By 반환타입 long
DISTINCT: findDistinct, findMemberDistinctBy
LIMIT: findFirst3, findFirst, findTop, findTop3
비교할수있는 여러가지 쿼리가 더있다. 자세한건 문서를 참고하자
이러한 쿼리 메소드들은 entity 의 필드 값이 변경될때마다 수정해줘한다. 로딩 시점에 오류를 인지할 수 있다는 장점이 있다
컴파일시점에 오류를 잡아주는 장점이있다.
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
....
}
//순수 JPA 로 Namedquery 호출
public class MemberRepository {
public List<Member> findByUsername(String username){
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
}
//data jpa 로 Namedquery 호출
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@Query(name = "Member.findByUsername") //entity의 namedquery를 자동으로 확인하기 때문에 생략가능
List<Member> findByUsername(@Param("username") String username);
}
참고: 스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다고 한다. 대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다
간단하게 작성가능, 컴파일 시점에 오류를 잡아준다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@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 기능을 자주 사용하게 된다고한다.
DTO로 직접 조회하려먼 JPA 의 new 명령어를 사용해야 한다
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();;
}
@Data
public class MemberDto {
private Long id;
private String username;
private String teamName;
public MemberDto(Long id, String username, String teamName) {
this.id = id;
this.username = username;
this.teamName = teamName;
}
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findListByUsername(String name); //컬렉션
Member findOneByUsername(String name); //단건
Optional<Member> findOptionalByUsername(String name); //단건 Optional
}
결과가 있을수도 있고 없을수도 있다면 Optional 타입을 쓰는게 좋다
반환타입이 컬렉션인데 결과가 없으면 빈 컬렉션을 반환한다
단건조회인데 결과가 없으면 null, 결과가 2건이상이면 javax.persistence.NonUniqueResultException 예외 발생
select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
}
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
}
코드 가독성과 유지보수를 위해 이름 기반을 우선으로 하자
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")
.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 Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
//Slice 타입으로 선언한다면, total count 쿼리를 줄일수있다.
//limit +1 까지만 count
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);
}
@Test
public void page() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));
//when
PageRequest pageRequest = PageRequest.of(0, 3, [Sort.by](http://sort.by/)(Sort.Direction.DESC,
"username")); //(현재페이지, 조회할 데이터수,정렬정보)
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//then
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(); //다음 페이지가 있는가?
}
두 번째 파라미터로 받은 Pageable 은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 PageRequest 객체를 사용한다
주의: Page는 1부터 시작이 아니라 0부터 시작이다.
countQuery 와 value 를 분리함으로써 , total count 의 쿼리는 join 이 일어나지 않도록 성능을 최적화 할수있다. 실무에서 매우 중요!!
@Query(value = "select m from Member m",
countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
페이지를 유지하면서 엔티티를 DTO 로 변환하여 결과를 보내고자 할때
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
//순수JPA를 사용한 벌크성 수정 쿼리
public int bulkAgePlus(int age) {
int resultCount = em.createQuery("update Member m set m.age = m.age + 1" +"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
//Spring Data JPA 를 사용한 벌크성 수정 쿼리
@Modifying(clearAutomatically = true) //update 쿼리는 요게 있어야함 // claerAutomatically true하면 em.clear() 필요없어짐
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
clear() 없이 findById 로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 하자
@Query("select m from Member m left join fetch m.team")
//1 + N 문제를 해결하기 위해 fetch join 활용
List<Member> findMemberFetchJoin();
@Override
@EntityGraph(attributePaths = {"team"})// JPQL 짜기 싫고, fetch join을 넣어야 한다면
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m") // jpql 에 fetch join 만 추가하고싶어
List<Member> findMemberEntityGraph();
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
@NamedEntityGraph(name = "Member.all", attributeNodes =@NamedAttributeNode("team"))
@Entity
public class Member {
}
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
JPA 구현체에게 제공되는 힌트이다
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true")) //변경감지를 안함
Member findReadOnlyByUsername(String username);
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
forCounting : 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true )
복잡한 Query 가 필요하여 interface안에서 @Query 로만 해결이 불가능할때(entitymanager 등 구현체에서 필요한것들이있을때) ,interface를 구현해야 하는데 , 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다. 따라서,새로운 interface를 만들어서 interface를 구현하고 구현체를 작성하여 extends하거나 그냥 새로운 Repository 를 생성 해야한다.(핵심 비지니스 로직을 잘따져서 선택해야함)
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
public interface MemberRepositoryCustom { //사용자 정의 인터페이스
List<Member> findMemberCustom();
}
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom { //Impl 이름규칙
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m").getResultList();
}
}
엔티티를 생성, 변경할 때 변경한 사람과 시간을 남긴다
순수 JPA 사용
@MappedSuperclass //상속느낌
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
public class Member extends JpaBaseEntity {}
스프링 데이터 JPA 사용
@EntityListeners(AuditingEntityListener.class) //이걸 생략하고 orm.xml에 등록하면 전체 적용이가능하다
@MappedSuperclass
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
@EnableJpaAuditing //(modifyOnCreate =false)를 넣으면 저장시점에 저장데이터만 입력
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean //등록자와 수정자를 처리해준다
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
}
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) { //도메인 클래스 컨버터 (조회용으로만 써야함)
return member.getUsername();
}
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
요청 파라미터 예) /members?page=0&size=3&sort=id,desc&sort=username,desc
page: 현재 페이지, 0부터 시작한다.
size: 한 페이지에 노출할 데이터 건수
sort: 정렬 조건을 정의한다. 예) 정렬 속성,정렬 속성...(ASC | DESC)
글로벌 설정법
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
개별설정법
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username",direction = Sort.Direction.DESC) Pageable pageable) {
...
}
페이징 정보가 둘 이상일때
예제: /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
page.map()을 사용해서 DTO 를 반환하도록 하자!
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> pageDto = page.map(MemberDto::new);
return pageDto;
}
스프링 데이터는 Page를 0부터 시작한다.
만약 1부터 시작하려면?
MemberRepository (interface) → extends -> JpaRepository(interface) → implements -> SimpleJpaRepository(구현체)
@Repository 를 적용함으로써 JPA 예외를 스프링이 추상화한 예외로 변환
@Transactional(readOnly= true)가 기본으로 설정되어있음 → flush 생략
@Transactional 적용으로 등록, 수정 ,삭제 메서드를 처리
save() 메서드 :새로운 엔티티면 저장(persist), 새로운 엔티티가 아니면 병합(merge)
JPA 식별자 생성 전략이 @GenerateValue 면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다. 그런데 JPA 식별자 생성 전략이 @Id 만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save() 를 호출한다. 따라서 이 경우 merge() 가 호출된다. merge() 는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율 적이다. 따라서 Persistable 를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다.
등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할수 있다. (@CreatedDate에 값이 없으면 새로운 엔티티로 판단)
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
}
Specifications(명세) - 구현하면 JPA criteria 를 활용해서 사용가능
실무에서는 JPA criteria 를 거의 안쓴다! 대신에 QueryDSL 을 사용하자.
Query By Example - 도메인 객체를 그대로 사용한다(Example)
실무에서 사용하기에는 매칭조건이 너무 단순하고, LEFT조인이 안됨
실무에서는 QueryDSL 을 사용하자
Projections - 실무의 복잡한 쿼리를 해결하기에는 한계가 있다. QueryDSL!
네이티브 쿼리 - QueryDSL!