Framework/Spring Boot

[Framework] Spring Boot [JPA + H2] 기초

SeungyubLee 2022. 11. 30. 14:08

<application.properties에 추가=""></application.properties에>

# H2 DB, 웹 콘솔 접근 허용
spring.h2.console.enabled=true
# 실행과 동시에 데이터 생성 가능하도록 변경
spring.jpa.defer-datasource-initialization=true
# JPA 로깅 설정
# 디버그 레벨로 쿼리 출력
logging.level.org.hibernate.SQL=DEBUG
# 정리해서 보여주기
spring.jpa.properties.hibernate.format_sql=true
# 파라미터 보여주기
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# DB URL 고정 설정
# 유니크 URL 생성 X
spring.datasource.generate-unique-name=false
# 고정 URL 설정
spring.datasource.url=jdbc:h2:mem:testdb

<build.gradle에 추가=""> (Lombok 추가 : 코드 간소화, 로깅 가능하게 해주는 라이브러리)</build.gradle에>

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

화면 우측 상단 코끼리와 새로고침이 있는
Load Gradle Changes를 클릭해 다운로드 받아올 수 있도록 한다.

Help -> Find Action -> plugins 입력 -> Marketplace -> Lombok 검색 후 Install

Preferences > Build, Execution, Deployment > Compiler > Annotation Processors
Enable annotation processing체크 해제되어 있다면 체크

 

<실행과 동시에 데이터 생성>

resources > data.sql 파일 생성 -> INSERT 쿼리 추가

EX : INSERT INTO ARTICLE(ID, TITLE, CONTENT) VALUES (2, 'AAAA', '1111');

 

<H2 DB 확인>

프로젝트 실행 후 localhost:8080/h2-console

JDBC URL에 jdbc:h2:mem:testdb입력(application.properties에 설정한 고정 URL) 후 Connect

<MVC 기본 구조>

src > main > java > com.example.firstproject > controller의 ArticleController

package com.example.firstproject.controller;

import com.example.firstproject.dto.ArticleDto;
import com.example.firstproject.entity.Article;
import com.example.firstproject.repository.ArticleRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;

@Controller
@Slf4j // 로깅을 위한 골뱅이(어노테이션)
public class ArticleController {

    @Autowired // new 안써도 된다! 스프링 부트가 미리 생성해놓은 객체를 가져다가 자동 연결
    private ArticleRepository articleRepository;

    @GetMapping("/article/new")
    public String newArticleDto() {
        return "article/new";
    }

    @PostMapping("/article/create")
    public String createArticle(ArticleDto dto) {
        log.info(dto.toString());
//        System.out.println(dto.toString()); // 실제 서버에서 기록이 남지도 않고 성능에도 문제를 줌 -> 로깅 기능으로 대체!

        // 1. DTO 변환! Entity로
        Article article = dto.toEntity();
        log.info(article.toString());
//        System.out.println(article.toString());

        // 2. Repository에게 Entity를 DB에 저장하게 함
        Article saved = articleRepository.save(article);
        log.info(saved.toString());
//        System.out.println(saved.toString());

        return "redirect:/article/" + saved.getId();
    }

    @GetMapping("/article/{id}")
    public String show(@PathVariable Long id, Model model) {
        log.info("id = " + id);

        // 1: id로 데이터를 가져옴
        Article articleEntity = articleRepository.findById(id).orElse(null);

        // 2. 가져온 데이터를 모델에 등록
        model.addAttribute("article", articleEntity);

        // 3. 보여줄 페이지를 설정
        return "article/show";
    }

    @GetMapping("/article")
    public String index(Model model) {
        // 1: 모든 Article을 가져온다.
        List<Article> articleEntityList = articleRepository.findAll();

        // 2: 가져온 Article 묶음을 뷰로 전달
        model.addAttribute("articleList", articleEntityList);

        // 3: 뷰 페이지를 설정
        return "article/index";
    }

    @GetMapping("/article/{id}/edit")
    public String edit(@PathVariable Long id, Model model) {
        // 수정할 데이터를 가져오기
        Article articleEntity = articleRepository.findById(id).orElse(null);

        // 모델에 데이터를 등록 !
        model.addAttribute("article", articleEntity);

        return "article/edit";
    }

    @PostMapping("/article/update")
    public String update(ArticleDto dto) {

        // 1: DTO를 Entity로 변환
        Article articleEntity = dto.toEntity();

        // 2: Entity를 DB에 저장
        // 2-1: DB에서 기존 데이터를 가져온다.
        Article target = articleRepository.findById(articleEntity.getId()).orElse(null);

        // 2-2 기존 데이터가 있다면 값을 갱신
        if (target != null) {
            articleRepository.save(articleEntity); // 엔티티가 db를 갱신
        }

        // 3: 수정 결과 페이지로 리다이렉트 한다.
        return "redirect:/article/" + articleEntity.getId();
    }

    @GetMapping("/article/{id}/delete")
    public String delete(@PathVariable Long id, RedirectAttributes rttr) {

        // 1: 삭제 대상을 가져온다
        Article target = articleRepository.findById(id).orElse(null);

        // 2: 대상을 삭제한다.
        if (target != null) {
            articleRepository.delete(target);
            rttr.addFlashAttribute("msg", "delete complete!");
        }

        // 3: 결과 페이지로 리다이렉트 한다.
        return "redirect:/article";
    }
}

src > main > java > com.example.firstproject > dto의 ArticleDto

package com.example.firstproject.dto;

import com.example.firstproject.entity.Article;
import lombok.AllArgsConstructor;
import lombok.ToString;

