스프링

 

 

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로 해결하기

3. [JPA] 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">&lt;</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'>&gt;</span>
            </a>
         </li>

      </ul>
   </div>






</html>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

대인(大人), 즉 도(道)를 닦은 훌륭한 자는 자기라는 것을 생각하지 않는다. -장자

댓글 ( 4)

댓글 남기기

작성