본문 바로가기

boot_JPA 수업 정리

Boot_JPA 1일

JPA를 써서 게시판을 만들어보자.

jpa(Java Persistence API)

JPA는 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스의 모음

ORM(Object-Relational Mapping)

우리가 일반 적으로 알고 있는 애플리케이션 Class와 RDB(Relational DataBase)의 테이블을 매핑(연결)한다는 뜻이며, 기술적으로는 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 것이라고 보면된다.

실제적으로 구현된것이 아니라 구현된 클래스와 매핑을 해주기 위해 사용되는 프레임워크임.
 
인터페이스이기 때문에 Hibernate, OpenJPA 등이 JPA를 구현함
 
객체와 테이블 매핑 : @Entity, @Table
기본키 매핑 : @Id
필드와 칼럼 매핑 : @Column
연관 관계 매핑 : @ManyToOne, @JoinColumn
 

JPA METHOD

 
 
 
 

 
● spring initializr 에서 프로젝트 생성 
 
- build.gradle 디펜던시 ( 타임리프, 레이아웃, 로그백 티카 썸네일 등 추가 )
 
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	// https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'

	implementation 'org.springframework.boot:spring-boot-starter-web'
	// https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	/*providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'*/
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
 

 

- application.properties 설정

spring.application.name=boot_JPA
server.port = 8089

spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/bootdb2
spring.datasource.username=springUser
spring.datasource.password=mysql

spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true

spring.thymeleaf.cache=false

 

● 로그백을 사용해서 로그를 관리

 

- log4jdbclog4jdbc.log4j2.properties 만들어서 코드 추가.

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator

 

- logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %-5p {%c} %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="com.ezen.boot_JPA" level="INFO" appender-ref="STDOUT"/>
    <logger name="jdbc" level="OFF"/>
    <logger name="jdbc.connection" level="OFF"/>
    <logger name="jdbc.audit" level="OFF"/>
    <logger name="jdbc.sqlonly" level="INFO" appender-ref="STDOUT"/>
    <logger name="jdbc.resultsettable" level="INFO" appender-ref="STDOUT"/>
    <logger name="org.springframework" level="error"/>
    <logger name="org.springframework.jdbc" level="error"/>



    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

 

BootJpaApplication에 @EnableJpaAuditing을 넣어줘야 JPA를 인식할 수 있음. 

@EnableJpaAuditing
@SpringBootApplication
public class BootJpaApplication {

    public static void main(String[] args) {
       SpringApplication.run(BootJpaApplication.class, args);
    }

}

 

● 스프링에서 했던 레이아웃 , 헤더, 푸터를 가져와서 일단 index 화면을 구성

 

● entity (테이블) 클래스 패키지 , dto ( 객체) 클래스 패키지  생성하고 컨트롤러부터 repository까지 패키지만 일단 생성

 

● entity에 Board 구성 자주쓰는 코드들은 따로 base 클래스로 별도로관리해서 extends 함

@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends TimeBase{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
    private long bno;

    @Column(length = 200, nullable = false)
    private String title;
    @Column(length = 200, nullable = false)
    private String writer;
    @Column(length = 2000, nullable = false)
    private String content;

}

 

- 등록일 , 수정일은 따로 관리

@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class}) // 필수 반드시 지정
@Getter
public class TimeBase {
    /* 등록일 수정일만 따로 뺴서 관리하는 슈퍼 테이블 */
    @CreatedDate
    @Column(name = "reg_at", updatable = false)
    private LocalDateTime regAt;

    @LastModifiedDate
    @Column(name = "mod_at")
    private LocalDateTime modAt;

}

 

● BoardDTO 구성

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BoardDTO {
    private long bno;
    private String title;
    private String writer;
    private String content;
    private LocalDateTime regAt, modAt;

}

 

