스프링부트 쇼핑몰 프로젝트 | 6 등록, 수정, QueryDSL 로 조건따라 조회

Yunny.Log ·2022년 5월 23일
0

Spring Boot

목록 보기
51/80
post-thumbnail

등록

  • 지연 로딩을 설정해 매핑된 엔티티 정보가 필요한 경우, 데이터를 조회
  • 상품을 등록할 때는 전달받은 dto 객체를 엔티티 객체로 변환하는 작업을 해야하고, 조회 시에는 객체를 dto 객체로 바꿔주는 작업 수행 필요

업로드할 파일 경로 설정

  • 업로드할 파일을 읽어올 경로를 설정해오는 과정 필요
  • WebMvcConfigurer 인터페이스를 구현하는 WebMvcConfig 파일 작성이 필요
    => addHandlers 메소드를 통해 내 로컬 컴퓨터에 업로드할 파일을 찾을 위치 설정

WebConfig.java


@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Value("${upload.image.location}")
    private String location;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/image/**")
                // url에 /image/ 경로로 접근을 하게 된다면 
                // uploadPath에 설정한 폴더 기준으로 파일을 읽어오게 한다
                .addResourceLocations("file:" + location)
                // FileSystem의 location 경로에서 파일 접근
               //로컬 컴퓨터에 저장된 파일을 읽어올 root 경로를 설정합니다. 
               .setCacheControl(CacheControl.maxAge(Duration.ofHours(1L)).cachePublic())
                // 업로드된 각 이미지는 고유 이름 가지며 & 수정 X 예정 따라서 => 1시간의 캐시 설정
                // 자원 접근 시 새로 접근 x , 캐시 자원 이용 (1시간 유효)


    }

FileService (interface)

/**
 * 파일 업로드 & 삭제 수행 인터페이스
 */

// 실제 파일 업로드, 삭제 로직 작동시킬 구현체 필요
@Primary //얘를 service 단에서 주입시키면 (required args) ,  구현체들 너무 많다고 어떤 구현체 주입시킬지
//모른다고 에러가 난다 따라서 얘를 primary로
public interface FileService {

    void upload(MultipartFile file, String filename);
    void delete(String filename);

}

LocalFileService (class)


@Service
@Slf4j
public class LocalFileService implements FileService {

    /**
     * 파일 업로드 위치
     */
     //파일 업로드할 위치를 지정 
    @Value("${upload.image.location}")
    private String location;

    /**
     * 파일 업로드 디렉토리 생성
     */
    @PostConstruct
    void postConstruct() {

        File dir = new File(location);
        if (!dir.exists()) {
            dir.mkdir();
        }
    }

    /**
     * MultipartFile 을 실제 파일 지정 위치에 저장
     * @param file
     * @param filename
     */
    @Override
    public void upload(MultipartFile file, String filename) {
    
        try {
        	///////여기서부터
            LocalDateTime now = LocalDateTime.now();
            //날짜별로 파일을 나누기 위해 시간 받아오기
            //now로 디렉토리를 생성하고, 거기에다 파일 저장할 것
            String targetDir = Path.of(
                    location,
                    now.format(DateTimeFormatter.ISO_DATE)
            ).toString();
            //현재 시간을 어떤 형식으로 나타내줄지 정하는 것

            File dirNow = new File(targetDir);
            if(!dirNow.exists()) dirNow.mkdir();//디렉토리 없으면 만들기
            //////여기까지는 단순히 파일이름 지정 과정 (현재 날짜, 시간) + 파일 이름

 			// MultipartFile을 실제 파일로 지정된 위치에 저장
            file.transferTo(Path.of(
                    targetDir,
                    filename
            ));

        } catch(IOException e) {
            throw new FileUploadFailureException(e);
        }

    }

//파일을 삭제하는 것 => 해당 로케이션의 파일이름의 이미지 파일을 삭제
    @Override
    public void delete(String filename) {
        new File(location+ filename).delete();
    }



}

