org.springframework.data.jpa.repository.support.SimpleJpaRepository
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ...{
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
} }
}
@Repository
적용: JPA 예외를 스프링이 추상화한 예외로 변환 @Transactional
트랜잭션 적용@Transactional(readOnly = true)
readOnly = true
옵션을 사용하면 플러시 를 생략해서 약간의 성능 향상을 얻을 수 있음save()
메서드*persist
) merge
)null
로 판단 0
으로판단Persistable
인터페이스를 구현해서 판단 로직 변경 가능Persistable
package org.springframework.data.domain;
public interface Persistable<ID> {
ID getId();
boolean isNew();
}
참고: 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;
}
책 도메인 주도 설계(Domain Driven Design)는 SPECIFICATION(명세)라는 개념을 소개
스프링 데이터 JPA는 JPA Criteria를 활용해서 이 개념을 사용할 수 있도록 지원
술어(predicate)
org.springframework.data.jpa.domain.Specification
클래스로 정의명세 사용 코드
@Test
public void specBasic() throws Exception {
//given
Team teamA = new Team("teamA");
em.persist(teamA);
Member m1 = new Member("m1", 0, teamA);
Member m2 = new Member("m2", 0, teamA);
em.persist(m1);
em.persist(m2);
em.flush();
em.clear();
//when
Specification<Member> spec =
MemberSpec.username("m1").and(MemberSpec.teamName("teamA"));
List<Member> result = memberRepository.findAll(spec);
//then
Assertions.assertThat(result.size()).isEqualTo(1);
}
Specification
을 구현하면 명세들을 조립할 수 있음. where()
, and()
, or()
, not()
제공
findAll
을 보면 회원 이름 명세( username
)와 팀 이름 명세( teamName
)를 and
로 조합해서 검색 조건으로 사용
MemberSpec
명세 정의 코드
public class MemberSpec {
public static Specification<Member> teamName(final String teamName) {
return (Specification<Member>) (root, query, builder) -> {
if (StringUtils.isEmpty(teamName)) {
return null;
}
Join<Member, Team> t = root.join("team", JoinType.INNER); //회원과 조인
return builder.equal(t.get("name"), teamName);
};
}
public static Specification<Member> username(final String username) {
return (Specification<Member>) (root, query, builder) ->
builder.equal(root.get("username"), username);
}
}
명세를 정의하려면 Specification
인터페이스를 구현
명세를 정의할 때는 toPredicate(...)
메서드만 구현하면 되는데 JPA Criteria의 Root
, CriteriaQuery
, CriteriaBuilder
클래스를 파라미터 제공
예제에서는 편의상 람다를 사용
참고: 실무에서는 JPA Criteria를 거의 안쓴다! 대신에 QueryDSL을 사용하자.
@SpringBootTest
@Transactional
public class QueryByExampleTest {
@Autowired MemberRepository memberRepository;
@Autowired EntityManager em;
@Test
public void basic() throws Exception {
//given
Team teamA = new Team("teamA");
em.persist(teamA);
em.persist(new Member("m1", 0, teamA));
em.persist(new Member("m2", 0, teamA));
em.flush();
//when
//Probe 생성
Member member = new Member("m1");
Team team = new Team("teamA"); //내부조인으로 teamA 가능 member.setTeam(team);
//ExampleMatcher 생성, age 프로퍼티는 무시 ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("age");
Example<Member> example = Example.of(member, matcher);
List<Member> result = memberRepository.findAll(example);
//then
assertThat(result.size()).isEqualTo(1);
}
}
Probe: 필드에 데이터가 있는 실제 도메인 객체
ExampleMatcher: 특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능
Example: Probe와 ExampleMatcher로 구성, 쿼리를 생성하는데 사용
장점
JpaRepository
인터페이스에 이미 포함단점
firstname = ?0 or (firstname = ?1 and lastname = ?2)
starts/contains/ends/regex
=
)만 지원정리
- 실무에서 사용하기에는 매칭 조건이 너무 단순하고, LEFT 조인이 안됨
- 실무에서는 QueryDSL을 사용하자
엔티티 대신에 DTO를 편리하게 조회할 때 사용
전체 엔티티가 아니라 만약 회원 이름만 딱 조회하고 싶으면?
public interface UsernameOnly {
String getUsername();
}
public interface MemberRepository ... {
List<UsernameOnly> findProjectionsByUsername(String username);
클래스 기반 Projection
다음과 같이 인터페이스가 아닌 구체적인 DTO 형식도 가능
생성자의 파라미터 이름으로 매칭
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
주의
정리
가급적 네이티브 쿼리는 사용하지 않는게 좋음, 정말 어쩔 수 없을 때 사용 최근에 나온 궁극의 방법 스프링 데이터 Projections 활용
스프링 데이터 JPA 기반 네이티브 쿼리
페이징 지원
반환 타입
제약