Querydsl 장점
1. 고정된 SQL 문이 아닌 조건에 맞게 동적으로 쿼리를 생성할 수 있음
2. 비슷한 쿼리를 재사용할 수 있으며 제약 조건 조립 및 가독성을 향상시킬 수 있음.
3. 문자열이 아닌 자바 소스코드로 작성하기 때문에 컴파일 시점에서 오류를 발견할 수 있음.
4. IDE 의 도움을 받아서 자동 완성 기능을 이용할 수 있기 때문에 생산성을 향상
* 설정
1. pom.xml 의존성 추가
스프링 부트 2.6.1 기준
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.1</version> <relativePath/> <!-- lookup parent from repository --> </parent>
querydsl-jpa , querydsl-apt 다음을 추가
<dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>5.0.0</version> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>5.0.0</version> </dependency>
<plugins> </plugins> 사이에 다음을 추가
<plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin>
전체
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins> </build>
이클립스는 maven 업데이트 , 인텔리제이는 Reload All Maven Projects 후
컴파일을 하면 다음과 같이 target 디렉토리에 QDomain 들이 생성된다.
인텔리제이는 QDomain 임포트가 안 될 때가 있는데,
[File] -[Project Structure]-[Modules] 메뉴에 들어가서,
Mark as : Sources , Tests , Resources, Test Resources Excluded 중에서 Sources 버튼을 클릭 후
target 폴더 아래의 generated-sources 폴더를 클릭 하여 소스코드로 인식할 수 있게 처리
사용예
JPAQuery 데이터 반환 메소드
List<T> fetch() : 조회 결과 리스트 반환
T fetchOne : 조회 대상인 1건인 경우 제네릭으로 지정한 타입 반환
T fetchFirst() : 조회 대상 중 1건만 반환
Long fetchCount() : 조회 대상 개수 반환
QueryResult<T> fetchResults() : 조회한 리스트와 전체 개수를 포함한 QueryResults 반환
테스트로 Item Domain 을 생성후 컴파일 빌드하면
import lombok.Getter; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity @Getter public class Item { @Id @GeneratedValue private Long id; }
target 디렉토리에 QItem 이 생성 된다.
MemberStatus
public enum MemberStatus { GENERAL_MEMBER, FULL_MEMBER }
Member
import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import com.shop.constant.MemberStatus; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Entity @Table(name="member") @Getter @Setter @ToString public class Member { @Id @Column(name="member_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(nullable = false,length = 50) private String name; //회원이름 @Column(name="email", nullable = false) private String email; //이메일 @Column(name="age") private int age ; //나이 @Column(name="address",length = 150) private String address; //주소 @Enumerated(EnumType.STRING) private MemberStatus memberStatus; //일반회원, 정회원 }
MemberRepository
import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; import com.shop.entity.Member; public interface MemberRepository extends JpaRepository<Member, Long> , QuerydslPredicateExecutor<Member> { /** * <S extends T> save (S entry) 엔티티 저장 및 수정 * void delete(T entity) 엔티티 삭제 * count() 엔티티 총 개수 반환 * Iterable<T> findAll() 모든 엔티티 조회 * */ List<Member> findByName(String name); List<Member> findByNameOrEmail(String name, String email); List<Member> findByAgeLessThan(Integer age); List<Member> findByAgeLessThanOrderByIdDesc(Integer age); @Query("select m from Member m where m.address like %:address% order by m.id desc") List<Member> findByAddress(@Param("address") String address); @Query(value = "select * from Member m where m.address like %:i% order by m.id desc", nativeQuery = true) List<Member> findByAddressByNative(@Param("i") String email); }
테스트 코드 생성
1. 인텔리제이
[스프링부트 (8)] SpringBoot Test(1) - Junit 설정 및 실행
2.이클립스
이클립스(스프링 sts)에서 Spring-boot, Jpa 사용 하기 (2)
MameberApplicationTests
import javax.naming.directory.SearchResult; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.QueryResults; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.shop.constant.ItemSellStatus; import com.shop.entity.QMember; import org.apache.tomcat.util.codec.binary.StringUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.TestPropertySource; import com.shop.constant.MemberStatus; import com.shop.entity.Item; import com.shop.entity.Member; import com.shop.repository.MemberRepository; import java.util.List; @SpringBootTest @TestPropertySource(locations = "classpath:application-test.properties") class MameberApplicationTests { @Autowired MemberRepository memberRepository; @PersistenceContext EntityManager em; @Test @DisplayName("회원생성") public void creatMemberList(){ int a=0; Member saveMember; for (int i = 1; i <=100; i++) { if(a==10) a=0; Member member = new Member(); member.setName("홍길동:"+i); member.setAge(20+a); if(i%2==0) member.setMemberStatus(MemberStatus.GENERAL_MEMBER); else member.setMemberStatus(MemberStatus.FULL_MEMBER); member.setEmail("test"+i+"@gmail.com"); member.setAddress("xxx"+i); memberRepository.save(member); a++; } for (int i = 101; i <=200; i++) { if(a==10) a=0; Member member = new Member(); member.setName("이순신:"+i); member.setAge(20+a); if(i%2==0) member.setMemberStatus(MemberStatus.GENERAL_MEMBER); else member.setMemberStatus(MemberStatus.FULL_MEMBER); member.setEmail("test"+i+"@gmail.com"); member.setAddress("xxx"+i); memberRepository.save(member); a++; } } @Test @DisplayName("Querydsl 회원 조회") public void queryDslTest(){ this.creatMemberList(); JPAQueryFactory queryFactory=new JPAQueryFactory(em); /** 서브 쿼리로 사용할 수 있기에 별칭을 지정해서 생성하길 권장 * 별칭을 사용 X 경우 QMember qMember = QMember.member (기본 인스턴스를 사용) * 별칭을 사용 O 경우 QMember qMember = new QMember("m") (별칭을 생성자에 넘겨줌) */ //QMember qMember=QMember.member; QMember qMember=new QMember("m");// 생성되는 JPQL 의 별칭 m JPAQuery<Member> members = queryFactory.selectFrom(qMember) .where(qMember.memberStatus.eq(MemberStatus.GENERAL_MEMBER)) //%길동% 이고 age >22 이상 .where(qMember.name.like("%"+"길동" +"%").and(qMember.age.gt(22) ) ) .orderBy(qMember.id.desc()); List<Member> memberList=members.fetch(); for(Member member: memberList){ System.out.println(member.toString()); } } @Test @DisplayName("회원 페이징") public void queryDslPagingTest() { this.creatMemberList(); JPAQueryFactory queryFactory=new JPAQueryFactory(em); QMember qMember=QMember.member; Pageable pageable= PageRequest.of(0, 10); long currentPage=pageable.getOffset();; //N 번부터 시작 pageable.getOffset(); int currentLimit=pageable.getPageSize(); //조회 갯수 , 페이지당 출력 갯수 pageable.getPageSize() QueryResults<Member> results=queryFactory.selectFrom(qMember) .where(qMember.age.gt(22) ) .orderBy(qMember.id.desc()) .offset(currentPage) .limit(currentLimit) .fetchResults(); long total = results.getTotal();//전체 데이터 수 long limit = results.getLimit(); long offset = results.getOffset(); List<Member> memberList = results.getResults(); System.out.println("currentPage : " +currentPage); System.out.println("currentLimit : " +currentLimit); System.out.println("total : " +total); System.out.println("limit : " +limit); System.out.println("offset : " +offset); System.out.println("memberList Size : " +memberList.size()); for(Member member: memberList){ System.out.println(member.toString()); } } @Test @DisplayName("querydsl 조회 페이징 ") public void queryDslPageTest(){ this.creatMemberList(); BooleanBuilder booleanBuilder =new BooleanBuilder(); QMember member=QMember.member; String name="이순신"; int age=25; String status="GENERAL_MEMBER"; booleanBuilder.and(member.name.like("%"+name+"%")); booleanBuilder.and(member.age.gt(age)); if(status.equals(MemberStatus.GENERAL_MEMBER)){ booleanBuilder.and(member.memberStatus.eq(MemberStatus.GENERAL_MEMBER)); } Pageable pageable =PageRequest.of(0, 10); Page<Member> membersPageResult= memberRepository.findAll(booleanBuilder, pageable); System.out.println("total element :"+membersPageResult.getTotalElements()); List<Member> membersList=membersPageResult.getContent(); for(Member m :membersList){ System.out.println(m.toString()); } } }
참조:
1.[Spring] pagination, 3분만에 paging 만들기
2. 나만 어려운 검색, 페이징 QueryDSL로 해결하기
4. 스프링부트 쇼핑몰 프로젝트 with JPA
예1)
1.Entty , dto
1) Item
import com.shop.constant.ItemSellStatus; import com.shop.dto.ItemFormDto; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; import javax.persistence.*; import java.time.LocalDateTime; import java.time.LocalDateTime; @Entity @Table(name="item") @Getter @Setter @ToString public class Item extends BaseEntity{ @Id @Column(name="item_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long id; //상품코드 @Column(nullable = false,length = 50) private String itemNm; //상품명 @Column(name="price", nullable = false) private int price; //가격 @Column(nullable = false) private int stockNumber; //재고수량 @Lob @Column(nullable = false) private String itemDetail; //상품 상세 설명 @Enumerated(EnumType.STRING) private ItemSellStatus itemSellStatus; //상품 판매 상태 public void updateItem(ItemFormDto itemFormDto){ this.itemNm=itemFormDto.getItemNm(); this.price=itemFormDto.getPrice(); this.stockNumber=itemFormDto.getStockNumber(); this.itemDetail=itemFormDto.getItemDetail(); this.itemSellStatus=itemFormDto.getItemSellStatus(); } }
2) BaseEntity
import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; @EntityListeners(value = {AuditingEntityListener.class}) @MappedSuperclass @Getter public class BaseEntity extends BaseTimeEntity{ @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedBy private String modifiedBy; }
3)BaseTimeEntity
import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.GeneratedValue; import javax.persistence.MappedSuperclass; import java.time.LocalDateTime; //Auditing을 적용하기 위해 @EntitiyListeners 어노테이션을 추가합니다 @EntityListeners(value = {AuditingEntityListener.class}) //공통 매핑 정보가 필요할 때 사용하는 어노테이션으로 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공합니다. @MappedSuperclass @Getter @Setter public abstract class BaseTimeEntity { //엔티티가 생성되어 저장될 때 시간을 자동으로 저장합니다. @CreatedDate @Column(updatable = false) private LocalDateTime regTime; //엔티티의 값을 변경할 때 시간을 자동으로 저장합니다. @LastModifiedDate private LocalDateTime updateTime; }
2.controller
//value에 상품 관리 화면 진입시 URL 에 페이지 번호가 없는 경우와 페이지번호가 있는 경우 2가지를 매핑합니다. @GetMapping(value = {"admin/items", "/admin/items/{page}"}) public String itemMange(ItemSearchDto itemSearchDto, @PathVariable("page")Optional<Integer> page, Model model){ //페이징을 위해서 PageRequest.of 메소드를 통해 Pageable 객체를 생성합니다. //첫 번째 파라미터로 조회할 페이지 번호, 두 번째 파라미터로 한 번에 가지고 올 데이터 수를 넣어줍니다. //URL 경로에 페이지 번호가 있으면 해당 페이지르 조회하도록 세팅하고, 페이지 번호가 없으면 0페이지를 조회하도록 합니다. Pageable pageable= PageRequest.of(page.isPresent()? page.get() :0,3); //조회 조건과 페이징 정보를 파라미터로 넘겨서 Page<Item> Page<Item> items=itemService.getAdminItemPage(itemSearchDto,pageable); //조회한 상품 데이터 및 페이징 정보를 뷰에 전달합니다. model.addAttribute("items",items); //페이지 전환 시 기존 검색 조건을 유지한 채 이동할 수 있도록 뷰레 다시 전달합니다. model.addAttribute("itemSearchDto",itemSearchDto); //상품 관리 메뉴 하단에 보여줄 페이지 번호의 최대 개수입니다. 5로 설정했으므로 최대 5개의 이동할 페이지번호만 보여줍니다. model.addAttribute("maxPage",5); return "item/itemMng"; }
3. service
@Service @Transactional @RequiredArgsConstructor public class ItemService { private final ItemRepository itemRepository; @Transactional(readOnly = true) public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable){ return itemRepository.getAdminItemPage(itemSearchDto,pageable); } }
4. DAO
1)ItemRepository
import com.shop.entity.Item; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; import java.util.List; public interface ItemRepository extends JpaRepository<Item, Long> , QuerydslPredicateExecutor<Item>, ItemRepositoryCustom { }
2)ItemRepositoryCustom
import com.shop.dto.ItemSearchDto; import com.shop.entity.Item; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface ItemRepositoryCustom { /** * 상품 조회 조건을 담고 있는 itemSearchDto 객체와 페이징 정보를 담고 있는 * pageable 객체를 파라미터로 받는 getAdminItemPage 메소드를 정의합니다. * 반환 데이터로 Page<Item> 객체를 반환합니다. */ Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable); }
3)ItemRepositoryCustomImpl
import com.querydsl.core.QueryResults; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import com.shop.constant.ItemSellStatus; import com.shop.dto.ItemSearchDto; import com.shop.entity.Item; import com.shop.entity.QItem; import jdk.jfr.Frequency; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.thymeleaf.util.StringUtils; import javax.persistence.EntityManager; import java.time.LocalDateTime; import java.util.List; public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{ /** 동적으로 쿼리를 생성하기 위해서 JPAQueryFactory 클래스를 사용합니다. */ private JPAQueryFactory queryFactory; /** JPAQueryFactory 생성자로 EntityManager 객체를 넣어줍니다. */ public ItemRepositoryCustomImpl(EntityManager em){ this.queryFactory =new JPAQueryFactory(em); } /** * 상품 판매 상태 조건이 전체(null) 일 경우는 null 을 리턴합니다. 결과값이 null 이면 where 절에서 해당 조건은 무시됩니다. * 상품 판매 상태 조건이 null 이 아니라 판매중 or 품절 상태라면 해당 조건의 상품만 조회됩니다. * */ private BooleanExpression searchShellStatusEq(ItemSellStatus searchSellStatus){ //상품 판매 상태 // ItemSellStatus : SELL, SOLD_OUT return searchSellStatus==null? null : QItem.item.itemSellStatus.eq(searchSellStatus); } private BooleanExpression regDtsAfter(String searchDataType){ LocalDateTime dateTime=LocalDateTime.now(); if(StringUtils.equals("all", searchDataType) || searchDataType ==null){ return null; }else if(StringUtils.equals("1d",searchDataType)){ dateTime=dateTime.minusDays(1); }else if(StringUtils.equals("1w", searchDataType)){ dateTime=dateTime.minusWeeks(1); }else if(StringUtils.equals("1m", searchDataType)){ dateTime=dateTime.minusMonths(1); }else if(StringUtils.equals("6m",searchDataType)){ dateTime=dateTime.minusMonths(6); } return QItem.item.regTime.after(dateTime); } /** searchBy 의 값에 따라서 상품명에 검색어를 포함하고 있는 상품 또는 상품 생성자의 아이디에 검색어를 포함하고 있는 * 상품을 조회하도록 조건값을 반환 * */ private BooleanExpression searchByLike(String searchBy, String searchQuery){ if(StringUtils.equals("itemNm", searchBy)){ return QItem.item.itemNm.like("%"+searchQuery+"%"); }else if (StringUtils.equals("createdBy", searchBy)){ return QItem.item.createdBy.like("%"+searchQuery+"%"); } return null; } /** * 이제 queryFactory를 이용해서 쿼리를 생성합니다. 쿼리문을 직접 작성할 때의 형태와 문법이 비슷한 것을 * 볼 수 있습니다. * selectFrom(Qitem.item): 상품 데이터를 조회하기 위해서 Qitem 의 item 을 지정합니다. * where 조건절 : BooleanExpression 반환하는 조건문을 넣어줍니다. '.' 단위로 넣어줄 경우 and 조건으로 인식합니다. * offset : 데이터를 가지고 올 시작 인덱스를 지정합니다. * limit : 한 번에 가지고 올 최대 개수를 지정합니다. * fetchResults(): 조회한 리스트 및 전체 개수를 포함하는 QueryResults 를 반환합니다. 상품 데이터 리스트조회및 * 상품 데이터 전체 개수를 조회하는 2번의 쿼리문이 실행됩니다. */ @Override public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) { QueryResults<Item> results =queryFactory.selectFrom(QItem.item) .where( regDtsAfter(itemSearchDto.getSearchDateType()), searchShellStatusEq(itemSearchDto.getSearchSellStatus()), searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()) ) .orderBy(QItem.item.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetchResults(); List<Item> content=results.getResults(); long total=results.getTotal(); //조회한 데이터를 page 클래스의 구현체인 Pageimpl 객체롤 반환합니다. return new PageImpl<>(content, pageable,total); } }
5. view
itemMng.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layouts/layout1}"> <!-- 사용자 CSS 추가 --> <th:block layout:fragment="css"> <style> select{ margin-right:10px; } </style> </th:block> <div layout:fragment="content" > <form th:action="@{'/admin/items/' + ${items.number}}" role="form" method="get" th:object="${items}"> <table class="table"> <thead> <tr> <td>상품아이디</td> <td>상품명</td> <td>상태</td> <td>등록자</td> <td>등록일</td> </tr> </thead> <tbody> <tr> <td colspan="5">[[${items.getContent()}]]</td> </tr> <tr th:each="item, status: ${items.getContent()}"> <td th:text="${item.id}"></td> <td> <a th:href="'/admin/item/'+${item.id}" th:text="${item.itemNm}"></a> </td> <td th:text="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL} ? '판매중' : '품절'"></td> <td th:text="${item.createdBy}"></td> <td th:text="${item.regTime}"></td> </tr> </tbody> </table> <!-- 1.th:with 는 변수값을 정의할 때 사용합니다. 페이지 시작 번호(strt) 와 페이지 번호(end)를 구해서 저장. 시작 페이지와 끝과 페이지 번호를 구하는 방법이 조금 복잡해 보이는데 정리하면 다음과 같다. start=(현재 페이지번호 /보여줄 페이지수)+1 end =strat +(보여줄 페이지수 -1) --> <div th:with="start=${(items.number/maxPage)*maxPage + 1}, end=(${(items.totalPages == 0) ? 1 : (start + (maxPage - 1) < items.totalPages ? start + (maxPage - 1) : items.totalPages)})" > <ul class="pagination justify-content-center"> <!--2.첫번째 페이지면 이전 페이지로 이동하는 <이전> 버튼을 선택 불가능하도록 disabled 클래스를 추가.--> <li class="page-item" th:classappend="${items.first}?'disabled'"> <!--3.<이전> 버튼 클릭 시 현재 페이지에서 이전 페이지로 이동하도록 page 함수를 호출.--> <a th:onclick="'javascript:page(' + ${items.number - 1} + ')'" aria-label='Previous' class="page-link"> <span aria-hidden='true'>Previous</span> </a> </li> <!--4.현재 페이지면 active 클래스를 추가.--> <li class="page-item" th:each="page: ${#numbers.sequence(start, end)}" th:classappend="${items.number eq page-1}?'active':''"> <!--5.페이지 번호 클릭시 해당 페이지로 이동하도록 page 함수를 호출.--> <a th:onclick="'javascript:page(' + ${page - 1} + ')'" th:inline="text" class="page-link">[[${page}]]</a> </li> <!--6.마지막 페이지일 경우 다음 페이지로 이동하는 Next 버튼을 선택 불가능하도록 disabled 클래스를 추가.--> <li class="page-item" th:classappend="${items.last}?'disabled'"> <!--7.Next 버튼 클릭시 현재 페이지에서 다음 페이지로 이동하도록 page 함수를 호출.--> <a th:onclick="'javascript:page(' + ${items.number + 1} + ')'" aria-label='Next' class="page-link"> <span aria-hidden='true'>Next</span> </a> </li> </ul> </div> <div class="form-inline justify-content-center" th:object="${itemSearchDto}"> <select th:field="*{searchDateType}" class="form-control" style="width:auto;"> <option value="all">전체기간</option> <option value="1d">1일</option> <option value="1w">1주</option> <option value="1m">1개월</option> <option value="6m">6개월</option> </select> <select th:field="*{searchSellStatus}" class="form-control" style="width:auto;"> <option value="">판매상태(전체)</option> <option value="SELL">판매</option> <option value="SOLD_OUT">품절</option> </select> <select th:field="*{searchBy}" class="form-control" style="width:auto;"> <option value="itemNm">상품명</option> <option value="createdBy">등록자</option> </select> <input th:field="*{searchQuery}" type="text" class="form-control" placeholder="검색어를 입력해주세요"> <button id="searchBtn" type="submit" class="btn btn-primary">검색</button> </div> </form> </div> <!-- 사용자 스크립트 추가 --> <th:block layout:fragment="script"> <script th:inline="javascript"> $(document).ready(function(){ $("#searchBtn").on("click",function(e) { e.preventDefault(); page(0); }); }); function page(page){ var searchDateType = $("#searchDateType").val(); var searchSellStatus = $("#searchSellStatus").val(); var searchBy = $("#searchBy").val(); var searchQuery = $("#searchQuery").val(); location.href="/admin/items/" + page + "?searchDateType=" + searchDateType + "&searchSellStatus=" + searchSellStatus + "&searchBy=" + searchBy + "&searchQuery=" + searchQuery; } </script> </th:block> </html>
예2)
1.Entty , dto
ItemSearchDto
import com.shop.constant.ItemSellStatus; import lombok.Getter; import lombok.Setter; @Getter @Setter public class ItemSearchDto { /** * 현재 시간과 상품 등록일을 비교해서 상품 데이터를 조회합니다. 조회 시간 기준은 아래와 같습니다. * all : 상품 등록일 전체 * 1d: 최근 하루 동안 등록된 상품 * 1w : 최근 일주일 동안 등록된 상품 * 1m : 최근 한달 동안 등록된 상품 * 6m : 최근 6개월 동안 등록된 상품 */ private String searchDateType; /** * 상품의 판매상태를 기준으로 상품 데이터를 조회합니다. */ private ItemSellStatus searchSellStatus; /** * 상품을 조회할 때 어떤 유형으로 조회할지 선택합니다. * itemNm: 상품명 * createBy: 상품 등록자 아이디 */ private String searchBy; /** * 조회할 검색어 저장할 변수입니다. * searchBy가 itemNm 일 경우 상품명을 기준으로 검색하고, * cretaeBy일 경우 상품 등록자 아이디 기준으로 검색합니다. */ private String searchQuery=""; }
Item
@Entity @Table(name="item") @Getter @Setter @ToString public class Item extends BaseEntity{ @Id @Column(name="item_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long id; //상품코드 @Column(nullable = false,length = 50) private String itemNm; //상품명 @Column(name="price", nullable = false) private int price; //가격 @Column(nullable = false) private int stockNumber; //재고수량 @Lob @Column(nullable = false) private String itemDetail; //상품 상세 설명 @Enumerated(EnumType.STRING) private ItemSellStatus itemSellStatus; //상품 판매 상태 public void updateItem(ItemFormDto itemFormDto){ this.itemNm=itemFormDto.getItemNm(); this.price=itemFormDto.getPrice(); this.stockNumber=itemFormDto.getStockNumber(); this.itemDetail=itemFormDto.getItemDetail(); this.itemSellStatus=itemFormDto.getItemSellStatus(); } }
ItemImg
@Entity @Table(name = "item_img") @Getter @Setter public class ItemImg extends BaseEntity{ @Id @Column(name = "item_img_id") @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String imgName; // 이미지 파일명 private String oriImgName; //원본 이미지 파일명 private String imgUrl; //이미지 조회 경로 private String repimgYn; // 대표 이미지 여부 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "item_id") private Item item; }
MainItemDto
@QueryProjection 사용하면 Entity 객체로 값을 받은 후 DTO 클래스로 변환하는 과정 없이 바로 DTO 객체를 뽑아낼수 있다.
import com.querydsl.core.annotations.QueryProjection; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class MainItemDto { private Long id; private String itemNm; private String itemDetail; private String imgUrl; private Integer price; @QueryProjection public MainItemDto(Long id, String itemNm, String itemDetail, String imgUrl, Integer price){ this.id=id; this.itemNm=itemNm; this.itemDetail=itemDetail; this.imgUrl=imgUrl; this.price=price; } }
2.Controller
MainController
@Controller @RequiredArgsConstructor public class MainController { private final ItemService itemService; @GetMapping(value = "/") public String main(ItemSearchDto itemSearchDto, Optional<Integer> page, Model model){ Pageable pageable= PageRequest.of(page.isPresent()? page.get() : 0, 6); Page<MainItemDto> items=itemService.getMainItemPage(itemSearchDto,pageable); model.addAttribute("items",items); model.addAttribute("itemSearchDto", itemSearchDto); model.addAttribute("maxPage", 5); return "main"; } }
3.Service
@Service @Transactional @RequiredArgsConstructor public class ItemService { @Transactional(readOnly = true) public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable){ return itemRepository.getMainItemPage(itemSearchDto, pageable); } }
4.Repository
ItemRepositoryCustom
import com.shop.dto.ItemSearchDto; import com.shop.dto.MainItemDto; import com.shop.entity.Item; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface ItemRepositoryCustom { Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable); }
ItemRepositoryCustomImpl
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{ private BooleanExpression itemNmLike(String searchQeury){ //검색어가 null 이 아니면 상품명에 해당 검색어가 포함되는 상품을 조회하는 조건을 반환 return StringUtils.isEmpty(searchQeury) ? null :QItem.item.itemNm.like("%" +searchQeury +"%"); } @Override public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) { QItem item =QItem.item; QItemImg itemImg=QItemImg.itemImg; //QMainItemDto 의 생성자에 반환할 값들을 넣어줍니다. //@QueryProjection 을 사용하면 DTO 로 바로 조회가 가능합니다. //엔티티 조회 후 DTO 로 변환하는 과정을 줄일 수 있습니다. QueryResults<MainItemDto> results=queryFactory .select( new QMainItemDto( item.id, item.itemNm, item.itemDetail, itemImg.imgUrl, item.price ) ) .from(itemImg) .join(itemImg.item, item) //itemImg와 item 을 내부 조인합니다. .where(itemImg.repimgYn.eq("Y")) //상품 이미지의 경우 대표 상품 이미지만 불러옵니다. .where(itemNmLike(itemSearchDto.getSearchQuery())) .orderBy(item.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetchResults(); List<MainItemDto> content=results.getResults(); long total=results.getTotal(); return new PageImpl<>(content, pageable, total); } }
4.View
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout=http://www.ultraq.net.nz/thymeleaf/layout layout:decorate="~{layouts/layout1}"> <!-- 사용자 CSS 추가 --> <th:block layout:fragment="css"> <style> .carousel-inner > .item { height: 350px; } .margin{ margin-bottom:30px; } .banner{ height: 300px; position: absolute; top:0; left: 0; width: 100%; height: 100%; } .card-text{ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } a:hover{ text-decoration:none; } .center{ text-align:center; } </style> </th:block> <div layout:fragment="content"> <div id="carouselControls" class="carousel slide margin" data-ride="carousel"> <div class="carousel-inner"> <div class="carousel-item active item"> <img class="d-block w-100 banner" src="https://user-images.githubusercontent.com/13268420/112147492-1ab76200-8c20-11eb-8649-3d2f282c3c02.png" alt="First slide"> </div> </div> </div> <input type="text" name="searchQuery" th:value="${itemSearchDto.searchQuery}"> [[${items.getContent()}]] <div class="row"> <th:block th:each="item , status: ${items.getContent()}"> <div class="col-md-4 margin"> <div class="card"> <a th:href="'/item/'+${item.id}" class="text-dark"> <img th:src="${item.imgUrl}" class="card-img-top" th:alt="${item.itemNm}" height="400"> <div class="card-body"> <h4 class="card-title">[[${item.itemNm}]]</h4> <p class="card-text">[[${item.itemDetail}]]</p> <h3 class="card-title text-danger">[[${item.price}]]원</h3> </div> </a> </div> </div> </th:block> </div> <!--페이징--> <!--th:with 는 변수값을 정의할 때 사용--> [[${items}]] <div th:with="start=${(items.number/maxPage)*maxPage + 1}, end=(${(items.totalPages == 0) ? 1 : (start + (maxPage - 1) < items.totalPages ? start + (maxPage - 1) : items.totalPages)})" > <ul class="pagination justify-content-center"> <li class="page-item" th:classappend="${items.number eq 0}? 'disabled':''"> <a th:href="@{'/' + '?searchQuery'+ ${itemSearchDto.searchQuery} +'$page=' +${items.number-1}}" aria-label='Previous' class="page-link"> <span aria-hidden="true"><</span> </a> </li> <li class="page-item" th:each="page, status:${#numbers.sequence(start,end)}" th:classappend="${items.number eq page-1}? 'active':''" > <a th:href="@{'/' +'?searchQuery='+${itemSearchDto.searchQuery} + '&page=' +${page-1}}" th:inline="text" class="page-link">[[${page}]]</a> </li> <li class="page-item" th:classappend="${items.number+1 ge items.totalPages}? 'disabled':''"> <a th:href="@{'/' + '?searchQuery='+ ${itemSearchDto.searchQuery} +'&page='+${items.number+1} }" aria-label="Next" class="page-link"> <span aria-hidden='true'>></span> </a> </li> </ul> </div> </html>
댓글 ( 4)
댓글 남기기