=> 요약

 			// MultipartFile을 실제 파일로 지정된 위치에 저장
            file.transferTo(Path.of(
                    targetDir,
                    filename
            ));
  • 아이템 서비스에서 받아온 멀티파일을 적절한 디렉토리 + 유일한 값 파일네임으로 저장해주는 것

(+) application.yml에 정의한 파일 업로드 로케이션

upload:
     location: src/main/prodmedia/image/
  1. url에 /image/ 접두 경로가 설정되어있으면,

  2. 파일 시스템의 location 경로에서 파일에 접근합니다.

ItemService.java - 아이템에 이미지를 첨부해주는 과정

    @Transactional
    public ItemCreateResponse create(ItemCreateRequest req) {

        Item item = itemRepository.save(
                ItemCreateRequest.toEntity(
                        req,
                        memberRepository,
                        colorRepository,
                        materialRepository,
                        manufactureRepository
                )

        );

        uploadImages(item.getThumbnail(), req.getThumbnail());
        if (!(req.getTag().size() == 0)) {//TODO : 나중에 함수로 빼기 (Attachment 유무 판단)
            //attachment가 존재할 땜나
            uploadAttachments(item.getAttachments(), req.getAttachments());
        }

        return new ItemCreateResponse(item.getId());
    }

    /**
     * 썸네일 존재 시에 File Upload로 이미지 업로드
     *
     * @param images
     * @param fileImages
     */

    private void uploadImages(List<Image> images, List<MultipartFile> fileImages) {
        // 실제 이미지 파일을 가지고 있는 Multipart 파일을
        // 이미지가 가지는 uniquename을 파일명으로 해서 파일저장소 업로드
        IntStream.range(0, images.size())
                .forEach(
                        i -> fileService.upload
                                (
                                        fileImages.get(i),
                                        images.get(i).getUniqueName()
                                )
                );
    }

=> 이미지를 생성해주는 메소드 : uploadImages 메소드

    private void uploadImages(List<Image> images, List<MultipartFile> fileImages) {
        // 실제 이미지 파일을 가지고 있는 Multipart 파일을
        // 이미지가 가지는 uniquename을 파일명으로 해서 파일저장소 업로드
        IntStream.range(0, images.size())
                .forEach(
                        i -> fileService.upload
                                (
                                        fileImages.get(i),
                                        images.get(i).getUniqueName()
                                )
                );
    }

&& 그럼 DB에는 뭐가 저장되느냐?
=> DB에는

=> 이것은 ItemCreateRequest가 들어와서 얘를 entity로 바꿔주는 과정에서!

DB에 저장되는 것은 파일의 이름, 주소, 등이 된다.

또한 아이템과 이 썸네일(이미지) 관계는

하나의 아이템에 많은 이미지가 들어갈 수 있는 형태입니다!
그리고 관계가 CASCADE 라서 아이템이 저장될 때 Image가 생성이 되기 때문에 dto를 entity로 바꿔주는 과정에서 자연스레 생성이 되게 됩니다~!

수정

ItemService.java


    @Transactional
    public ItemUpdateResponse update(Long id, ItemUpdateRequest req) {

        Item item = itemRepository.findById(id).orElseThrow(ItemNotFoundException::new);

        if (item.getTempsave() == false) { //true면 임시저장 상태, false면 찐 저장 상태
            //찐 저장 상태라면 UPDATE 불가, 임시저장 일때만 가능
            throw new ItemUpdateImpossibleException();
        }

        Item.FileUpdatedResult result = item.update(
                req,
                colorRepository,
                memberRepository,
                materialRepository,
                manufactureRepository,
                itemManufactureRepository,
                itemMaterialRepository
                );

        uploadImages(
                result.getImageUpdatedResult().getAddedImages(),
                result.getImageUpdatedResult().getAddedImageFiles()
        );
        deleteImages(
                result.getImageUpdatedResult().getDeletedImages()
        );

        uploadAttachments(
                result.getAttachmentUpdatedResult().getAddedAttachments(),
                result.getAttachmentUpdatedResult().getAddedAttachmentFiles()
        );
        deleteAttachments(
                result.getAttachmentUpdatedResult().getDeletedAttachments()
        );

        return new ItemUpdateResponse(id);
    }
  • @Transactional(readOnly=true)로 설정하게 된다면 JPA가 더티체킹(변경감지)를 수행하지 않아 성능향상이 가능하다고 한다!

