티스토리 뷰

반응형





Spring Data JPA 적용

- 환경설정

- Repository 리팩토링

- 명세 적용




| 환경설정



ㅇ pom.xml에 spring-data-jpa 라이브러리 추가

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>1.8.0.RELEASE</version>
</dependency>
 
cs


ㅇ appConfig.xml에 <jpa:repositories> 추가 후 base-package 속성에 repository 위치 지정

1
<jpa:repositories base-package="jpabook.jpashop.repository" />
cs




| Repository Refactoring



기존 Repository들이 Spring Data JPA를 사용하도록 Refactoring


||| Member Repository



c. 기존 Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
 
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
 
@Repository
public class MemberRepository {
 
    @PersistenceContext
    EntityManager em;
 
    public void save(Member member) {
        em.persist(member);
    }
 
    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }
 
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
 
    public List<Member> findByName(String name) {
        return em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }
}
 
cs


c. 리펙토링 후

- 클래스를 인터페이스로 변경

- Spring Data JPA가 제공하는 JpaRepository를 상속받고 제네릭 타입 지정

- JpaRepository가 제공하는 기본 메소드 save(), findOne(), findAll() 제거

1
2
3
4
5
6
7
8
9
10
import jpabook.jpashop.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
 
import java.util.List;
 
public interface MemberRepository extends JpaRepository<Member, Long> {
 
    List<Member> findByName(String name);
}
 
cs


||| Item Repository



c. 기존 Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import jpabook.jpashop.domain.item.Item;
import org.springframework.stereotype.Repository;
 
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
 
@Repository
public class ItemRepository {
 
    @PersistenceContext
    EntityManager em;
 
    public void save(Item item) {
        if (item.getId() == null) {
            em.persist(item);
        } else {
            em.merge(item);
        }
    }
 
    public Item findOne(Long id) {
        return em.find(Item.class, id);
    }
 
    public List<Item> findAll() {
        return em.createQuery("select i from Item i",Item.class).getResultList();
    }
}
 
cs


c. 리펙토링 후

- ItemRepository가 제공하는 모든 기능은 JpaRepository가 제공하는 공통 인터페이스로

1
2
3
4
5
6
import jpabook.jpashop.domain.item.Item;
import org.springframework.data.jpa.repository.JpaRepository;
 
public interface ItemRepository extends JpaRepository<Item, Long> {
 
}
cs


||| Order Repository



c. 기존 Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Repository
public class OrderRepository {
 
    @PersistenceContext
    EntityManager em;
 
    public void save(Order order) {
        em.persist(order);
    }
 
    public Order findOne(Long id) {
        return em.find(Order.class, id);
    }
 
    public List<Order> findAll(OrderSearch orderSearch) {
 
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Order> cq = cb.createQuery(Order.class);
        Root<Order> o = cq.from(Order.class);
 
        List<Predicate> criteria = new ArrayList<Predicate>();
 
        //주문 상태 검색
        if (orderSearch.getOrderStatus() != null) {
            Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
            criteria.add(status);
        }
        //회원 이름 검색
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인
            Predicate name = cb.like(m.<String>get("name"), "%" + orderSearch.getMemberName() + "%");
            criteria.add(name);
        }
 
        cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
        TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대 검색 1000 건으로 제한
        return query.getResultList();
    }
}
 
cs


c. 리펙토링 후

- findAll()의 복잡한 로직은 명세 기능을 사용

- 명세 기능 사용을 위해 JpaSpecificationExecutor 추가 상속

1
2
3
4
5
6
7
8
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.custom.CustomOrderRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order>, CustomOrderRepository {
 
}
cs



| 명세(Specification) 적용



c. 명세를 작성하기 위한 OrderSpec 클래스 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
 
import javax.persistence.criteria.*;
 
public class OrderSpec {
 
    public static Specification<Order> memberNameLike(final String memberName) {
        return new Specification<Order>() {
            public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
 
                if (StringUtils.isEmpty(memberName)) return null;
 
                Join<Order, Member> m = root.join("member", JoinType.INNER); //회원과 조인
                return builder.like(m.<String>get("name"), "%" + memberName + "%");
            }
        };
    }
 
    public static Specification<Order> orderStatusEq(final OrderStatus orderStatus) {
        return new Specification<Order>() {
            public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
 
                if (orderStatus == nullreturn null;
 
                return builder.equal(root.get("status"), orderStatus);
            }
        };
    }
}
cs


c. 검색 조건을 가지고 있는 OrderSearch 객체에 Specification을 생성하도록 코드 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderSearch {
 
    private String memberName;       //회원 이름
    private OrderStatus orderStatus; //주문 상태
 
   // Getter, Setter
 
    public Specifications<Order> toSpecification() {
        return where(memberNameLike(memberName))
                .and(orderStatusEq(orderStatus));
    }
 
}
cs


