[Spring Data JPA] 1.0.0.RC1 Chapter 2
2장. JPA 리파지토리
이 챕터에서는 JPA 리파지토리 구현체를 자세히 설명한다.
2.1. 쿼리 메서드
2.1.1. 쿼리 룩업 전략
JPA 모듈은 쿼리를 직접 정의하는 방법과 메서드 이름으로 정의하는 방법을 제공한다.
선언된 쿼리
메서드 이름을 사용해서 쿼리를 만들수도 있지만 사용하고 싶은 키워드를 지원하지 않는다거나, 메서드 이름이 너무 장황해지길 원치 않을 수도 있다. 그래서 JPA 네임드 쿼리를 사용하거나 쿼리 메서드에 @Query 애노테이션으르 사용할 수 있다.
전략
JPA @NamedQeury 또는 Hades @Query 애노테이션을 찾아내는 전략이다. 선언된 쿼리를 찾지 못하면 쿼리 실행은 실패한다.
CREATE_IF_NOT_FOUND(기본값)
1장에서 설명을 했을텐데…
2.1.2. 쿼리 생성
기본적으로 쿼리 생성은 1.3 “쿼리 메서드”에서 설명한 방식으로 만들어 진다.
예제 2.1. 메서드 이름으로 쿼리 만들기
public interface UserRepository extends Repository<User, Long> {
List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
메서드 이름을 읽어서 JPA Criteria API를 사용해서 쿼리를 만들지만 결국 다음과 같은 쿼리가 된다.
select u from User u where u.emailAddress = ?1 and u.lastname = ?2
Spring Data JPA는 1.3.2.2.1 “property expressions”에서 설명한대로 속성을 확인한다. 다음 표에서 JPA가 지원하는 키워드를 살펴보고 키워드가 어떻게 해석되는지 살펴보자.
표 2.1. 메서드 이름으로 지원하는 키워드
Keyword | Sample | JPQL snippet |
---|---|---|
And |
findByLastnameAndFirstname |
… where x.lastname = ?1 and x.firstname = ?2 |
Or |
findByLastnameOrFirstname |
… where x.lastname = ?1 or x.firstname = ?2 |
Between |
findByStartDateBetween |
… where x.startDate between 1? and ?2 |
LessThan |
findByAgeLessThan |
… where x.age < ?1 |
GreaterThan |
findByAgeGreaterThan |
… where x.age > ?1 |
IsNull |
findByAgeIsNull |
… where x.age is null |
IsNotNull,NotNull |
findByAge(Is)NotNull |
… where x.age not null |
Like |
findByFirstnameLike |
… where x.firstname like ?1 |
NotLike |
findByFirstnameNotLike |
… where x.firstname not like ?1 |
OrderBy |
findByAgeOrderByLastnameDesc |
… where x.age = ?1 order by x.lastname desc |
Not |
findByLastnameNot |
… where x.lastname <> ?1 |
In |
findByAgeIn(Collection<Age> ages) |
… where x.age in ?1 |
NotIn |
findByAgeNotIn(Collection<Age> age) |
… where x.age not in ?1 |
In이나 NotIn을 사용할 때 매개변수로 Collection 말고 배열이나 가변인자도 사용할 수 있다.
2.1.3. JPA NamedQueries 사용하기
예제에서는 간단하게 <named-query/> 엘리먼트와 @NamedQeury 애노테이션을 사용한다. 여기서 사용할 쿼리는 JPA 쿼리 언어(JQL)로 작성되어야 한다. 물론, <named-native-query /> 또는 @NamedNativeQuery를 사용할 수도 있다. 이 엘리먼트들은 DB에 특화된 네이티브 SQL을 사용할 수 있다.
XML named 쿼리 정의
XML 설정을 사용하려면, 클래스패스에 있는 META-INF 폴더의 orgm.xml에 <named-query/> 엘리먼트를 추가하라.
예제 2.2. XML named 쿼리 설정
[xml]
<named-query name="User.findByLastname">
<query>select u from User u where u.lastname = ?1</query>
</named-query>
[/xml]
보시다시피 쿼리마다 유일한 이름을 가지게 된다.
애노테이션 설정
애노테이션 설정은 다른 설정 파일을 작성할 필요가 없어서 유지 보수 비용을 낮출 수 있다는 장점이 있다. 대신 쿼리를 수정하면 도메인 클래스를 다시 컴파일 해야 하는 비용이 있다.
예제 2.3. 애노테이션 기반 named 쿼리 설정
[java]
@Entity
@NamedQuery(name = "User.findByEmailAddress",
query = "select u from User u where u.emailAddress = ?1")
public class User {
}
[/java]
인터페이스 정의하기
네임드 쿼리를 실행하려면 UserRepository에 다음과 같이 정의하면 된다.
예제 2.4. UserRepository에 쿼리 메서드 선언
[java]
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastname(String lastname);
User findByEmailAddress(String emailAddress);
}
[/java]
도메인 클래스 이름 뒤에 점을 붙인 다음 메서드 이름을 붙여서(User.findByLastname) 네임드 쿼리를 찾는다.
2.1.4. @Query 사용하기
실행하려는 쿼리가 특정 메서드에 묶여있기 때문에 Spring Data JPA의 @Query 애노테이션을 사용해서 도메인 클래스 보다는 메서드에 선언할 수 있다. 이렇게 하면 도메인 클래스가 저장소 관련 정보를 담고 있지 않게 할 수 있으며, 쿼리를 리파지토리 인터페이스에 둘 수 있다.
예제 2.5. @Query 사용해서 쿼리 메서드에 쿼리 선언하기
[java]
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
[/java]
2.1.5. 네임드 매개변수 사용하기
기본으로 Spring Data JPA는 위치 기반 매개변수 바인딩을 사용한다. 이런 식의 쿼리는 약간 에러 발생 가능성이 높고 위치를 조정하기 어렵ㄴ다. 이 이슈를 해결하려면 @Param 애노테이션을 사용해서 정확한 이름을 사용해서 쿼리에 있는 이름에 바인딩할 수 있다.
에제 2.6. 네임드 매개변수 사용하기
[java]
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname,
@Param("firstname") String firstname);
}
[/java]
메서드 매개변수는 순서가 실제 SQL에서 사용된 순서와 같지 않아도 된다는 점을 눈여겨 보시라.
2.1.6. 수정 쿼리
가져오는 쿼리 말고 수정하는 쿼리도 커스터마이징 할 수 있다. 쿼리 메서드에 @Modifying을 추가하면 된다.
예제 2.7. 수정 쿼리 선언
[java]
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
[/java]
select가 아니라 update 쿼리를 실행한다. EntityManager가 수정 쿼리를 실행한 다음 변경되기 전 데이터를 가지고 있을 수 있기 때문에, 기본적으로 EntityManager.clear()를 실행한다. 이렇게 하면, EntityManager에 남아있던 동기화(flush)되지 않은 데이터를 모두 버리는데, 그렇게 하고 싶지 않다면 @Modifying 애노테이션의 clearAutomatically 속성을 false로 설정하자.
2.2 Specifications
JPA 2는 쿼리를 프로그래밍으로 만들 수 있도록 Criteria API를 추가했다.
Spring Data JPA는 에릭 에반스의 DDD에서 specification 개념을 차용했다. JPA Criteria API를 사용해서 Specification을 만들 수 있다. 그렇게 하려면 JpaSpecificationExecutor 인터페이스를 리파지토리에 추가 확장 시켜야 한다.
[java]
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
…
}
[/java]
여기서 추가된 인터페이스의 메서드로는 다음과 같은 것들이 있다.
[java]
List<T> readAll(Specification<T> spec);
[/java]
Specification 인터페이스는 다음과 같이 생겼다.
[java]
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder);
}
[/java]
좋아. 그래서 이걸 어떻게 쓰냐고? Specification은 확장 가능한 Predicate를 손쉽게 만드는데 사용한다. 그리고 그렇게 만든 것들을 조합해서 활용할 수 있다.
예제 2.8. Customer 스팩
[java]
public class CustomerSpecs {
public static Specification<Customer> isLongTermCustomer() {
return new Specification<Customer>() {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
LocalDate date = new LocalDate().minusYears(2);
return builder.lessThan(root.get(Customer_.createdAt), date);
}
};
}
public static Specification<Customer> hasSalesOfMoreThan(MontaryAmount value) {
return new Specification<Customer>() {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// build query here
}
};
}
}
[/java]
구체적인 구현은 생략했다. 클라이언트는 이렇게 만들어둔 스팩을 다음과 같이 사용할 수 있다.
예제 2.9. 스팩 사용하기
[java]
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());
[/java]
그냥 쿼리 하나 만들지 뭐하러 이렇게 하냐는 생각이 들겠지만, 스팩을 하나만 사용했을 때는 그 진가를 잘 발휘하지 못한다. 스팩의 진정한 힘은 스팩 여러개를 조합해서 새로운 스팩을 만들 때 그 빛을 발한다. 스프링에서 제공하는 스팩 헬퍼 클래스(이게 뭔진 안보이는데.. 흠..)의 도움을 받아서 다음가 같이 만들 수 있다.
예제 2.10. 스팩 조합하기
[java]
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.readAll(
where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));
[/java]
보시다시피, 스팩을 조합하고 체인 형태로 만들 수 있다. 따라서 데이터 접근 계층을 확장하는 것은 새로운 스팩 구현체를 만들거나, 기존의 스팩을 어떻게 조합하느냐의 문제가 된다.
2.3. Transactionality
리파지토리의 CRUD 메서드는 기본적으로 트랜잭셔널하다. 읽어오는 오퍼레이션에는 기본으로 readOnly 플래스가 true로 설정되어 있다. 다른 것들은 모두 @Transactional이 붙어있다. 자세한 내용은 Repository 자바독을 살펴보자. 트랜잭션 설정을 바꾸려면 자신의 리파지토리 인터페이스에서 다음과 같이 @Transactional을 재정의하면 된다.
예제 2.11. 커스텀 트랜잭션 설정
[java]
public interface UserRepository extends JpaRepository<User, Long> {
@Override
@Transactional(timeout = 10)
public List<User> findAll();
// Further query method declarations
}
[/java]
findAll() 메서드 실행 시간을 10초로 하고, readOnly 플래스를 제거했다.
또다르느 트랜잭션 설정은 퍼사드 또는 여러 리파지토리를 사용하는 서비스 구현체에서 트랜잭션을 담당하는 것이다.
예제 2.12. 여러 저장소 호출할 때 퍼사드 사용하기
[java]
@Service
class UserManagementImpl implements UserManagement {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Autowired
public UserManagementImpl(UserRepository userRepository,
RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}
@Transactional
public void addRoleToAllUsers(String roleName) {
Role role = roleRepository.findByName(roleName);
for (User user : userRepository.readAll()) {
user.addRole(role);
userRepository.save(user);
}
}
[/java]
이렇게 하면 저장소에 설정해둔 트랜잭션은 무시되고 서비스에 적용한 트랜잭션으로 동작한다. <tx:annotation=driven/>과 컴포넌스 스캔을 사용하고 있다는 가정하게 작성한 예제다.
2.3.1. 트랜잭셔널 쿼리 메서드
쿼리 메서드를 트랜잭션으로 묶으려면 @Transactional을 리파지토리 인터페이스에 사용하면 된다.
예제 2.13. 쿼리 메서드에 @Transactional 사용하기
[java]
@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastname(String lastname);
@Modifying
@Transactional
@Query("delete from User u where u.active = false")
void deleteInactiveUsers();
}
[/java]
해당 클래스의 모든 메서드의 readOnly 플래그를 true로 설정하고, deleteInactiveUsers 같은 경우는 수정하는 메서드니까 @Modifying을 붙이고, @Transactional을 재정의해서 readOnly를 제거했다.
*주의*
읽기 전용 오퍼레이션에는 반드시 readOnly 플래그를 true로 설정하는게 좋다. 이렇게 한다고 해서 수정 쿼리가 날아가는 것을 확인하는게 아니라, 기반으로 사용하고 있는 JDBC 드라이버에 힌트를 알려줘서 성능 최적화를 할 수 있게 해준다. 거기에 스프링이 기반하고 있는 JPA provider에 몇몇 최적화를 수행한다. 가령, 하이버네이트의 flush 모드를 NEVER로 설정해서 하이버네이트가 dirty checking을 무시하게 한다.
2.4. Auditing
대부분의 애플리케이션은 특정 엔티티의 작성일, 장성자, 갱신일, 갱신자를 기록하는 작업이 필요하다. Spring Data JPA는 이런 오딧 정보를 AOP를 사용해서 투명하게 처리한다. 이 기능을 도메인에 추가하려면 고급 인터페이스를 구현할 필요가 있다.
에제 2.14. Auditable 인터페이스
[java]
public interface Auditable<U, ID extends Serializable>
extends Persistable<ID> {
U getCreatedBy();
void setCreatedBy(U createdBy);
DateTime getCreatedDate();
void setCreated(Date creationDate);
U getLastModifiedBy();
void setLastModifiedBy(U lastModifiedBy);
DateTime getLastModifiedDate();
void setLastModified(Date lastModifiedDate);
}
[/java]
보시다시피 수정하려는 엔티티는 반드시 엔티티어야 한다. 보통 User 엔티티가 필요하기 때문에 U 매개변수 타입을 선택하자.
*주의*
코딩을 줄일 수 있도록 Spring Data JPA에서 AbstractPersisteable과 AbstractAuditable 기반 클래스를 제공한다. 그러니까, 단순히 인터페이스를 구현할지 아니면 기반 클래스를 확장해서 좀 더 복잡한 기능을 즐길지 맘대로해라.
일반적인 오딧 설정
Spring Data JPA는 오딧 정보를 캡춰할 수 있는 트리거로 사용할 엔티티 리스너를 제공한다. 우선, orm.xml에 모든 엔티티에 적용할 AuditingEntityListener를 등록해야 한다.
예제 2.15. orm.xml 오딧 설정
[xml]
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="….data.jpa.domain.support.AuditingEntityListener" />
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
[/xml]
이제 auditing 엘리먼트만 추가하면 된다.
예제 2.16. 스프링 설정에 오딧 기능 활성화하기
<jpa:auditing auditor-aware-ref="yourAuditorAwareBean" />
다음과 같이 생긴 AuditorAware 인터페이스의 구현체를 설정해야 한다.
예제 2.17. AuditorAware 인터페이스
[java]
public interface AuditorAware<T, ID extends Serializable> {
T getCurrentAuditor();
}
[/java]
보통 현재 시스템 사용자를 추적하는데 사용하는 인증 요소가 있을 것이다. 해당 컴포넌트는 보통 AuditAware여야 하며 오디터를 지속적으로 확인해야 한다.
2.5. 기타
2.5.1. 퍼시스턴스 유닛 합치기
모듈화 때문에 애플리케이션을 여럿으로 쪼개놨는데, 런타임 때 그렇게 여럿으로 쪼갠 것을 하나로 묶어서 잘 동작하는 확인하고 싶을 수 있다. 그래서 Spring Data JPA는 자동으로 퍼시스턴스 이름을 기준으로 합쳐주는 PersistenceUnitManager 구현체를 제공한다.
예제 2.18. MergingPersistenceUnitManager 사용하기
[xml]
<bean class="….LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitManager">
<bean class="….MergingPersistenceUnitManager" />
</property
</bean>
[/xml]