Item.java

  • Item entity안에서 update 로직을 구현, 즉 엔티티 클래스에 비즈니스 로직을 추가하게 된다면 조금 더 객체지향적인 코딩 가능 & 코드 재활용 가능!

    /**
     * postupdaterequest 받아서 update 수행
     *
     * @param req
     * @return 새로 수정된 이미지
     */
    public FileUpdatedResult update(
            ItemUpdateRequest req,
            ColorRepository colorRepository,
            MemberRepository memberRepository,
            MaterialRepository materialRepository,
            ManufactureRepository manufactureRepository,
            ItemManufactureRepository itemManufactureRepository,
            ItemMaterialRepository itemMaterialRepository
    ) {

        //isBlank 랑 isNull로 판단해서 기존 값 / req 값 채워넣기
        this.name = req.getName();
        this.type = req.getType();
        this.width = req.getWidth();
        this.height = req.getHeight();
        this.weight = req.getWeight();

        ImageUpdatedResult resultImage =
                findImageUpdatedResult(
                        req.getAddedImages(),
                        req.getDeletedImages()
                );

        addImages(resultImage.getAddedImages());
        deleteImages(resultImage.getDeletedImages());

        AttachmentUpdatedResult resultAttachment =

                findAttachmentUpdatedResult(
                        req.getAddedAttachments(),
                        req.getDeletedAttachments()
                );
        addUpdatedAttachments(req, resultAttachment.getAddedAttachments());

        deleteAttachments(resultAttachment.getDeletedAttachments());

        FileUpdatedResult fileUpdatedResult = new FileUpdatedResult(resultAttachment,resultImage);

        this.modifier =
                memberRepository.findById(
                        req.getModifierId()
                ).orElseThrow(MemberNotFoundException::new);//05 -22 생성자 추가



        return fileUpdatedResult;
        
    }
  • 업데이트 시에는 Transactional 에 readonly를 주지 않음 => 변경감지 옵션을 가능하게 해주기

  • 이미지 추가, 삭제 구현
    1) 추가된 이미지
    => Item 의 update 메소드 중에서 아래 수행

        ImageUpdatedResult resultImage =
                findImageUpdatedResult(
                        req.getAddedImages(),
                        req.getDeletedImages()
                );

        addImages(resultImage.getAddedImages());
    /**
     * 추가할 이미지
     *
     * @param added
     */
    private void addImages(List<Image> added) {
        added.stream().forEach(i -> {
            thumbnail.add(i);
            i.initItem(this);
        });
    }
  • 인스턴스 변수 thumbnail에 Image 리스트에 속한 image 를 추가하고, 해당 Image에 this(Item)를 등록해줍니다.
  • cascade 옵션을 PERSIST로 설정해두었기 때문에, Item이 저장되면서 Image도 자동적으로 함께! 저장해줍니다.

    => thumbnail.add(i); 에서의 thumbnail이 여기 인스턴스의 썸네일을 가리킵니다.

2) 삭제된 이미지

  • orphanremoval = true 속성을 넣어줘서, 아이템과의 연관관계를 아래와 같이 remove 해준다면 관계가 끊어지니깐 저장됐던 객체도 사라지게 된다! (#100번째 이슈 관련)
    /**
     * 삭제될 이미지 제거 (고아 객체 이미지 제거)
     *
     * @param deleted
     */
    private void deleteImages(List<Image> deleted) {
        deleted.stream().
                forEach(di ->
                        this.thumbnail.remove(di)
                );
    }

QueryDSL을 이용한 조회 기능

QueryDsl의 사용 이유와 장점

  • 조회 조건이 복잡한 화면은 querydsl 을 이용해서 조건에 맞는 쿼리를 동적으로 쉽게 생성하는 것이 가능
  • 비슷한 쿼리 재활용 가능
  • 자바 코드로 작성하기 때문에 IDE의 도움을 받아서 문법 오류를 바로 수정 가능

1) 상품 조회 조건 DTO 설정