c. Repository 검색 코드가 명세를 파라미터로 넘기도록 Order Service 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
@Transactional
public class OrderService {
 
    @Autowired MemberRepository memberRepository;
    @Autowired OrderRepository orderRepository;
    @Autowired ItemService itemService;
 
    // 주문, 주문 취소
 
    /**
     * 주문 검색
     */
    public List<Order> findOrders(OrderSearch orderSearch) {
        // return orderRepository.findAll(orderSearch); // 리펙토리 전
        return orderRepository.findAll(orderSearch.toSpecification()); // Specification 사용
        //return orderRepository.search(orderSearch);  //QueryDSL 사용
    }
 
}
 
cs




| QueryDSL 통합



Spring Data JPA는 두 가지 방법으로 QueryDSL 지원

org.springframework.data.querydsl.QuerydslPredicateExecutor

org.springframework.data.jpa.repository.support.QuerydslRepositorySupport



|| QuerydslPredicateExecutor



QuerydslPredicateExecutor 는 Spring Data JPA에서 편리하게 QueryDSL을 사용할 수 있지만 기능에 한계

- 예로 join, fetch는 사용할 수 없음

- QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 QuerydslRepositorySupport 사용


c. Repository에서 QuerydslPredicateExecutor 상속

1
2
3
4
public interface itemRepository extends 
        JpaRepository<Item. Long>, QuerydslPredicateExecutor<Item> {
 
}
cs


c. QueryDSL 사용 예시

1
2
3
4
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll (
    item.name.contains("장난감").and(item.price.between(1000020000))
);
cs


QueryDSL을 검색조건으로 사용하면 Spring Data JPA가 제공하는 페이징과 정렬 기능도 함께 사용 가능

c. QuerydslPredicateExecutor Interface

1
2
3
4
5
6
7
8
public interface QuerydslPredicateExecutor<T> {
 
    T findOne(Predicate predicate);
    Iterable<T> findAll (Predicate predicate);
    Iterable<T> findAll (Predicate predicate, OrderSpecifier<?>... orders);
    Page<T> findAll (Predicate predicate, Pageable pageable);
    long count (Predicate predicate);
}
cs



|| QuerydslRepositorySupport


--

QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용

QuerydslRepositorySupport를 상속받아 사용하면 더 편리하게 QueryDSL 사용 가능


c. 사용자 정의 Repository

- 공통 인터페이스는 직접 구현할 수 없기 떄문에 사용자 정의 Repository 생성

1
2
3
4
5
6
7
8
9
10
11
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
 
import java.util.List;
 
public interface CustomOrderRepository {
 
    public List<Order> search(OrderSearch orderSearch);
 
}
 
cs


c. QuerydslRepositorySupport 사용

- 검색 조건에 따라 동적으로 쿼리 생성 (line 25~32)

- 생성자에서 QuerydslRepositorySupport에 엔티티 클래스 정보를 넘겨주어야 함 (line 13~15)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.mysema.query.jpa.JPQLQuery;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import jpabook.jpashop.domain.QMember;
import jpabook.jpashop.domain.QOrder;
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
import org.springframework.util.StringUtils;
 
import java.util.List;
 
public class OrderRepositoryImpl extends QueryDslRepositorySupport implements CustomOrderRepository {
 
    public OrderRepositoryImpl() {
        super(Order.class);
    }
 
    @Override
    public List<Order> search(OrderSearch orderSearch) {
 
        QOrder order = QOrder.order;
        QMember member = QMember.member;
 
        JPQLQuery query = from(order);
 
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            query.leftJoin(order.member, member)
                    .where(member.name.contains(orderSearch.getMemberName()));
        }
 
        if (orderSearch.getOrderStatus() != null) {
            query.where(order.status.eq(orderSearch.getOrderStatus()));
        }
 
        return query.list(order);
    }
}
cs


||| QuerydslRepositorySupport 핵심 기능


--

c. QuerydslRepositorySupport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Repository
public abstract class QuerydslRepositorySupport {
 
    // 엔티티 매니저 반환
    protected EntityManager getEntityManager() {
        return entityManager;
    }
 
    // from 절 반환
    protected JPQLQuery from(EntityPath<?>... paths) {
        return querydsl.createQuery(paths);
    }
 
    // QueryDSL delete 절 반환
    protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
        retirn new JPADeleteClause(entityManager, path);
    }
 
    // QueryDSL update 절 반환
    protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
        return new JPAUpdateClause(entityManager, path);
    }
 
    // Spring Data JPA가 제공하는 QueryDSL을 편하게 사용하도록 돕는 헬터 객체 반환
    protected Querydsl getQuerydsl() {
        return this.querydsl;
    }
}
cs





출처 : 자바 ORM 표준 JPA 프로그래밍

https://github.com/holyeye/jpabook/tree/master/ch12-springdata-shop

반응형
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday