JPA 간단 CR 기능 만들기

jinvicky·2023년 12월 20일
0
post-thumbnail

커미션 블로그에서 내가 그린 그림들을 한눈에 보여줄 페이지를 만들 것이다. 또한 커미션 타입별로 탭으로 나누어 보여줄 생각이다.

UI를 크게 벌일 생각이 없어서 아래 좋아하는 작가 사이트처럼 간단히 하자. 상세 페이지도 없고 제목, 이미지, 링크만 있다.
참고 https://uekuraeku.com/

BACK-END


1. 테이블 설계

2. endpoint 추가

개발 환경 : Windows + Spring boot + Gradle

전체 프로젝트 구조

Post~ 관련 클래스들에 포커스를 두자.

1. JPA Entity, Vo 만들기

import com.cms.world.utils.GlobalCode;
import com.cms.world.utils.StringUtil;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@Entity
@Getter
@Setter
@Table(name = "post")
public class PostDto {

    public PostDto() {

    }

    @Builder
    public PostDto(String TITLE, String CONTENT, String IMG_URL, String TP_CD) {
        this.TITLE = TITLE;
        this.CONTENT = CONTENT;
        this.IMG_URL = IMG_URL;
        this.TP_CD = TP_CD;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long ID;

    @Column
    private String TITLE;

    @Column
    private String CONTENT;

    @Column
    private String IMG_URL;

    @Column
    private String TP_CD; // 커미션 타입 코드

    @Column
    private String RGTR_DT;

    @PrePersist
    public void onPrePersist() { // 디비에 넣기 전에 현재 시각 날짜를 format해서 insert
        this.RGTR_DT = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
        if (StringUtil.isEmpty(this.TP_CD)) this.TP_CD = GlobalCode.TYPE_SINGLE.getCode(); // 타입 코드가 없으면 1인 기본 설정
    }
}

//참고로 StringUtil.isEmpty()는 커스텀이다. 
//StringUtil.java 

 public static boolean isEmpty(String str) {
        return (str == null || str.isEmpty());
 }

기본 문법

나도 JPA가 익숙지 않아서 insert, findall만 하는데 시간이 오래 걸렸다;;

간단히 문법을 설명하자면
@Table : 디비 테이블이 될 객체이다.
@Entity : JPA의 관리 대상이다.
@Getter, @Setter : 이거 없으면 쿼리만 돌아가고 findAll이 안된다. (사실 당연함)
@Column(name ="id") : 테이블 컬럼 (name 지정 가능)

@Id, @GeneratedValue(strategy = GenerationType.IDENTITY) : pk 컬럼에 추가한다. @GeneratedValue는 DB의 auto_increment다.

<기타>

  • @Data를 지양하기 때문에 별도로 get, set 으로 추가한 것이다.
  • @AllArgsConstructor를 지양하려고 @Builder를 추가했다.

Table insert전 작업

@PrePersist를 사용하면 여러 작업을 할 수 있다.
1. 현재 시간을 vo때부터 설정하지 않고 테이블 insert 전에 format해서 넣는다.
2. 타입 코드 등이 null일 때 기본값을 설정할 수 있다.

  @PrePersist
    public void onPrePersist() { // 디비에 넣기 전에 현재 시각 날짜를 format해서 insert
        this.RGTR_DT = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
        if (StringUtil.isEmpty(this.TP_CD)) this.TP_CD = GlobalCode.TYPE_SINGLE.getCode(); // 타입 코드가 없으면 1인 기본 설정
    }

타입 코드는 아래와 같이 정리한다.
GlobalCode.java

import lombok.Getter;

@Getter
@AllArgsConstructor
public enum GlobalCode { //타입 등의 코드

    TYPE_SINGLE("TY01", "1인 기본"),
    TYPE_SINGLE_BG("TY02", "1인 배경"),
    TYPE_COUP("TY03", "2인 기본"),
    TYPE_COUP_BG("TY04", "2인 배경"),
    TYPE_MULTI_BG("TY05", "3인 이상 배경");

    private final String code;
    private final String desc;
}

VO와 DTO

개발 중에 insert할 때의 이미지 타입과 select할 때의 이미지 타입이 달라서 entity 하나로 안되더라.
구조도가 대략 아래 같다.

그래서 client -> server로 가는 쪽의 폼은 vo로, 서버단에서 가져오거나 디비랑 연동되는 쪽은 dto로 각각 추가했다.

vo와 dto가 뭐가 다른가에 대해서는 다루지 않겠다;;

PostVo.java


import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;

@Getter
@Setter
@ToString
public class PostVo {

    private Long id;
    private String title;
    private String content;
    private String type; //커미션 타입
    private MultipartFile img; //dto와 다른 부분.
    private String reg_date;

    @Builder
    public PostVo (String title, String content, MultipartFile img, String type,String reg_date) {
        this.title = title;
        this.content = content;
        this.img = img;
        this.type = type;
        this.reg_date = reg_date;
    }
}

Q. Table insert가 안될 때

나의 경우 원인이 아주~ 많았다.
1. 테이블, 컬럼명 대소문자 차이
2. @Getter, @Setter 부재
3. import 잘못해서 꼬임.

1. 테이블, 컬럼명 대소문자 차이


왜 컬럼명만 소문자인 것일까....

참고 https://velog.io/@gillog/JPA-Spring-Boot-JPA-Entity-Table-대-소문자-구분-못하는-경우-해결

application.yml에 가서

... 생략
  jpa:
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl # 테이블 컬럼명 대소문자 구분 못할 때 추가
      ddl-auto: update
    show-sql: true

physical-strategy 부분을 추가해 준다. 또한 show-sql을 쓰면 쿼리를 로그로 볼 수 있어서 추가했다.

따란

2. repository, service, controller 만들기

PostRepository.java

import com.cms.world.domain.dto.PostDto;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<PostDto, Long> {

}

<객체, pk의 데이터 타입> 식으로 들어간다. JpaRepository 또는 CRUDRepository를 상속받을 수 있다.

PostService.java

import com.cms.world.domain.dto.PostDto;
import com.cms.world.domain.vo.PostVo;
import com.cms.world.repository.PostRepository;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;

@Service
public class PostService {

    private PostRepository repository;

    private S3UploadService s3UploadService;

    public PostService (PostRepository postRepository, S3UploadService s3UploadService) {
        this.repository = postRepository;
        this.s3UploadService = s3UploadService;
    }

    public void add(PostVo vo) throws IOException {
        String imgUrl = s3UploadService.saveFile(vo.getImg());
        PostDto dto = PostDto.builder().TITLE(vo.getTitle()).CONTENT(vo.getContent()).IMG_URL(imgUrl).build();
        repository.save(dto);
    }

    public List<PostDto> list () {
        return repository.findAll();
    }
}

PostController.java

import com.cms.world.domain.dto.PostDto;
import com.cms.world.domain.vo.PostVo;
import com.cms.world.service.PostService;
import com.cms.world.utils.GlobalStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.List;

@RestController
@RequestMapping("/post")
public class PostController { // 블로그 이미지 리스트

    private final PostService service;

    public PostController(PostService service) {
        this.service = service;
    }

    @PostMapping("/insert")
    public ResponseEntity<String> add(PostVo vo) throws IOException {
        try {
            service.add(vo);
            return ResponseEntity.ok().body(GlobalStatus.SUCCESS.getMsg());
        } catch (IOException e) {
            return ResponseEntity.status(GlobalStatus.INTERNAL_SERVER_ERR.getStatus()).body(GlobalStatus.INTERNAL_SERVER_ERR.getMsg());
        }
    }

    @GetMapping("/list")
    public List<PostDto> list() {
        return service.list();
    }
}

GlobalStatus.java

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum GlobalStatus {


    SUCCESS(200, "성공"),
    INVALID_PARAMETER(400, "잘못된 요청"),
    INTERNAL_SERVER_ERR(500, "서버 에러");

    private final int status;
    private final String msg;

}

3. Test

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글