ItemProjectCreateReadCondition.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ItemProjectCreateReadCondition {

    @NotNull(message = "페이지 번호를 입력해주세요.")
    @PositiveOrZero(message = "올바른 페이지 번호를 입력해주세요. (0 이상)")
    private Integer page;

    @NotNull(message = "페이지 크기를 입력해주세요.")
    @Positive(message = "올바른 페이지 크기를 입력해주세요. (1 이상)")
    private Integer size;

}

}

QueryDsl와 JPA 함께 사용하기 위해서는 사용자 정의 레포지토리를 정의해야 한다

1) 사용자 정의 인터페이스 작성

/**
 * 쿼리를 구현하는 메소드
 * 검색 조건에 대한 정보가 담긴
 * ItemReadCondition 전달받음
 * 이를 Page로 반환하여
 * 페이징 결과에 대한 각종 정보 확인
 */
public interface CustomItemRepository {
    Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond);

}

2) 사용자 정의 인터페이스 구현


/**
 * CustomItemRepository의 구현체
 */
@Transactional(readOnly = true) 
public class CustomItemRepositoryImpl extends QuerydslRepositorySupport implements CustomItemRepository { 

    private final JPAQueryFactory jpaQueryFactory; 


    public CustomItemRepositoryImpl(JPAQueryFactory jpaQueryFactory) { 
        super(Item.class);

        this.jpaQueryFactory = jpaQueryFactory;

    }

    /**
     *  전달받은 ItemReadCondition(검색)으로
     *  Predicate와 PageRequest를 생성 &
     *  조회 쿼리와 카운트 쿼리를 수행한 결과를 Page의 구현체로 반환
     * @param cond
     * @return Page
     */
    @Override
    public Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond) { 
        Pageable pageable = PageRequest.of(cond.getPage(), cond.getSize());
        Predicate predicate = createPredicate(cond);
        System.out.println(fetchAll(predicate, pageable).toString());
        return new PageImpl<>(fetchAll(predicate, pageable), pageable, fetchCount(predicate));
    }


    /**
     * 아이템 목록을 ItemSimpleDto로 조회한 결과 반환환
     * @param predicate
     * @param pageable
     * @return getQuerydsl().applyPagination (페이징 적용 쿼리)
     */

    private List<ItemProjectCreateDto> fetchAll(Predicate predicate, Pageable pageable) {


        List<ItemProjectCreateDto> itemProjectCreateDtos = getQuerydsl().applyPagination(
                pageable,
                jpaQueryFactory
                        .select(constructor
                                (
                                        ItemProjectCreateDto.class,
                                        item.id,

                                        item.name,
                                        item.type,
                                        item.itemNumber,

                                        item.revision,
                                        routeOrdering.lifecycleStatus

                                        )

                        )
                        .from(item)
                        //.join(itemMaterial).on(item.id.eq(itemMaterial.item.id))

                        //.join(itemMaterial).on(item.id.eq(itemMaterial.item.id))
                        //jqpl은 연관관계 없으면 직접 못하고 join on으로 해줘야 함
                        .join(routeOrdering).on(item.id.eq(routeOrdering.item.id))

                        .join(item.member) //아이템 작성자 닉네임 조회 위해 Member와 조인

                        .where(predicate)
                        .orderBy(item.id.desc())
        ).fetch(); //리스트 반환

        return itemProjectCreateDtos;
    }

    private Long fetchCount(Predicate predicate) { // 7
        return jpaQueryFactory.select(
                        item.count()
                ).from(item).
                where(predicate).fetchOne();
    }

    private Predicate createPredicate(ItemProjectCreateReadCondition cond) { // 8
        return new BooleanBuilder();
    }


    private <T> Predicate orConditions(List<T> values, Function<T, BooleanExpression> term) { // 11
        return values.stream()
                .map(term)
                .reduce(BooleanExpression::or)
                .orElse(null);
    }
}