● Repository는 JpaRepository를 상속받아야 JPA를 인지할 수 있음 JpaRepository 안의 값은 테이블과 , id인데 id의  타입이 기본타입말고 클래스타입으로 들어가야함으로 구성한 DTO와 Entity에서 long을 Long으로 수정해주어야함 

/* JpaRepository<테이블, id> */
public interface BoardRepository extends JpaRepository<Board, Long> {}

 

● 컨트롤러에서 일단 register로 넘어갈 수 있게 GetMapping으로 구현하고

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/board/*")
@Controller
public class BoardController {
    private final BoardService boardService;
    @GetMapping("/register")
    public void register(){

    }
}

 

- register 화면을 구성한다음 실행해서 데이터베이스가 잘 만들어지는지, 화면이 잘나오는지 확인

  <div layout:fragment="content" class="container-md">

    <div>
      <h1>Boot Register Page</h1>
      <hr>
      <form action="/board/register" method="post">
        <div class="mb-3">
          <label for="t" class="form-label">Title</label>
          <input type="text" class="form-control" id="t" name="title" placeholder="title...">
        </div>
        <div class="mb-3">
          <label for="w" class="form-label">Writer</label>
          <input type="text" class="form-control" id="w" name="writer" placeholder="writer...">
        </div>
        <div class="mb-3">
          <label for="c" class="form-label">Content</label>
          <textarea class="form-control" id="c"  name="content" cols="10" rows="3" placeholder="content..."></textarea>
        </div>
        <button type="submit" class="btn btn-primary" id="regBtn">register</button>
      </form>
    </div>
  </div>

 

구성한 Entity에 맞게 테이블이 잘 생성되는걸 확인할 수 있음

 

● 컨트롤러에서 register에서 post한 입력한 정보를 받아와서 DB에 보내서 insert하고 insert에 성공하면 

JPA에서는 row를 리턴하는게아닌 id를 리턴하므로 Long형의 bno로 받음.

    @PostMapping("/register")
    public String register(BoardDTO boardDTO){
        log.info("boardDTO >> {} ", boardDTO);
        // insert, update , delete => return 1 row
        // JPA insert, update ,delete => return id

        Long bno = boardService.insert(boardDTO);

        log.info(" >>> insert Ok?  >> {} " , bno > 0 ? "Ok" : "Fail");

        return "/index";
    }

 

● 서비스에서도 default를 사용하면 메서드를 구현할 수 있음.

화면에서 가져온 DTO를 Board 객체로 변환하고 DB에서 가져온 Board객체를 DTO객체로 변환해주는 메서드를 작성해줌.

public interface BoardService {
    // 추상 메서드만 가능한 인터페이스
    // 메서드가 default(접근제한자) 구현 가능.
    Long insert(BoardDTO boardDTO);


    // BoardDTO(class) : bno title writer content regAt modAt
    // Board(table) : bno title writer content
    // BoardDTO => board 변환
    // 화면에서 가져온 BoardDTO 객체를 저장을 위한 Board 객체로 변환
    default Board convertDtoToEntity(BoardDTO boardDTO){

        return Board.builder()
                .bno(boardDTO.getBno())
                .title(boardDTO.getTitle())
                .writer(boardDTO.getWriter())
                .content(boardDTO.getContent())
                .build();
    }


    // board => BoardDTO 변환
    // DB에서 가져온 Board 객체를 화면에 뿌리기위한 BoardDTO 객체로 변환
    default  BoardDTO convertEntityToDto(Board board){

        return BoardDTO.builder()
                .bno(board.getBno())
                .title(board.getTitle())
                .writer(board.getWriter())
                .content(board.getContent())
                .regAt(board.getRegAt())
                .modAt(board.getModAt())
                .build();
    }
 }

 

  ServiceImpl로가서 save()를 이용해 insert후 id를 리턴하도록함 save에는 class 객체가아닌 Entity를 파라미터로 전송해야하므로 서비스에서 만든 convert 변환 메서드를 사용해주어야하기에 서비스에서 변환메서드를 필수로 만들어주어야함.

    @Override
    public Long insert(BoardDTO boardDTO) {
        // 저장 객체는 Board
        // save() : insert 후 저장 객체의 id를 리턴
        // save() Entity 객체를 파라미터로 전송

        return boardRepository.save(convertDtoToEntity(boardDTO)).getBno();
    }

 

●  list를 가져오도록 구현 

 

- 리스트 화면부터 구성해주고 

<div class="container-md">
        <h1>Board List Page</h1>
        <hr>

        <table class="table table-hover">
            <thead>
            <tr>
                <th scope="col">no.</th>
                <th scope="col">title</th>
                <th scope="col">writer</th>
                <th scope="col">regAt</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="bvo:${list}">
                <td>[[${bvo.bno }]]</td>
                <td> <a th:href="@{/board/detail(bno=${bvo.bno})}">[[${bvo.title }]]</a></td>
                <td>[[${bvo.writer }]]</td>
                <td>[[${bvo.regAt }]]</td>
            </tr>
            </tbody>
        </table>
    </div>

 

- 컨트롤러에서 연결해서 DB에서 리스트를 가져와 model을 이용해서 화면에 전달

    @GetMapping("/list")
    public void list(Model model){

        List<BoardDTO> list = boardService.getList();
        model.addAttribute("list",list);
    }

 

- 서비스 구현체에서 구현해주는데 DB에서 Board의 리스트로 리턴하므로 BoardDTO의 리스트로 변환해주어야함 

steam().map을 사용해서 변환해서 List안에 넣어주고 리턴

    @Override
    public List<BoardDTO> getList() {
        // 컨트롤러로 보내야하는 리턴은 List<BoardDTO>
        // DB에서 가져오는 리턴은 List<Board> -> BoardDTO 객체로 변환
        // findAll
        // 정렬 : Sort.by(Sort.Direction.DESC, "정렬기준 칼럼명")
        List<Board> boardList = boardRepository.findAll(Sort.by(Sort.Direction.DESC, "bno"));

        List<BoardDTO> boardDTOList = boardList.stream()
                .map(b -> convertEntityToDto(b)).toList();

        return boardDTOList;
    }

 

● detail 페이지 구현 

 

- js로 detail 페이지에서 상태변화로 modify까지 구현할 것이므로 form안에 넣어 화면구성

스크립트도 넣어두고 js 파일도 생성.

  <div class="container-md">
        <h1>Board Detail Page [[${boardDTO.bno}]]</h1>
        <hr>
        <form action="/board/modify" method="post" id="modForm">
            <input type="hidden" name="bno" th:value="${boardDTO.bno}">
            <div class="mb-3">
                <label for="t" class="form-label">title</label>
                <input type="text"
                       class="form-control" id="t" name="title" th:value="${boardDTO.title }"
                       readonly="readonly">
            </div>
            <div class="mb-3">
                <label for="w" class="form-label">writer</label>
                <input type="text"
                       class="form-control" id="w" name="writer" th:value="${boardDTO.writer }"
                       readonly="readonly"> <span class="badge text-bg-info">[[${boardDTO.regAt }]]</span>
                <br> <span class="badge text-bg-info">[[${boardDTO.modAt }]]</span>
            </div>
            <div class="mb-3">
                <label for="c" class="form-label">content</label>
                <textarea class="form-control" id="c" rows="3" name="content"
                          readonly="readonly">[[${boardDTO.content }]]</textarea>
            </div>
            <button type="button" id="listBtn" class="btn btn-info">List</button>
            <button type="button" id="modBtn" class="btn btn-primary">Modify</button>
            <a th:href="@{/board/delete(bno=${boardDTO.bno})}"><button type="button" id="delBtn" class="btn btn-danger">Delete</button></a>
        </form>
    </div>

    <script th:src="@{/js/boardDetail.js}"></script>

 

- 컨트롤러에서 받아온 bno를 이용해서 DB에서 가져와서 model을 이용해서 boardDTO를 화면에 보낼 수 있도록함.

    @GetMapping("/detail")
    public void detail(Model model, @RequestParam("bno") Long bno){
        BoardDTO boardDTO = boardService.getDetail(bno);

        model.addAttribute("boardDTO" , boardDTO);
    }

 

- findById의 리턴타입 Optional<Board> 타입으로 리턴
◉ Optional<T> : nullPointException 발생하지 않도록 도와줌.
◉ Optional.isEmpty() : null일 경우 확인가능 true / false
◉ Optional.isPresent() : 값이 있는지 확인 true / false
◉ Optional.get() : 객체 가져오기

   @Override
    public BoardDTO getDetail(Long bno) {
        /* findById : 아이디(PK)를 주고 해당 객체를 리턴 */

        Optional<Board> optional = boardRepository.findById(bno);
        if(optional.isPresent()){
            BoardDTO boardDTO = convertEntityToDto(optional.get());
            return boardDTO;
        }

        return null;
    }

 