@AllArgsConstructor // Lombok 사용
@ToString // Lombok 사용
public class ArticleDto { // DTO 클래스

    private Long id;
    private String title;
    private String content;

//    public ArticleDto(Long id, String title, String content) { // @AllArgsConstructor와 같음
//        this.id = id;
//        this.title = title;
//        this.content = content;
//    }

//    @Override
//    public String toString() { // @ToString과 같음
//        return "ArticleDto{" +
//                "id=" + id +
//                ", title='" + title + '\'' +
//                ", content='" + content + '\'' +
//                '}';
//    }

    public Article toEntity() {
        return new Article(id, title, content);
    }
}

src > main > java > com.example.firstproject > entity의 Article

package com.example.firstproject.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.*;

@Entity // DB가 해당 객체를 인식 가능! (해당 클래스로 테이블 생성)
@NoArgsConstructor // Lombok 사용
@AllArgsConstructor // Lombok 사용
@ToString // Lombok 사용
@Getter // Lombok 사용
public class Article {

    @Id // 대표값을 지정! PK (EX : 주민등록번호)
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 1, 2, 3,  ... DB가 id를 자동 채번하도록 하는 어노테이션 // JPA 버전 문제로 자동 채번이 안될 수 있다.
    private Long id;

    @Column
    private String title;

    @Column
    private String content;

//    public Article() { // Entity 기본 생성자 필요 // @NoArgsConstructor와 같음
//
//    }

//    public Article(Long id, String title, String content) { // @AllArgsConstructor와 같음
//        this.id = id;
//        this.title = title;
//        this.content = content;
//    }

//    @Override
//    public String toString() { // @ToString과 같음
//        return "Article{" +
//                "id=" + id +
//                ", title='" + title + '\'' +
//                ", content='" + content + '\'' +
//                '}';
//    }

//    // @Getter와 같음
//    public Long getId() {
//        return id;
//    }
//
//    public String getTitle() {
//        return title;
//    }
//
//    public String getContent() {
//        return content;
//    }

    public void patch(Article article) {
        if (article.title != null) {
            this.title = article.title;
        }

        if (article.content != null) {
            this.content = article.content;
        }
    }
}

src > main > java > com.example.firstproject > repository의 ArticleRepository

package com.example.firstproject.repository;

import com.example.firstproject.entity.Article;
import org.springframework.data.repository.CrudRepository;
import java.util.ArrayList;

public interface ArticleRepository extends CrudRepository<Article, Long> { // JPA가 제공 <관리대상 Entity, PK 타입>

    @Override
    ArrayList<Article> findAll(); // List 형태로 받기 위해 Override 했음
}

 

src > main > resources > templates > layouts > header.mustache in [Framework] Spring Boot [JPA + H2] 시작

src > main > resources > templates > layouts > footer.mustache in [Framework] Spring Boot [JPA + H2] 시작

 

src > main > resources > templates > article > index.mustache

{{>layouts/header}}
<table class="table">
    <thead>
    <tr>
        <th scope="col">ID</th>
        <th scope="col">Title</th>
        <th scope="col">Content</th>
    </tr>
    </thead>
    <tbody>
    {{#articleList}}
        <tr>
            <th>{{id}}</th>
            <td><a href="/article/{{id}}">{{title}}</td>
            <td>{{content}}</td>
        </tr>
    {{/articleList}}
    </tbody>
</table>

<a href="/article/new">new Article</a>

{{>layouts/footer}}

src > main > resources > templates > article > new.mustache

{{>layouts/header}}

<form action="/article/create" method="post">
    <input type="text" name="title">
    <textarea name="content"></textarea>
    <button type="submit">submit</button>
    <a href="/article">back</a>
</form>

{{>layouts/footer}}

src > main > resources > templates > article > show.mustache

{{>layouts/header}}
<table class="table">
    <thead>
    <tr>
        <th scope="col">ID</th>
        <th scope="col">Title</th>
        <th scope="col">Content</th>
    </tr>
    </thead>
    <tbody>
    {{#article}}
    <tr>
        <th>{{id}}</th>
        <td>{{title}}</td>
        <td>{{content}}</td>
    </tr>
    {{/article}}
    </tbody>
</table>
<a href="/article/{{article.id}}/edit" class="btn btn-primary">edit</a>
<a href="/article/{{article.id}}/delete" class="btn btn-danger">delete</a>
<a href="/article">go to article</a>
{{>layouts/footer}}

src > main > resources > templates > article > edit.mustache

{{>layouts/header}}

{{#article}}
    <form action="/article/update" method="post">
        <input type="hidden" name="id" value="{{id}}">
        <input type="text" name="title" value="{{title}}">
        <textarea name="content">{{content}}</textarea>
        <button type="submit">submit</button>
        <a href="/article/{{id}}">back</a>
    </form>
{{/article}}

{{>layouts/footer}}

필드 영역 드래그 -> Generate -> Getter, Setter 등 쉽게 만들 수 있다.

 

Entity의 @GeneratedValue(strategy = GenerationType.IDENTITY)로 인해

PK인 ID가 DB에 의해 자동 채번되어야 하지만, PK가 정상적으로 채번되지 않는다면

build.gradle의 plugins의 'org.springframework.boot' version을 2.4.1 등으로 변경해주는 방법이 있다.

Spring Boot 버전에 따라 JPA 버전도 결정되는데, JPA 버전이 상향됨에 따라 발생하는 문제로 확인된다.

단, 이 경우 다른 도구들과의 버전 호환 문제가 발생할 수 있다.