2) 세부 탐구

2-1 ) CustomItemRepositoryImpl 구현체는 본래 인터페이스 implements

public class CustomItemRepositoryImpl extends QuerydslRepositorySupport implements CustomItemRepository { 

2-2 ) 동적으로 쿼리 생성 위한 JPAQueryFactory 사용

private final JPAQueryFactory jpaQueryFactory; 

2-3 ) JPAQueryFactory 생성자로 객체 생성

    public CustomItemRepositoryImpl(JPAQueryFactory jpaQueryFactory) { // 4
        super(Item.class);
        this.jpaQueryFactory = jpaQueryFactory;

    }

2-4 ) 조건에 맞는 아이를 queryFactory로 쿼리 생성

  • where 조건에 맞는 아이들을 데려오도록 Boolean 결과값을 주는 조건문 넣어주기
    => 내가 지정한 DTO 리스트를 반환
private List<ItemProjectCreateDto> fetchAll(Predicate predicate, Pageable pageable) {


        List<ItemProjectCreateDto> itemProjectCreateDtos = getQuerydsl().applyPagination(
                pageable,
                jpaQueryFactory
                        .select(constructor
                                (
                                        ItemProjectCreateDto.class,
                                        item.id,

                                        item.name,
                                        item.type,
                                        item.itemNumber,

                                        item.revision,
                                        routeOrdering.lifecycleStatus

                                        )

                        )
                        .from(item)
//jqpl은 연관관계 없으면 직접 못하고 join on으로 해줘야 함                           .join(routeOrdering).on(item.id.eq(routeOrdering.item.id))

                        .join(item.member) //아이템 작성자 닉네임 조회 위해 Member와 조인

                        .where(predicate)
                        .orderBy(item.id.desc())
        ).fetch(); //리스트 반환

        return itemProjectCreateDtos;
    }

2-5 ) FETCHALL 을 통해 조회한 데이터는 Page 클래스의 구현체인 PageImpl 객체로 반환

    /**
     *  전달받은 ItemReadCondition(검색)으로
     *  Predicate와 PageRequest를 생성 &
     *  조회 쿼리와 카운트 쿼리를 수행한 결과를 Page의 구현체로 반환
     * @param cond
     * @return Page
     */
    @Override
    public Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond) {
        Pageable pageable = PageRequest.of(cond.getPage(), cond.getSize());
        Predicate predicate = createPredicate(cond);
        System.out.println(fetchAll(predicate, pageable).toString());
        return new PageImpl<>(fetchAll(predicate, pageable), pageable, fetchCount(predicate));
    }

3) Spring Data JPA 레포지토리에서 사용자 정의 인터페이스 상속

public interface ItemRepository extends JpaRepository<Item, Long>,  CustomItemRepository {

=> 이렇게 되면 이제 itemRepo 에서 public Page<ItemProjectCreateDto> findAllByCondition(ItemProjectCreateReadCondition cond) { 를 사용하는 것이 가능해지지

service

    public ItemProjectCreateDtoList readItemCandidatesAll(ItemProjectCreateReadCondition cond) {
        return ItemProjectCreateDtoList.toDto(
                itemRepository.findAllByCondition(cond)
        );
    }
  • 서비스에서 인터페이스 정의해두고, 레포지토리가 implements 받았던 메소드를 수행하면 된다.

배운 점

  • 데이터 수정이 이뤄지지 않는다면 @Transactional(readonly=Tre) 속성을 지저애주는 것이 효율적

0개의 댓글