DB에서 Board로 가져온 optional이 존재하면 객체를 가져와서 DTO로 변환해서 저장한 후 리턴

 

● modify 페이지 구현 

 

- js 파일에 버튼을 눌렀을때 버튼이 바뀌고 수정이가능하도록 readOnly가 false되도록 구현 

document.getElementById('listBtn').addEventListener('click', () => {
    location.href = "/board/list";
})

document.getElementById('modBtn').addEventListener('click' , () => {
    document.getElementById('t').readOnly = false;
    document.getElementById('c').readOnly = false;

    // 버튼 생성
    let modBtn = document.createElement("button");
    modBtn.setAttribute('type','submit');
    modBtn.classList.add('btn', 'btn-outline-primary');
    modBtn.innerText="submit";

    // 추가 
    document.getElementById('modForm').appendChild(modBtn);
    document.getElementById("modBtn").remove();
    document.getElementById("delBtn").remove();

})

 

- 컨트롤러에서 DB에 화면에서 수정된 내용보내고 update 할 수 있도록 구현.

    @PostMapping("/modify")
    public String modify(BoardDTO boardDTO, RedirectAttributes redirectAttributes){

        Long bno = boardService.modify(boardDTO);
        redirectAttributes.addAttribute("bno",boardDTO.getBno());
        return "redirect:/board/detail";
    }

 

