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)
댓글 남기기