- update는 따로없으니 save를 이용 수정내용을 set해주고 save해주어야함.

    @Override
    public Long modify(BoardDTO boardDTO) {
        // update : jpa는 업데이트가 없음
        // 기존 객체를 가져와서 set 수정 후 다시 저장

        Optional<Board> optional = boardRepository.findById(boardDTO.getBno());

        if(optional.isPresent()){
            Board entity = optional.get();
            entity.setTitle(boardDTO.getTitle());
            entity.setContent(boardDTO.getContent());
            return  boardRepository.save(entity).getBno();
        }
        return null;
    }

 

● delete 구현 

 

- 버튼에 a태그달아서 delete 경로 적어주고 bno를 보내줌

 <a th:href="@{/board/delete(bno=${boardDTO.bno})}"><button type="button" id="delBtn" class="btn btn-danger">Delete</button></a>

 

- 컨트롤러에서 받아온 bno로 삭제 서비스 연결 

    @GetMapping("/delete")
    public String delete(@RequestParam("bno") Long bno){

        boardService.delete(bno);

        return "redirect:/board/list";
    }

 

- deleteById로 받아온 bno로 삭제 void이므로 리턴없음.

    // 삭제 : deleteById(id)
    @Override
    public void delete(Long bno) {
        boardRepository.deleteById(bno);
    }

 

'boot_JPA 수업 정리' 카테고리의 다른 글

5일  (0) 2024.11.26
Boot_JPA 4일차  (1) 2024.11.25
Boot_JPA 3일차  (0) 2024.11.22
Boot_JPA 2일차  (3) 2024.11.21