반응형

📖 프론트엔드 작업

(1) Qna Type 지정

// IQna.ts : 타입 인터페이스

export default interface IQna {
    qno?: any | null,
    question: string,
    answer: string,
    questioner: string,
    answerer: string 
}

(2) Qna 게시판 CRUD 함수 생성을 위한 QnaService 생성

// QnaService.ts : axios 공통함수 (벡엔드 CRUD 연동함수)

import IQna from "../../types/basic/IQna";
import http from "../../utils/http-common";

// 전체 조회 + like 검색(paging 기능 : page(현재페이지), size(한 페이지당 개수))
// 셀렉트박스 : (question) 입력창 : 질문 like 검색
// 셀렉트박스 : (questioner) 입력창 : 질문자 like 검색
// 변수 : searchSelect(question, questioner)
// 변수 : searchKeyword : 검색어
const getAll = (searchSelect:string, searchKeyword:string, page:number, size:number) => {
    return http.get<Array<IQna>>(`/basic/qna?searchSelect=${searchSelect}&searchKeyword=${searchKeyword}&page=${page}&size=${size}`);
  };
  
  // 상세 조회
  const get = (qno:any) => {
    return http.get<IQna>(`/basic/qna/${qno}`);
  };
  
  // 저장 함수
  const create = (data:IQna) => {
    return http.post<IQna>("/basic/qna", data);
  };
  
  // 수정 함수
  const update = (qno:any, data:IQna) => {
    return http.put<any>(`/basic/qna/${qno}`, data);
  };
  
  // 삭제 함수
  const remove = (qno:any) => {
    return http.delete<any>(`/basic/qna/deletion/${qno}`);
  };
  
  const QnaService = {
    getAll,
    get,
    create,
    update,
    remove,
  };
  
  export default QnaService;

(3) Qna 화면 및 JS 코딩

// QnaList.tsx : rfce
import { Pagination } from "@mui/material";
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import TitleCom from "../../../components/common/TitleCom";
import IQna from "../../../types/basic/IQna";
import QnaService from "../../../services/basic/QnaServics";


function QnaList() {
  // 변수 정의
  // qna 배열 변수
  const [qna, setQna] = useState<Array<IQna>>([]);
  // select 태그에 선택된 값을 저장할 변수 : 기본 (question)
  const [searchSelect, setSearchSelect] = useState<string>("question");
  // 검색어(input) 변수
  const [searchKeyword, setSearchKeyword] = useState<string>("");

  // todo: 공통 페이징 변수 4개
  // todo: 공통 변수 : page(현재페이지번호), count(총페이지건수), pageSize(3,6,9 배열)
  const [page, setPage] = useState<number>(1);
  const [count, setCount] = useState<number>(1);
  const [pageSize, setPageSize] = useState<number>(3); // 1페이지당개수
  const pageSizes = [3, 6, 9]; // 공통 pageSizes : 배열 (셀렉트 박스 사용)

  // 함수 정의
  //   화면이 뜰때 실행되는 이벤트 + 감시변수
  useEffect(() => {
    retrieveQna(); // 전체조회 실행
  }, [page, pageSize]);

  // 전체조회
  const retrieveQna = () => {
    // 벡엔드 매개변수 전송 : + 현재페이지(page), 1페이지당개수(pageSize)
    QnaService.getAll(searchSelect, searchKeyword, page - 1, pageSize) // 벡엔드 전체조회요청
      .then((response: any) => {
        const { qna, totalPages } = response.data;
        setQna(qna);
        setCount(totalPages);
        // 로그 출력
        console.log("response", response.data);
      })
      .catch((e: Error) => {
        console.log(e);
      });
  };

  //   select 태그 수동바인딩
  const onChangeSearchSelect = (e: any) => {
    setSearchSelect(e.target.value); // 화면값 -> 변수저장
  };

  //   input 태그 수동바인딩
  const onChangeSearchKeyword = (e: any) => {
    setSearchKeyword(e.target.value); // 화면값 -> 변수저장
  };

  //   todo: 공통 페이징 함수 2개
  // todo: handlePageSizeChange(공통) : pageSize 값 변경시 실행되는 함수
  //  select 태그 수동 바인딩 : 화면값 -> 변수에 저장
  const handlePageSizeChange = (event: any) => {
    setPageSize(event.target.value); // 1페이지당 개수저장(3,6,9)
    setPage(1); // 현재페이지번호 : 1로 강제설정
  };

  //  todo: Pagination 수동 바인딩(공통)
  //  페이지 번호를 누르면 => page 변수에 값 저장
  const handlePageChange = (event: any, value: number) => {
    // value == 화면의 페이지번호
    setPage(value);
  };

  return (
    // 여기
    <>
      {/* 제목 start */}
      <TitleCom title="Qna List" />
      {/* 제목 end */}

      {/* question start(다양한 검색어 부분) */}
      <div className="row mb-5 justify-content-center">
        <div className="col-md-8">
          <div className="input-group mb-3">
            {/* 다양한 검색(select : question,questioner) 시작 */}
            <div className="col-2">
              <select
                className="form-select"
                onChange={onChangeSearchSelect}
                value={searchSelect}
              >
                <option key="question" value="question">
                  question
                </option>
                <option key="questioner" value="questioner">
                  questioner
                </option>
              </select>
            </div>
            {/* 다양한 검색(select) 끝 */}

            {/* 검색어(searchKeyword) 입력창(input) 시작 */}
            <div className="col-9  w-50 input-group mb-3">
              <input
                type="text"
                className="form-control"
                placeholder="Search by Question"
                value={searchKeyword}
                onChange={onChangeSearchKeyword}
              />
            </div>
            {/* 검색어 입력창 끝 */}

            {/* 검색버튼 시작 */}
            <div className="input-group-append col-md-1">
              <button
                className="btn btn-outline-secondary"
                type="button"
                onClick={retrieveQna}
              >
                Search
              </button>
            </div>
            {/* 검색버튼 끝 */}
          </div>
        </div>
      </div>
      {/* question end */}

      <div className="col-md-12">
        {/* page control start(페이징 html) */}
        <div className="mt-3">
          {"Items per Page: "}
          <select onChange={handlePageSizeChange} value={pageSize}>
            {pageSizes.map((size) => (
              <option key={size} value={size}>
                {size}
              </option>
            ))}
          </select>

          <Pagination
            className="my-3"
            count={count}
            page={page}
            siblingCount={1}
            boundaryCount={1}
            variant="outlined"
            shape="rounded"
            onChange={handlePageChange}
          />
        </div>
        {/* page control end */}

        {/* table start(본문) */}
        <table className="table">
          <thead>
            <tr>
              <th scope="col">Question</th>
              <th scope="col">Questioner</th>
              <th scope="col">Answer</th>
              <th scope="col">Answerer</th>
            </tr>
          </thead>
          <tbody>
            {qna &&
              qna.map((data) => (
                // 키값 추가 않하면 react 에서 경고를 추가 : 키는 내부적으로 리액트가 rerending 할때 체크하는 값임
                <tr key={data.question}>
                  <td>{data.question}</td>
                  <td>{data.questioner}</td>
                  <td>{data.answer}</td>
                  <td>{data.answerer}</td>
                  <td>
                    <Link to={"/qna/" + data.qno}>
                      <span className="badge bg-success">Edit</span>
                    </Link>
                  </td>
                </tr>
              ))}
          </tbody>
        </table>
        {/* table end */}
      </div>
    </>
  );
}

export default QnaList;

(4) 프론트 엔드 서버 시작 후 화면 확인

 

📖 벡엔드 작업

(1) Qna Entity 생성

package com.example.simpledms.model.entity.basic;

import com.example.simpledms.model.common.BaseTimeEntity;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

/**
 * packageName : com.example.simpledms.model.entity.basic
 * fileName : Qna
 * author : GGG
 * date : 2023-10-24
 * description : Qna 엔티티(vo)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-24         GGG          최초 생성
 */
@Entity
@Table(name="TB_QNA")
@SequenceGenerator(
        name = "SQ_QNA_GENERATOR"
        , sequenceName = "SQ_QNA"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_QNA SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE QNO = ?")
public class Qna extends BaseTimeEntity {
    //    속성
//    단축키 : ctrl + shift + u
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
            generator = "SQ_QNA_GENERATOR")
    private Integer qno; // 기본키(@Id), 시퀀스

    private String question;

    private String answer;

    private String questioner;

    private String answerer;

}

🔎 빌더 패턴?

빌더 패턴은 여러 생성자 인수가 필요한 복잡한 개체를 만드는 방법을 제공하는 소프트웨어 디자인 패턴입니다. 선택적 매개변수가 많은 객체를 생성할 필요가 있을 때 자주 사용됩니다.

빌더 패턴은 여러 생성자 인수가 필요한 복잡한 개체를 만드는 문제를 해결하는 데 사용되는 생성 디자인 패턴입니다. 빌더 패턴은 객체의 구성과 표현을 분리합니다. 이를 통해 동일한 구성 프로세스에서 다른 표현을 만들 수 있습니다.

팩토리 패턴은 동일한 유형의 객체를 생성해야 할 때 사용됩니다. 빌더 패턴은 다른 유형의 객체를 생성해야 할 때 사용됩니다.

빌더 패턴에는 다음과 같은 이점이 있습니다.

  • 객체의 구성과 표현을 분리합니다. 이를 통해 동일한 구성 프로세스에서 다른 표현을 만들 수 있습니다.
  • 복잡한 개체를 단계별로 생성할 수 있습니다.
  • 클라이언트 코드에 영향을 주지 않고 구성 프로세스를 변경할 수 있습니다.

빌더 패턴에는 다음과 같은 단점이 있습니다.

  • 많은 상용구 코드로 이어질 수 있습니다.
  • 빌더 패턴을 올바르게 사용하지 않으면 결과 코드를 이해하기 어려울 수 있습니다.

🔎 빌더 패턴은 언제 사용할까?

빌더 패턴은 여러 생성자 인수가 필요한 복잡한 객체를 생성해야 할 때 사용해야 합니다. 선택적 매개변수가 많은 객체를 생성할 필요가 있을 때 자주 사용됩니다.

(2) QnaRepository 생성

package com.example.simpledms.repository.basic;

import com.example.simpledms.model.entity.basic.Qna;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * packageName : com.example.simpledms.repository.basic
 * fileName : QnaRepository
 * author : GGG
 * date : 2023-10-24
 * description : Qna CRUD Repository
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-24         GGG          최초 생성
 */
@Repository
public interface QnaRepository extends JpaRepository<Qna, Integer> {
//    1) question(질문) like 검색
    Page<Qna> findAllByQuestionContaining(String question, Pageable pageable);

//    2) questioner(질문자) like 검색
    Page<Qna> findAllByQuestionerContaining(String questioner, Pageable pageable);
}

(3) QnaService 생성

package com.example.simpledms.service.basic;

import com.example.simpledms.model.entity.basic.Qna;
import com.example.simpledms.repository.basic.QnaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

/**
 * packageName : com.example.simpledms.service.basic
 * fileName : QnaService
 * author : GGG
 * date : 2023-10-24
 * description : Qna 서비스(업무로직 : 비지니스 로직)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-24         GGG          최초 생성
 */
@Service
public class QnaService {
    @Autowired
    QnaRepository qnaRepository;    // DI

//    1) question like 검색
    public Page<Qna> findAllByQuestionContaining(String question, Pageable pageable) {
        Page<Qna> page = qnaRepository.findAllByQuestionContaining(question, pageable);
        return page;
    }

//    2) questioner like 검색
    public Page<Qna> find(String questioner, Pageable pageable){
        Page<Qna> page = qnaRepository.findAllByQuestionerContaining(questioner, pageable);
        return page;
    }


}

(4) QnaController 생성

package com.example.simpledms.controller.basic;

import com.example.simpledms.model.entity.basic.Dept;
import com.example.simpledms.model.entity.basic.Qna;
import com.example.simpledms.service.basic.QnaService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * packageName : com.example.simpledms.controller.basic
 * fileName : QnaController
 * author : GGG
 * date : 2023-10-24
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-24         GGG          최초 생성
 */
@RestController
@Slf4j
@RequestMapping("/api/basic")
public class QnaController {

    @Autowired
    QnaService qnaService;  // DI

//    전체조회 + question/questioner like 검색
    @GetMapping("/qna")
    public ResponseEntity<Object> findAllByContaining(
            @RequestParam(defaultValue = "") String searchSelect,
            @RequestParam(defaultValue = "") String searchKeyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "3") int size

    ) {
        try {
//            페이지 변수 저장 (page:현재페이지 번호, size: 한 페이지당 개수)
//            함수 매개변수 : Pageable(위의 값을 넣기)
//            사용법 : Pageable pageable = PageRequest.of(현재페이지번호, 한페이지당개수);
            Pageable pageable = PageRequest.of(page, size);
            Page<Qna> qnaPage;
            if(searchSelect.equals("question")){    // searchSelect가 question 이면
                qnaPage
                        = qnaService.findAllByQuestionContaining(searchSelect, pageable);   // question like 검색하기

            } else {
                qnaPage = qnaService.findAllByQuestionerContaining(searchKeyword, pageable); // 아니면 questioner like 검색하기
            }
//            question like 검색

//            리액트 전송 : qna배열, 페이징 정보 [자료구조 : Map<키이름, 값>]
            Map<String, Object> response = new HashMap<>();
            response.put("qna", qnaPage.getContent());                // qna배열 전송
            response.put("currentPage", qnaPage.getNumber());          // 현재페이지번호 전송
            response.put("totalItems", qnaPage.getTotalElements());    // 총 건수(개수) 전송
            response.put("totalPages", qnaPage.getTotalPages());       // 총 페이지수 전송

//            신호 보내기
            if (qnaPage.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }


        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }


}

(5) 프론트 + 벡엔드 연동확인

1) 전체검색

2) Question like 검색 = 3으로 검색 시

3) Questioner like 검색 = '홍'으로 검색 시

반응형

'Spring Boot > 스프링부트 예제' 카테고리의 다른 글

답변형 게시판 구현 (2)  (1) 2023.10.27
답변형 게시판 만들기 (1)  (1) 2023.10.26
게시판 페이징 처리  (1) 2023.10.23
front + backend 게시판 CRUD 구현 (2)  (0) 2023.10.20
JPA - 연관관계 매핑  (0) 2023.10.19
반응형

01. 데이터 모델링의 이해

(1) 모델링(Modeling)

데이터베이스에서의 모델링은 '현실 세계를 단순화하여 표현하는 기법'이다.

(2) 모델링의 특징

① 추상화(Abstraction) : 현실 세계를 일정한 형식으로 표현하는 것, 아이디어나 개념을 간략하게 표현하는 과정

② 단순화(Simplification) : 복잡한 현실 세계를 정해진 표기법으로 단순하고 쉽게 표현한다는 의미

③ 명확화(Clarity) : 불분명함을 제거하고 명확하게 해석할 수 있도록 기술한다는 의미

(3) 모델링의 세 가지 관점

① 데이터 관점

데이터 위주의 모델링, 어떤 데이터들이 업무와 얽혀있는지, 그리고 그 데이터 간에는 어떤 관계가 있는지에 대해서 모델링하는 방법

② 프로세스 관점

프로세스 위주의 모델링, 이 업무가 실제로 처리하고 있는 일은 무엇인지 또는 처리해야 하는 일은 무엇인지 모델링하는 방법

③ 데이터와 프로세스의 상관 관점

데이터와 프로세스의 관계를 위주로 한 모델링, 프로세스의 흐름에 따라 데이터가 어떤 영향을 받는지를 모델링하는 방법

 

(4) 모델링의 세 가지 단계

① 개념적 데이터 모델링

전사적 데이터 모델링 수행 시 행해지며 추상화 레벨이 가장 높은 모델링, 이 단계는 업무 중심적이고 포괄적인 수준의 모델링이 진행

② 논리적 데이터 모델링

재사용성이 가장 높은 모델링으로 데이터베이스 모델에 대한 키, 속성, 관계 등을 모두 표현하는 단계

③ 물리적 데이터 모델링

실제 데이터베이스로 구현할 수 있도록 성능이나 가용성 등의 물리적인 성격을 고려하여 모델을 표현하는 단계

 

(5) 데이터의 독립성

① 3단계 스키마 구조

가. 외부스키마

사용자의 관점 : Multiple User's View 단계로 각 사용자가 보는 데이터베이스의 스키마를 정의

나. 개념스키마

통합된 관점 : Community View of DB 단계로 모든 사용자가 보는 데이터베이스의 스키마를 통합하여 전체 데이터베이스를 나타내는 것, 데이터베이스에 저장되는 데이터들을 표현하고 데이터들 간의 관계를 나타냄

다. 내부스키마

물리적 관점 : Physical Representation 단계로 물리적인 저장 구조를 나타낸다. 실질적인 데이터의 저장 구조나 컬럼 정의, 인덱스 등이 포함

 

② 3단계 스키마 구조가 보장하는 독립성

가. 논리적 독립성 : 개념스키마가 변경되어도 외부스키마는 영향받지 X

나. 물리적 독립성 : 내부스키마가 변경되어도 외부/개념 스키마는 영향받지 X

 

(6) ERD

시스템에 어떤 엔터티들이 존재하며 그들 간에 어떤 관계가 있는지를 나타내는 다이어그램

출처:&nbsp;Kdata 데이터자격검정 - - www.dataq.or.kr

① ERD 작성순서

ㄱ. 엔터티를 도출하고 그린다.

ㄴ. 엔터티를 적절하게 배치한다.

ㄷ. 엔터티 간의 관계를 설정한다.

ㄹ. 관계명을 기입한다.

ㅁ. 관계의 참여도를 기입한다.

ㅂ. 관계의 필수/선택 여부를 기입한다.

 

02. 엔터티(Entity)

(1) 엔터티란?

엔터티의 사전적 의미는 '독립체'이다. DB에서 엔터티는 식별이 가능한 객체라는 의미를 가지고 있다.

쉽게 말해 엔터티는 업무에서 쓰이는 데이터를 용도별로 분류한 그룹이라 볼 수 있다.

 

(2) 엔터티의 특징

업무에서 쓰이는 정보여야 함

엔터티 특징의 첫 번째는 반드시 시스템을 구축하고자 하는 업무에서 필요로 하고 관리하고자 하는 정보여야 한다는 점이다. 예를 들어 환자라는 엔터티는 의료시스템을 개발하는 병원에서는 반드시 필요한 엔터티이지만 일반회사에서 직원들이 병에 걸려 업무에 지장을 준다하더라도 이 정보를 그 회사의 정보로서 활용하지는 않을 것이다. 즉 시스템 구축 대상인 해당업무에서 그 엔터티를 필요로 하는가를 판단하는 것이 중요하다.

 

식별이 가능해야 한다

두 번째는 식별자(Unique Identifier)에 의해 식별이 가능해야 한다는 점이다. 어떤 엔터티이건 임의의 식별자(일련번호)를 부여하여 유일하게 만들 수는 있지만, 엔터티를 도출하는 경우에 각각의 업무적으로 의미를 가지는 인스턴스가 식별자에 의해 한 개씩만 존재하는지 검증해 보아야 한다.

유일한 식별자는 그 엔터티의 인스턴스만의 고유한 이름이다. 두 개 이상의 엔터티를 대변하면 그 식별자는 잘못 설계된 것이다. 예를 들어 직원을 구분할 수 있는 방법은 이름이나 사원번호가 될 수가 있다. 그러나 이름은 동명이인(同名異人)이 될 수 있으므로 유일하게 식별될 수 없다. 사원번호는 회사에 입사한 사람에게 고유하게 부여된 번호이므로 유일한 식별자가 될 수 있는 것이다.

 

2개 이상의 인스턴스를 가지고 있어야 함

세 번째는 영속적으로 존재하는 인스턴스의 집합이 되어야 한다는 점이다. 엔터티의 특징 중 “한 개”가 아니라 “두 개 이상”이라는 집합개념은 매우 중요한 개념이다. 두 개 이상이라는 개념은 엔터티뿐만 아니라 엔터티간의 관계, 프로세스와의 관계 등 업무를 분석하고 설계하는 동안 설계자가 모든 업무에 대입해보고 검증해 보아야 할 중요한 개념이다. 하나의 엔터티는 여러 개의 인스턴스를 포함한다.

 

반드시 속성을 가지고 있어야 함

네 번째는 엔터티에는 반드시 속성(Attributes)이 포함되어야 한다는 점이다. 속성을 포함하지 않고 엔터티의 이름만 가지고 있는 경우는 관계가 생략되어 있거나 업무 분석이 미진하여 속성정보가 누락되는 경우에 해당한다. 또한 주식별자만 존재하고 일반속성은 전혀 없는 경우도 마찬가지로 적절한 엔터티라고 할 수 없다. 단, 예외적으로 관계엔터티(Associative Entity)의 경우는 주식별자 속성만 가지고 있어도 엔터티로 인정한다.

 

다른 엔터티와 1개 이상의 관계를 가지고 있어야 함

다섯 번째는 엔터티는 다른 엔터티와 최소 한 개 이상의 관계가 존재해야 한다는 것이다. 기본적으로 엔터티가 도출되었다는 것은 해당 업무내에서 업무적인 연관성(존재적 연관성, 행위적 연관성)을 가지고 다른 엔터티와의 연관의 의미를 가지고 있음을 나타낸다. 그러나 관계가 설정되지 않은 엔터티의 도출은 부적절한 엔터티가 도출되었거나 아니면 다른 엔터티와 적절한 관계를 찾지 못했을 가능성이 크다.

 

(3) 엔터티의 분류

ㄱ. 유무형에 따른 분류

일반적으로 엔터티는 유무형에 따라 유형엔터티, 개념엔터티, 사건엔터티로 구분된다.
유형엔터티(Tangible Entity)는 물리적인 형태가 있고 안정적이며 지속적으로 활용되는 엔터티로 업무로부터 엔터티를 구분하기가 가장 용이하다. 예를 들면, 사원, 물품, 강사 등이 이에 해당된다.

개념엔터티(Conceptual Entity)는 물리적인 형태는 존재하지 않고 관리해야 할 개념적 정보로 구분이 되는 엔터티로 조직, 보험상품 등이 이에 해당된다.

사건 엔터티(Event Entity)는 업무를 수행함에 따라 발생되는 엔터티로서 비교적 발생량이 많으며 각종 통계자료에 이용될 수 있다. 주문, 청구, 미납 등이 이에 해당된다.

 

ㄴ. 발생시점에 따른 분류

엔터티의 발생시점에 따라 기본/키엔터티(Fundamental Entity, Key Entity), 중심엔터티(Main Entity), 행위엔터티(Active Entity)로 구분할 수 있다.

 

1) 기본엔터티

기본엔터티란 그 업무에 원래 존재하는 정보로서 다른 엔터티와 관계에 의해 생성되지 않고 독립적으로 생성이 가능하고 자신은 타 엔터티의 부모의 역할을 하게 된다. 다른 엔터티로부터 주식별자를 상속받지 않고 자신의 고유한 주식별자를 가지게 된다. 예를 들어 사원, 부서, 고객, 상품, 자재 등이 기본엔터티가 될 수 있다.

 

2) 중심엔터티

중심엔터티란 기본엔터티로부터 발생되고 그 업무에 있어서 중심적인 역할을 한다. 데이터의 양이 많이 발생되고 다른 엔터티와의 관계를 통해 많은 행위엔터티를 생성한다. 예를 들어 계약, 사고, 예금원장, 청구, 주문, 매출 등이 될 수 있다.

 

3) 행위엔터티

행위엔터티는 두 개 이상의 부모엔터티로부터 발생되고 자주 내용이 바뀌거나 데이터량이 증가된다. 분석초기 단계에서는 잘 나타나지 않으며 상세 설계단계나 프로세스와 상관모델링을 진행하면서 도출될 수 있다. 예를 들어 주문목록, 사원변경이력 등이 포함된다.

 

03. 속성

(1) 속성이란?

    속성이란 사전적인 의미로는 사물(事物)의 성질, 특징 또는 본질적인 성질, 그것이 없다면 실체를 생각할 수 없는 것으로 정의할 수 있다. 본질적 속성이란 어떤 사물 또는 개념에 없어서는 안 될 징표(徵表)의 전부이다. 이 징표는 사물이나 개념이 어떤 것인지를 나타내고 그것을 다른 것과 구별하는 성질이라고 할 수 있다.

    이런 사전적인 정의 이외에 데이터 모델링 관점에서 속성을 정의하자면, “업무에서 필요로 하는 인스턴스로 관리하고자 하는 의미상 더 이상 분리되지 않는 최소의 데이터 단위”로 정의할 수 있다. 업무상 관리하기 위한 최소의 의미 단위로 생각할 수 있고, 이것은 엔터티에서 한 분야를 담당하고 있다.

 

속성의 정의를 정리해 보면 다음과 같다.

  • 업무에서 필요로 한다.
  • 의미상 더 이상 분리되지 않는다.
  • 엔터티를 설명하고 인스턴스의 구성요소가 된다.

(2) 속성의 특징

  • 엔터티와 마찬가지로 반드시 해당 업무에서 필요하고 관리하고자 하는 정보이어야 한다. (예, 강사의 교재이름)
  • 정규화 이론에 근간하여 정해진 주식별자에 함수적 종속성을 가져야 한다.
  • 하나의 속성에는 한 개의 값만을 가진다. 하나의 속성에 여러 개의 값이 있는 다중값일 경우 별도의 엔터티를 이용하여 분리한다.

(3) 엔터티, 인스턴스, 속성, 속성값의 관계

엔터티, 인스턴스, 속성, 속성값에 대한 관계를 분석하면 다음과 같은 결과를 얻을 수 있다.

출처:&nbsp;Kdata 데이터자격검정 - - www.dataq.or.kr

  1. 한 개의 엔터티는 두 개 이상의 인스턴스의 집합이어야 한다.
  2. 한 개의 엔터티는 두 개 이상의 속성을 갖는다.
  3. 한 개의 속성은 한 개의 속성값을 갖는다.

(4) 속성의 분류

① 특성에 따른 분류

속성은 업무분석을 통해 바로 정의한 속성을 기본속성(Basic Attribute), 원래 업무상 존재하지는 않지만 설계를 하면서 도출해내는 속성을 설계속성(Designed Attribute), 다른 속성으로부터 계산이나 변형이 되어 생성되는 속성을 파생속성(Derived Attribute)이라고 한다.

 

1) 기본속성

기본 속성은 업무로부터 추출한 모든 속성이 여기에 해당하며 엔터티에 가장 일반적이고 많은 속성을 차지한다. 코드성 데이터, 엔터티를 식별하기 위해 부여된 일련번호, 그리고 다른 속성을 계산하거나 영향을 받아 생성된 속성을 제외한 모든 속성은 기본속성이다. 주의해야 할 것은 업무로부터 분석한 속성이라도 이미 업무상 코드로 정의한 속성이 많다는 것이다. 이러한 경우도 속성의 값이 원래 속성을 나타내지 못하므로 기본속성이 되지 않는다.

 

2) 설계속성

설계속성은 업무상 필요한 데이터 이외에 데이터 모델링을 위해, 업무를 규칙화하기 위해 속성을 새로 만들거나 변형하여 정의하는 속성이다. 대개 코드성 속성은 원래 속성을 업무상 필요에 의해 변형하여 만든 설계속성이고 일련번호와 같은 속성은 단일(Unique)한 식별자를 부여하기 위해 모델 상에서 새로 정의하는 설계속성이다.

 

3) 파생속성

파생속성은 다른 속성에 영향을 받아 발생하는 속성으로서 보통 계산된 값들이 이에 해당된다. 다른 속성에 영향을 받기 때문에 프로세스 설계 시 데이터 정합성을 유지하기 위해 유의해야 할 점이 많으며 가급적 파생속성을 적게 정의하는 것이 좋다.

 

② 구성방식에 따른 분류

엔터티를 식별할 수 있는 속성을 PK(Primary Key)속성, 다른 엔터티와의 관계에서 포함된 속성을 FK(Foreign Key)속성, 엔터티에 포함되어 있고 PK, FK에 포함되지 않은 속성을 일반속성이라 한다.

 

(4) 도메인

각 속성은 가질 수 있는 값의 범위가 있는데 이를 그 속성의 도메인(Domain)이라 한다. 예를 들면 학생이라는 엔터티가 있을 때 학점이라는 속성의 도메인은 0.0에서 4.0 사이의 실수 값이며 주소라는 속성은 길이가 20자리 이내인 문자열로 정의할 수 있다. 여기서 물론 각 속성은 도메인 이외의 값을 갖지 못한다. 따라서 도메인을 좀더 이해하기 쉽게 정리하면, 엔터티 내에서 속성에 대한 데이터타입과 크기 그리고 제약사항을 지정하는 것이라 할 수 있다.

 

04. 관계

(1) 관계란?

관계(Relationship)를 사전적으로 정의하면 상호 연관성이 있는 상태로 말할 수 있다. 이것을 데이터 모델에 대입하여 정의해 보면, “엔터티의 인스턴스 사이의 논리적인 연관성으로서 존재의 형태로서나 행위로서 서로에게 연관성이 부여된 상태”라고 할 수 있다. 관계는 엔터티와 엔터티 간 연관성을 표현하기 때문에 엔터티의 정의에 따라 영향을 받기도 하고, 속성 정의 및 관계 정의에 따라서도 다양하게 변할 수 있다.

 

(2) 관계의 분류

관계가 존재에 의한 관계행위에 의한 관계로 구분될 수 있는 것은 관계를 연결함에 있어 어떤 목적으로 연결되었느냐에 따라 분류하기 때문이다.

 

(3) 표기법

관계에서는 표기법이 상당히 복잡하고 여러 가지 의미를 가지고 있다. 다음 3가지 개념과 함께 표기법을 이해할 필요가 있다.

  • 관계명(Membership) : 관계의 이름
  • 관계차수(Cardinality) : 1:1, 1:M, M:N
  • 관계선택사양(Optionality) : 필수관계, 선택관계

1) 관계명

엔터티와 엔터티가 어떠한 관계를 맺고 있는지를 나타내는 문장, 모든 관계는 두 개의 관계명을 가지고 있는데 이유는 각 엔터티의 관점에서 관계명을 하나씩 가지기 때문이다. 관계명은 반드시 명확한 문장으로 표현해야 하며 현재형이어야 한다.

 

2) 관계차수

각 엔터티에서 관계에 참여하는 수를 의미, 일반적으로 1:1, 1:M, M:N 형식으로 구분할 수 있다.

 

3) 관계 선택사양

관계 선택사양은 이 관계가 필수요소인지 선택사항인지를 나타내는 말이다.

 

05. 식별자

(1) 식별자란?

모든 엔터티는 인스턴스를 가지고 있고, 인스턴스는 속성으로 자신의 특성을 나타낸다고 하였다. 식별자는 이런 속성 중에 각각의 인스턴스를 구분 가능하게 만들어주는 대표 격인 속성을 의미한다.

 

(2) 주식별자

주식별자는 기본키, PK(Primary Key)에 해당하는 속성이다. 하나의 속성이 주식별자가 될 수도 있고, 여러 개의 속성이 주식별자가 될 수도 있다.

출처:&nbsp;Kdata 데이터자격검정 - - www.dataq.or.kr

(3) 분류

식별자를 분류하는 방식은 여러 가지로 나뉜다. 대체적으로 대표성 여부, 스스로 생성되었는지 여부, 단일 속성의 여부, 대체 여부가 분류의 기준이 되는데 각각의 기준대로 나누면 다음과 같이 구분될 수있다.

출처:&nbsp;Kdata 데이터자격검정 - - www.dataq.or.kr

 

(4) 식별자 관계 / 비식별자 관계

① 식별자 관계

부모 엔터티의 식별자가 자식 엔터티의 주식별자가 되는 관계, 주식별자는 반드시 존재해야 하므로(존재성) 부모 엔터티가 있어야 생성 가능하며 단일식별자인지 복합식별자인지에 따라 1:1이거나 1:M이거나가 결정된다.

 

② 비식별자 관계

부모의 엔터티의 식별자가 자식 엔터티의 주식별자가 아닌 일반 속성이 되는 관계, 일반 속성의 속성값은 Null이 될 수 있으므로 부모 엔터티가 없는 자식 엔터티 생성이 가능하고 마찬가지의 이유로 자식 엔터티가 존재하는 상태에서 부모 엔터티가 삭제될 수도 있다.

반응형
반응형

이번에는 페이징 처리를 적용한 게시판을 만들어보겠습니다.

 

우선 DeptList 라는 페이지를 생성하고 변수를 정의합니다.

 

1) 부서 배열을 담을 변수 정의

const [dept, setDept] = useState<Array<IDept>>([]);

2) 상단 검색어 값을 담을 변수 정의

const [searchDname, setSearchDname] = useState<string>("");

3) 공통 변수 정의 : page(현재페이지 번호), count(총 페이지 건수), pageSize(3, 6, 9 배열)

  const [page, setPage] = useState<number>(1);
  const [count, setCount] = useState<number>(1);
  const [pageSize, setPageSize] = useState<number>(3); // 한 페이지당 개수

4) pageSizes 배열 정의 => selectbox에 3, 6, 9 선택 옵션

 const pageSizes = [3, 6, 9];

 

변수를 정의하였다면 게시판에 사용될 함수를 정의합니다.

 

1) useEffect를 사용하여 컴포넌트의 변수값이 변할 때 실행되는 함수 정의

  // TODO: 1) 컴포넌트가 mounted 될때 한번만 실행됨 : useEffect(() => {},[])
  // TODO: 2) 컴포넌트의 변수값이 변할 때 실행됨 : useEffect(() => {실행문},[감시할 변수])
  useEffect(() => {
    retrieveDept(); // 전체 조회
  }, [page, pageSize]);

2) 전체 조회 함수 정의

// 전체 조회 함수
  const retrieveDept = () => {
    // 벡엔드 매개변수 전송 : 현재페이지(page), 1페이지당 개수(pageSize)
    DeptService.getAll(searchDname, page -1, pageSize)  // 벡엔드 전체조회요청
    .then((response:any)=>{
      // 벡엔드 성공시 실행됨
      // es6(모던js) 문법 : 객체 분해 할당
      // 원래 코드
      // const dept =  response.data.dept;  // 부서배열
      // const totalPage = response.data.totalPages;  // 전체페이지수
      const { dept, totalPages } = response.data;
      // dept 저장
      setDept(dept);
      setCount(totalPages);
      // 로그 출력
      console.log("response", response.data);


    })
    .catch((e:Error)=>{
      // 벡엔드 실패시 실행됨
      console.log(e);
    })
  };

3) 검색어 수동 바인딩 함수 정의

  // 검색어 수동 바인딩 함수
  const onChangeSearchDname = (e: React.ChangeEvent<HTMLInputElement>) => {
    const searchDname = e.target.value;
    setSearchDname(searchDname);
  };

4) OnChange event 함수 정의

// handlePageSizeChange : pageSize 값 변경시 실행되는 함수
  // 수동바인딩 : 화면 값을 변수에 저장하는 것
  const handlePageSizeChange = (e: any) => { 
      setPageSize(e.target.value);  // 1 페이지당 개수 저장(3, 6, 9)
      setPage(1); // 현재페이지 번호 : 1로 강제설정


   }
   
   // todo Pagination 수동바인딩(공통)
  // page 번호를 누르면 => page 변수에 값 저장
  const handlePageChange = (e:any, value:number) => {
    // value == 화면의 페이지번호
    setPage(value);

  };

✅ 화면 확인

// 벡엔드 작업

벡엔드 프로젝트를 인텔리제이에서 생성

build.gradle 파일에 추가

//    오라클 라이브러리( 19c )
implementation 'com.oracle.database.jdbc:ucp:19.14.0.0'
implementation 'com.oracle.database.security:oraclepki:19.14.0.0'
implementation 'com.oracle.database.security:osdt_cert:19.14.0.0'
implementation 'com.oracle.database.security:osdt_core:19.14.0.0'
//    logback , log4jdbc 설정
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'org.slf4j:jcl-over-slf4j:1.7.36'

application.properties 파일 추가

# 서버 포트
server.port=8000

# 오라클 설정 : log4j 적용 : 도커
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:1521/xepdb1
spring.datasource.username=scott
spring.datasource.password=!Ds1234567890

## 오라클 설정 ( 오라클 클라우드 전자지갑 설정 ) : log4j 적용
#spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
## jdbc:log4jdbc:oracle:thin:@orcl_medium?TNS_ADMIN=전자지갑경로
#spring.datasource.url=jdbc:log4jdbc:oracle:thin:@orcl_low?TNS_ADMIN=전자지갑경로
#spring.datasource.username=scott
#spring.datasource.password=!Ds1234567890

# jpa 설정
spring.jpa.hibernate.ddl-auto=none
#spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
spring.jpa.show-sql=true
# sql log 찍기
spring.jpa.properties.hibernate.format_sql=true
#Logging Setting , hibernate info 레벨 로깅 설정 : debug, trace 등
logging.level.org.hibernate=info
# batch size 설정 : 연관관계 설정 시 N+1 문제 최소화
#  여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어줍
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
# 1) resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.jpa.defer-datasource-initialization=true
# 2)  resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.sql.init.mode=always
# sql 에러 무시하고 스프링 서버 로딩
spring.sql.init.continue-on-error=true

# 자바 소스 변경시 스프링 서버 자동 재시작
spring.devtools.restart.enabled=true

# todo :: HikariCP settings : DB 커넥션 풀(기본 10개) -> 기본 1개로 제한
spring.datasource.hikari.minimumIdle=1
spring.datasource.hikari.maximumPoolSize=1
spring.datasource.hikari.poolName=HikariPoolBooks

# todo :: file upload 최대 size 설정
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

 

프로젝트 폴더 구성

// CORS 보안 허용 설정 파일 -> config 폴더에 저장

package com.example.simpledms.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * packageName : com.example.dongsungsi.controller
 * fileName : WebConfig
 * author : kangtaegyung
 * date : 2022/06/14
 * description :
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/06/14         kangtaegyung          최초 생성
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
//                아래 url 허용
                .allowedOrigins("http://localhost:3000")
//                .allowedOrigins("http://192.168.35.192:3000/")
//                Todo: 아래 추가해야 update, delete, insert, select 가 cors 문제가 안생김
                .allowedMethods(
                        HttpMethod.GET.name(),
                        HttpMethod.POST.name(),
                        HttpMethod.PUT.name(),
                        HttpMethod.DELETE.name(),
                        HttpMethod.PATCH.name()
                );
    }
}

// 공통 entity 인 BaseTimeEntity 추가

package com.example.simpledms.model.common;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.SQLDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * packageName : com.example.jpaexam.model
 * fileName : BaseTimeEntity
 * author : kangtaegyung
 * date : 2022/10/16
 * description : JPA 에서 자동으로 생성일자/수정일자 만들어주는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/16         kangtaegyung          최초 생성
 */
@Getter
@Setter
// @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우
// 필드들(createdDate, modifiedDate)도 칼럼으로 인식하도록 한다.
@MappedSuperclass
// @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에
// Auditing 기능을(자동 생성일, 수정일) 포함시킨다.
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    private String insertTime;

    private String updateTime;

    private String deleteYn;

    private String deleteTime;

    @PrePersist
        //해당 엔티티 저장하기 전
    void onPrePersist(){
        this.insertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.deleteYn = "N";
    }

    @PreUpdate
        //해당 엔티티 수정 하기 전
    void onPreUpdate(){
        this.updateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.insertTime = this.updateTime;
        this.deleteYn = "N";
    }
}

// Dept entity 생성

package com.example.simpledms.model.entity.basic;

import com.example.simpledms.model.common.BaseTimeEntity;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

/**
 * packageName : com.example.modelexam.model
 * fileName : Dept
 * author : kangtaegyung
 * date : 2022/10/12
 * description : 부서 모델 클래스
 * 요약 :
 * Soft Delete는 물리적인 데이터 삭제로 발생할 수 있는 문제를 방지하고 쉽게 복원할 필요가 있거나 삭제된 데이터들을 보관하여 데이터로써 활용할 필요나 가치가 있는 경우에 사용
 * 실무에서는 법적으로 개인자료일 경우 3년 또는 1년이상 데이터를 보관할 의무가 있고 어길수 법적 문제가 생길 수 있음 -> 그래서 soft delete 방식을 대부분 구현하고 있음
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/12         kangtaegyung          최초 생성
 */
@Entity
@Table(name="TB_DEPT")
@SequenceGenerator(
        name = "SQ_DEPT_GENERATOR"
        , sequenceName = "SQ_DEPT"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_DEPT SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE DNO = ?")
public class Dept extends BaseTimeEntity {
    //    부서넘버
//    @Id : Primary Key 에 해당
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "SQ_DEPT_GENERATOR"
    )
    private Integer dno;

    //    부서이름
    private String dname;

    //    부서위치
    private String loc;
}

// DeptRepository 생성

package com.example.simpledms.repository.basic;

import com.example.simpledms.model.entity.basic.Dept;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * packageName : com.example.simpledms.repository.basic
 * fileName : DeptRepository
 * author : GGG
 * date : 2023-10-23
 * description : JPA CRUD 인터페이스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-23         GGG          최초 생성
 */
@Repository
public interface DeptRepository extends JpaRepository<Dept, Integer> {
    // dname like 검색 : 쿼리메소드 + 페이징(리턴: Page, 매개변수: Pageable) 처리
    Page<Dept> findAllByDnameContaining(String dname, Pageable pageable);

}

//DeptService 생성

package com.example.simpledms.service.basic;

import com.example.simpledms.model.entity.basic.Dept;
import com.example.simpledms.repository.basic.DeptRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

/**
 * packageName : com.example.simpledms.service.basic
 * fileName : DeptService
 * author : GGG
 * date : 2023-10-23
 * description : 부서 서비스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-23         GGG          최초 생성
 */
@Service
public class DeptService {

    @Autowired
    DeptRepository deptRepository;  // DI

    //    전체조회
    public Page<Dept> findAll(Pageable pageable) {
        Page<Dept> page = deptRepository.findAll(pageable);
        return page;

    }


    //    dname like 조회 + paging
    public Page<Dept> findAllByDnameContaining(String dname, Pageable pageable) {
        Page<Dept> page
                = deptRepository.findAllByDnameContaining(dname, pageable);
        return page;

    }


}

// DeptController 생성

package com.example.simpledms.controller.basic;

import com.example.simpledms.model.entity.basic.Dept;
import com.example.simpledms.service.basic.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * packageName : com.example.simpledms.controller.basic
 * fileName : DeptController
 * author : GGG
 * date : 2023-10-23
 * description : 부서 컨트롤러
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-23         GGG          최초 생성
 */
@RestController
@Slf4j
@RequestMapping("/api/basic")
public class DeptController {

    @Autowired
    DeptService deptService;    // DI

    // 전체 조회 + dname like 검색
    @GetMapping("/dept")
    public ResponseEntity<Object> findAllByDnameContaining(
            @RequestParam(defaultValue = "") String dname,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "3") int size

    ) {
        try {
//            페이지 변수 저장 (page:현재페이지 번호, size: 한 페이지당 개수)
//            함수 매개변수 : Pageable(위의 값을 넣기)
//            사용법 : Pageable pageable = PageRequest.of(현재페이지번호, 한페이지당개수);
            Pageable pageable = PageRequest.of(page, size);

//            전체조회(dname="") + like 검색(dname="S")
            Page<Dept> deptPage
                    = deptService.findAllByDnameContaining(dname, pageable);

//            리액트 전송 : 부서배열, 페이징 정보 [자료구조 : Map<키이름, 값>]
            Map<String, Object> response = new HashMap<>();
            response.put("dept", deptPage.getContent());                // 부서배열 전송
            response.put("currentPage", deptPage.getNumber());          // 현재페이지번호 전송
            response.put("totalItems", deptPage.getTotalElements());    // 총 건수(개수) 전송
            response.put("totalPages", deptPage.getTotalPages());       // 총 페이지수 전송

//            신호 보내기
            if (deptPage.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
            


        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }



}

// 서버 시작 후 전체조회 controller API 테스트

GET http://localhost:8000/api/basic/dept

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 23 Oct 2023 04:21:59 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "totalItems": 4,
  "totalPages": 2,
  "dept": [
    {
      "insertTime": "2023-10-23 03:46:21",
      "updateTime": null,
      "deleteYn": "N",
      "deleteTime": null,
      "dno": 50,
      "dname": "ACCOUNTING",
      "loc": "NEW YORK"
    },
    {
      "insertTime": "2023-10-23 03:46:21",
      "updateTime": null,
      "deleteYn": "N",
      "deleteTime": null,
      "dno": 60,
      "dname": "RESEARCH",
      "loc": "DALLAS"
    },
    {
      "insertTime": "2023-10-23 03:46:21",
      "updateTime": null,
      "deleteYn": "N",
      "deleteTime": null,
      "dno": 70,
      "dname": "SALES",
      "loc": "CHICAGO"
    }
  ],
  "currentPage": 0
}
응답 파일이 저장되었습니다.
> 2023-10-23T132159.200.json

Response code: 200; Time: 31ms (31 ms); Content length: 451 bytes (451 B)

 

// 프론트 + 벡엔드 동시 실행 후 화면 출력 테스트

반응형

'Spring Boot > 스프링부트 예제' 카테고리의 다른 글

답변형 게시판 만들기 (1)  (1) 2023.10.26
QnA 다양한 검색 게시판 CRUD (1)  (1) 2023.10.24
front + backend 게시판 CRUD 구현 (2)  (0) 2023.10.20
JPA - 연관관계 매핑  (0) 2023.10.19
JPA 페이징 처리  (1) 2023.10.18
반응형

https://devjunyeok.tistory.com/186#3)%20%EB%B6%80%EC%84%9C(Department)%20%EA%B2%8C%EC%8B%9C%ED%8C%90%20%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0-1

 

front + backend 게시판 CRUD 구현 (1)

안녕하세요 ! :) front + backend 를 통합한 게시판을 생성하는 예제를 만들어보겠습니다. 📖 frontend작업 프론트 작업은 VSCode를 이용하여 프로젝트를 생성하였습니다. frontend 프로젝트는 React + typescri

devjunyeok.tistory.com

지난시간에 이어 이번에는 부서 게시판 기능 중 부서 추가 페이지를 생성해보겠습니다 :)

 

먼저 프론트 프로젝트 작업부터 시작하겠습니다.

 

📖 프론트 작업

1) pages > dept-nop > AddDeptNop.tsx 파일 생성

이 페이지는 부서를 추가하는 기능을 하는 페이지 입니다. 그래서 번저 함수 안에 변수를 정의합니다.

// todo ) 새 부서(객체 1개) 저장 페이지
function AddDeptNop() {
    // 변수정의
    // todo : 초기화 객체
    const initialDept = {
        dno: null,
        dname: "",
        loc: "",
    }

그리고 부서를 담을 변수와 저장하면 바뀌는 변수 (true/false)를 선언합니다.

// todo 바인딩 변수
    // todo 새부서 객체 변수
    const [dept, setDept] = useState<IDept>(initialDept);
    // todo 저장하면 true, 아니면 false 인 변수(값에따라 화면바뀜)
    const [submitted, setSubmitted] = useState<boolean>(false);

그리고 아래에 함수를 정의합니다.

함수는 새로운 입력 폼을 보여주는 함수와 그리고 각 입력창에 수동 바인딩 공통함수와 저장함수를 작성합니다.

 

  // 함수정의
    // todo 새로운 폼(form)을 보여주는 함수
    const newDept = () => { 
        // 새 폼 == 객체 초기화, submitted 변수 초기화(false)
        setDept(initialDept);   // 객체 초기화
        setSubmitted(false);   
     }
     // todo 각각의 입력창 수동 바인딩 공통함수
     const handleInputChange = (e:React.ChangeEvent<HTMLInputElement>) => { 
        const { name, value } = e.target;   // 화면 값[이름]
        // 화면 값을 Dept 객체의 속성에 저장
        setDept({...dept, [name]: value});

      }
      // todo 저장 함수
      const saveDept = () => { 
        // 임시 부서 변수(저장될 객체)
        var data = {
            dname: dept.dname,
            loc: dept.loc
        }
        // 저장 함수 호출
        DeptService.create(data)   // 벡엔드로 저장 요청
        .then((response:any)=>{
            // 저장 성공유무 -> submitted 변수에 값을 true로 변경
            setSubmitted(true); // 화면 변경
            console.log(response.date);
        })
        .catch((e:Error)=>{
            console.log(e);
        })
       }

그리고 리턴 값 아래의 HTML 작성은 다음과 같이 하였습니다.

return (
    <div className="row">
      {submitted ? (
        // 저장버튼 클릭하면 아래 화면이 보임
        <div className="col-6 mx-auto">
          <h4>You submitted successfully!</h4>
          {/* Add 버튼 클릭하면 다시 새로운 부서 저장 페이지로 이동(새폼 보이기) */}
          <button className="btn btn-success" onClick={newDept}>
            Add
          </button>
        </div>
      ) : (
        <>
          {/* 제목 start */}
          <TitleCom title="Add Dept No Page" />
          {/* 제목 end */}

          <div className="col-6 mx-auto">
            {/* 부서명 입력창 */}
            <div className="row g-3 align-items-center mb-3">
              <div className="col-3">
                <label htmlFor="dname" className="col-form-label">
                  Dname
                </label>
              </div>

              <div className="col-9">
                <input
                  type="text"
                  id="dname"
                  required
                  className="form-control"
                  value={dept.dname}
                  onChange={handleInputChange}
                  placeholder="dname"
                  name="dname"
                />
              </div>
            </div>
            {/* 부서명 입력창 끝*/}
            {/* 부서위치 입력창 시작 */}
            <div className="row g-3 align-items-center mb-3">
              <div className="col-3">
                <label htmlFor="loc" className="col-form-label">
                  Loc
                </label>
              </div>
              <div className="col-9">
                <input
                  type="text"
                  id="loc"
                  required
                  className="form-control"
                  value={dept.loc}
                  onChange={handleInputChange}
                  placeholder="loc"
                  name="loc"
                />
              </div>
            </div>
            {/* 부서위치 입력창 끝 */}

            {/* 저장버튼 시작 */}
            <div className="row g-3 mt-3 mb-3">
              <button
                onClick={saveDept}
                className="btn btn-outline-primary ms-2 col"
              >
                Submit
              </button>
            </div>
            {/* 저장버튼 끝 */}
          </div>
          
        </>
      )}
    </div>
  );
}

export default AddDeptNop;

App.tsx 파일의 AddDeptNop 페이지 라우트 처리

 {/* dept */}
          <Route path="/dept-nop" element={<DeptListNop />} />
          <Route path="/add-dept-nop" element={<AddDeptNop />} />

📖 벡엔드 작업

1) service > DeptService.java 파일에 전체조회, 검색어 조회 함수를 작성합니다.

@Service
public class DeptService {
    @Autowired
    DeptRepository deptRepository;  // DI

    /** 전체조회 */
    public List<Dept> findAll(){
        List<Dept> list = deptRepository.findAll();
        return list;
    }

    /** 검색어(dname like) 조회 함수 */
    public List<Dept> findAllByDnameContaining(String dname){
        List<Dept> list = deptRepository.findAllByDnameContaining(dname);
        return list;
    }

2) controller > DeptController.java 파일에 전체 조회 함수를 작성합니다.

@Slf4j
@RestController
@RequestMapping("/api")
public class DeptController {
    @Autowired
    DeptService deptService;    // DI

    /**
     * 전체 조회 + like 검색
     */
    @GetMapping("/dept")
    public ResponseEntity<Object> getDeptAll(
            @RequestParam(defaultValue = "") String dname) {
        try {
            // 전체 조회 + like 검색
            List<Dept> list = deptService.findAllByDnameContaining(dname);
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

3) API 테스트 - 아래 화면과 같이 GET 메서드로 테스트 합니다.

결과

더보기
GET http://localhost:8000/api/dept

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 20 Oct 2023 07:42:35 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 50,
    "dname": "ACCOUNTING",
    "loc": "NEW YORK"
  },
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 60,
    "dname": "RESEARCH",
    "loc": "DALLAS"
  },
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 70,
    "dname": "SALES",
    "loc": "CHICAGO"
  },
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 80,
    "dname": "OPERATIONS",
    "loc": "BOSTON"
  }
]
응답 파일이 저장되었습니다.
> 2023-10-20T164235.200.json

Response code: 200; Time: 234ms (234 ms); Content length: 529 bytes (529 B)

4) 프론트엔드 + 벡엔드 연동 확인

프론트 엔드 서버와 벡엔드 서버를 재시작하여 전체 조회 기능을 확인합니다.

반응형

'Spring Boot > 스프링부트 예제' 카테고리의 다른 글

QnA 다양한 검색 게시판 CRUD (1)  (1) 2023.10.24
게시판 페이징 처리  (1) 2023.10.23
JPA - 연관관계 매핑  (0) 2023.10.19
JPA 페이징 처리  (1) 2023.10.18
JPA - JPQL(Java Persistence Query Language)  (1) 2023.10.17
반응형

안녕하세요 ! :)

front + backend 를 통합한 게시판을 생성하는 예제를 만들어보겠습니다.

 

📖 frontend작업

프론트 작업은 VSCode를 이용하여 프로젝트를 생성하였습니다.

frontend 프로젝트는 React + typescript를 이용하였습니다.

프로젝트 형태는 다음과 같습니다.

src 폴더 아래에는 다음과 같은 파일들이 들어갑니다.

  • assets(css파일, 이미지파일, javascript 파일)
  • compoents(common(헤더, 푸터, 타이틀))
  • page(auth(로그인관련), 각종 메뉴 페이지들)
  • common(공통으로 사용할 페이지 관련)
  • service(벡엔드에 요청할 서비스 함수작성 파일)
  • types(자바의 모델과 같은 파일)
  • utils(백앤드 서버와 연동) 파일을 생성하였습니다.

1) 패키지 설치

# 설치 패키지 
# 1) 메뉴 라이브러리 설치
npm i react-router-dom

# 2) 벡엔드 연동 라이브러리 설치 : ajax 업그레이드
npm i axios

# 3) pre css 컴파일러 : node-sass -> 더이상 안씀 : sass 설치할것
<!-- npm i node-sass -->
npm install sass
# 4) Material Page component 업그레이드 
# 과거 v4 -> v5 변경 설치
npm i @mui/material @emotion/react @emotion/styled

# 4-1) 소스에서 임포트 사용법 : <Pagination />
import Pagination from '@mui/material/Pagination';

# 5) typescript jquery, jqueryui type 넣기
# 5-1) typescript jquery 사용
npm i --save-dev @types/jquery

2) axios 설정

📂 utils > http-commons.ts

이곳에서는 axios를 이용하여 벡엔드와 연동하는 작업을 하는 곳입니다.

import axios from "axios";

// todo : baseURL: "http://스프링ip주소:스프링포트번호/공통url"
export default axios.create({
  baseURL: "http://localhost:8000/api",
  headers: {
    "Content-Type": "application/json"
  }
});

base url에는 스프링부트주소를 입력합니다. 주소뒤에는 공통url인 api로 지정하였습니다.

 

3) 부서(Department) 게시판 생성하기

1) 부서 인터페이스 생성

📂 types > IDept.ts 생성

// 자바의 모델 클래스와 같음
// 인터페이스 : 속성의 자료형을 미리 지정하는 것
export default interface IDept {
    dno?: any | null,
    dname: string,
    loc: string
}

2) 부서 서비스 생성

📂 service > DeptService.ts 생성

// DeptService.ts
// axios 통신을 위한 import 작업
import http from "../utils/http-common";
// type 인터페이스 import
import IDept from "../types/IDept";

// 화살표 함수 단축키 : nfn
/** 전체 조회 요청 함수 */
const getAll = () => {
  // 조회요청 : .get("/url")
  // 사용법 : http.get<리턴타입>("스프링부트의 컨트롤러url");
  return http.get<Array<IDept>>("/dept");
};

/** 상세 조회(1건조회) 요청 함수 : 기본키 */
//  매개변수로는 부서번호(dno) type는 any
const get = (dno: any) => {
  return http.get<IDept>(`/dept/${dno}`);
};

/** 저장 요청 함수 */
const create = (data: IDept) => {
  return http.post<IDept>("/dept", data);
};

/** 수정 요청 함수 : 기본키와 객체 필요 */
const update = (dno: any, data: IDept) => {
  return http.put<any>(`/dept/${dno}`, data);
};

/** 삭제 요청 함수 : 기본키(dno) */
const remove = (dno: any) => {
  return http.delete<any>(`/dept/deletion/${dno}`);
};

/** 부서명 검색 요청 함수 */
const findByDname = (dname: string) => {
  return http.get<Array<IDept>>(`/dept?dname=${dname}`);
};


// 함수 기능 내보내기
const DeptService = {
    getAll,
    get,
    create,
    update,
    remove,
    findByDname
}

export default DeptService;

3) 부서페이지 생성

📂 page> DeptListNop.tsx 생성

// DeptListNop.tsx

import React from 'react'

function DeptListNop() {
  return (
    <div>DeptListNop</div>
  )
}

export default DeptListNop

3-1) App.ts 페이지 라우터 처리

import React from "react";
// app css import
import "./assets/css/app.css";

import HeaderCom from "./components/common/HeaderCom";
import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/auth/Login";
import Register from "./pages/auth/Register";
import ForgotPassword from "./pages/auth/ForgotPassword";
import NotFound from "./pages/common/NotFound";
import DeptListNop from "./pages/dept-nop/DeptListNop";
import EmpListNop from "./pages/emp-nop/EmpListNop";

function App() {
  return (
    <div className="App">
      <HeaderCom />

      {/* <!-- 구분 막대 시작 --> */}
      <div className="gutter text-center text-muted fade-in-box">
        <div>클론 코딩 예제 사이트에 오신 것을 환영합니다.</div>
      </div>
      {/* <!-- 구분 막대 끝 --> */}

      <div id="content-wrapper">
        {/* 라우터 정의 시작 */}
        <Routes>
          {/* login */}
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />
          <Route path="/forgot-password" element={<ForgotPassword />} />

          {/* dept */}
          <Route path="/dept-nop" element={<DeptListNop />} />

          {/* emp */}
          <Route path="/emp-nop" element={<EmpListNop />} />

          {/* NotFound */}
          <Route path="*" element={<NotFound />} />
        </Routes>
        {/* 라우터 정의 끝 */}
      </div>
    </div>
  );
}

export default App;

4) 서버 시작

5) 리스트 페이지 변수 및 함수 정의

// DeptListNop.tsx

import React, { useEffect, useState } from "react";
import TitleCom from "../../components/common/TitleCom";
import { Link } from "react-router-dom";
import IDept from "../../types/IDept";
import DeptService from "./../../services/DeptService";

function DeptListNop() {
  // 변수 정의
  // todo: 부서 배열 변수
  const [dept, setDept] = useState<Array<IDept>>([]);
  // todo: 검색어 변수
  const [searchDname, setSearchDname] = useState<string>("");

  // 함수 정의
  // todo: useEffect : 화면이 뜨자마자 실행되는 이벤트 함수(1번)
  // 사용법 : useEffect(()=>{실행문},[])
  useEffect(() => {
    // 전체 조회 실행
    retrieveDept();
  }, []);

  // todo) 검색어 수동 바인딩 함수
  const onChangeSearchDname = (e: any) => {
    // todo) e.target : input 태그에 현재 걸린 이벤트
    // => e.target.value : 현재 조작하는 태그의 value 값
    setSearchDname(e.target.value);
  };

  // todo) 전체 조회 함수
  const retrieveDept = () => {
    DeptService.getAll() // backend에 전체조회요청
      .then((response: any) => {
        // todo 성공했을때 처리
        setDept(response.data);
        // todo 로그 찍기
        console.log("response", response.data);
      })
      .catch((e: Error) => {
        // todo 실패했을때 처리
        console.log(e);
      });
  };

  // todo) 검색어 조회 함수
  const findByDname = () => {
    DeptService.findByDname(searchDname) // backend 요청
      .then((response: any) => {
        // todo 성공했을때 처리
        setDept(response.data);
        // todo 로그 찍기
        console.log("response", response.data);
      })
      .catch((e: Error) => {
        // todo 실패했을때 처리
        console.log(e);
      });
  };

  return (
    <>
      {/* 제목 보여주는 공통 컴포넌트 */}
      <TitleCom title="Dept List No Page" />
      {/* 제목 end */}

      {/* dname start(검색창) */}
      <div className="row mb-5 justify-content-center">
        {/* w-50 : 크기 조정, mx-auto : 중앙정렬(margin: 0 auto), justify-content-center */}
        <div className="col-12 w-50 input-group mb-3">
          {/* 입력창 시작 */}
          <input
            type="text"
            className="form-control"
            placeholder="Search by dname"
            value={searchDname}
            onChange={onChangeSearchDname}
          />
          {/* 입력창 끝 */}

          {/* 검색버튼 시작 */}
          <div className="input-group-append">
            <button
              className="btn btn-outline-secondary"
              type="button"
              onClick={findByDname}
            >
              Search
            </button>
          </div>
          {/* 검색버튼 끝 */}
        </div>
      </div>
      {/* dname end */}

      {/* table start(본문) */}
      <div className="col-md-12">
        {/* table start */}
        <table className="table">
          {/* 테이블 제목 시작 */}
          <thead className="table-light">
            <tr>
              <th scope="col">Dname</th>
              <th scope="col">Loc</th>
              <th scope="col">Actions</th>
            </tr>
          </thead>
          {/* 테이블 제목 끝 */}
          {/* TODO: 테이블 데이터 시작 */}
          <tbody>
            {dept &&
              dept.map((data) => (
                <tr key={data.dno}>
                  <td>{data.dname}</td>
                  <td>{data.loc}</td>
                  <td>
                    <Link to={"/dept-nop/" + data.dno}>
                      <span className="badge bg-success">Edit</span>
                    </Link>
                  </td>
                </tr>
              ))}
          </tbody>
          {/* 테이블 데이터 끝 */}
        </table>
        {/* table end */}
      </div>
      {/* table end */}
    </>
  );
}

export default DeptListNop;

6) 서버 재시작 및 결과 확인

📖 backend 작업

frontend 작업 후 backend 작업을 해봅시다 :)

backend 작업은 인텔리제이에서 프로젝트를 생성하였습니다.

 

1) backend 프로젝트 생성

프로젝트 이름은 SimpleDMS로 정하고 위치는 프론트엔드 작업물과 같은 폴더 위치로 지정합니다.

JDK는 azul-11 버전, Java 버전은 11버전으로 하고 패키지 생성은 War 파일로 선택하였습니다.

 

스프링부트 버전은 2.7.16 으로 하고 종속성 추가는

  1. Spring Boot DevTools
  2. Lombok
  3. Spring Web
  4. Spring Data JPA
  5. Oracle Driver

를 선택후 프로젝트를 생성합니다.

 

2) backend 프로젝트 환경설정

2-1) build.gradle 파일에 dependencies 추가 

//    오라클 라이브러리( 19c )
implementation 'com.oracle.database.jdbc:ucp:19.14.0.0'
implementation 'com.oracle.database.security:oraclepki:19.14.0.0'
implementation 'com.oracle.database.security:osdt_cert:19.14.0.0'
implementation 'com.oracle.database.security:osdt_core:19.14.0.0'
//    logback , log4jdbc 설정
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'org.slf4j:jcl-over-slf4j:1.7.36'

2-2) 프로퍼티 파일 한글 깨짐 방지를 위한 설정

설정 > 에디터 > 파일 인코딩 > 프로퍼티 파일에 대한 디폴트 인코딩을 UTF-8로 저장 후 아래 체크박스를 체크한후 확인

2-3) 프로퍼티 파일 설정

main > resources > application.properties 파일에 설정을 추가합니다.

# 서버 포트
server.port=8000

# 오라클 설정 : log4j 적용
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:오라클서버포트/db명
spring.datasource.username=서버id
spring.datasource.password=서버패스워드

# jpa 설정
spring.jpa.hibernate.ddl-auto=none
#spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
spring.jpa.show-sql=true
# sql log 찍기
spring.jpa.properties.hibernate.format_sql=true
#Logging Setting , hibernate info 레벨 로깅 설정 : debug, trace 등
logging.level.org.hibernate=info
# batch size 설정 : 연관관계 설정 시 N+1 문제 최소화
#  여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어줌
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
# 1) resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.jpa.defer-datasource-initialization=true
# 2)  resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.sql.init.mode=always
# sql 에러 무시하고 스프링 서버 로딩
spring.sql.init.continue-on-error=true

# 자바 소스 변경시 스프링 서버 자동 재시작
spring.devtools.restart.enabled=true

# HikariCP settings
spring.datasource.hikari.minimumIdle=1
spring.datasource.hikari.maximumPoolSize=1
spring.datasource.hikari.poolName=HikariPoolBooks

 

2-4) 실습용 스크립트 logback 추가

실습용 스크립트 파일 및 logback 파일을 resources 파일 아래에 추가합니다.

1. data.sql

더보기
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'ACCOUNTING', 'NEW YORK','N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'RESEARCH', 'DALLAS', 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'SALES', 'CHICAGO', 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'OPERATIONS', 'BOSTON', 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);

INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'SMITH', 'CLERK', 7902, TO_CHAR(to_date('17-12-1980', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 800, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'ALLEN', 'SALESMAN', 7698, TO_CHAR(to_date('20-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1600,
        300, 30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'WARD', 'SALESMAN', 7698, TO_CHAR(to_date('22-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1250, 500,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'JONES', 'MANAGER', 7839, TO_CHAR(to_date('2-4-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 2975, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'MARTIN', 'SALESMAN', 7698, TO_CHAR(to_date('28-9-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1250,
        1400, 30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'BLAKE', 'MANAGER', 7839, TO_CHAR(to_date('1-5-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 2850, NULL,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'CLARK', 'MANAGER', 7839, TO_CHAR(to_date('9-6-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 2450, NULL,
        10, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'SCOTT', 'ANALYST', 7566, TO_CHAR(to_date('13-07-1987', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 3000,
        NULL, 20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'KING', 'PRESIDENT', NULL, TO_CHAR(to_date('17-11-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 5000,
        NULL, 10, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'TURNER', 'SALESMAN', 7698, TO_CHAR(to_date('8-9-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1500, 0,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'ADAMS', 'CLERK', 7788, TO_CHAR(to_date('13-07-1987', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1100, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'JAMES', 'CLERK', 7698, TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 950, NULL,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'FORD', 'ANALYST', 7566, TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 3000, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'MILLER', 'CLERK', 7782, TO_CHAR(to_date('23-1-1982', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1300, NULL,
        10, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);

COMMIT;

 

2. log4j 프로퍼티 파일

더보기
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

3. logback xml 파일

더보기
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

	<!-- Appenders -->
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<Pattern>%d %5p [%c] %m%n</Pattern>
		</encoder>
	</appender>

	<appender name="console-infolog" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<Pattern>%d %5p %m%n</Pattern>
		</encoder>
	</appender>

	<!-- Logger -->
	<logger name="com.example.simpledms" level="DEBUG" appender-ref="console" />
	<logger name="jdbc.sqlonly" level="INFO" appender-ref="console-infolog" />
	<logger name="jdbc.resultsettable" level="INFO" appender-ref="console-infolog" />

	<!-- Root Logger -->
	<root level="off">
		<appender-ref ref="console" />
	</root>
</configuration>

4. schema.sql 파일

더보기
DROP SEQUENCE SQ_DEPT;
CREATE SEQUENCE SQ_DEPT START WITH 50 INCREMENT BY 10;

DROP SEQUENCE SQ_EMP;
CREATE SEQUENCE SQ_EMP START WITH 8000 INCREMENT BY 1;

DROP TABLE TB_EMP CASCADE CONSTRAINT;
DROP TABLE TB_DEPT CASCADE CONSTRAINT;

CREATE TABLE TB_DEPT
(
    DNO         NUMBER NOT NULL PRIMARY KEY,
    DNAME       VARCHAR2(255),
    LOC         VARCHAR2(255),
    DELETE_YN   VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME VARCHAR2(255),
    UPDATE_TIME VARCHAR2(255),
    DELETE_TIME VARCHAR2(255)
);

CREATE TABLE TB_EMP
(
    ENO         NUMBER NOT NULL PRIMARY KEY,
    ENAME       VARCHAR2(255),
    JOB         VARCHAR2(255),
    MANAGER     NUMBER,
    HIREDATE    VARCHAR2(255),
    SALARY      NUMBER,
    COMMISSION  NUMBER,
    DNO         NUMBER,
    DELETE_YN   VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME VARCHAR2(255),
    UPDATE_TIME VARCHAR2(255),
    DELETE_TIME VARCHAR2(255)
);

2-5) com.example.simpledms > 패키지 폴더 생성

아래와 같이 설정, 컨트롤러, 모델, 레포지토리, 서비스 폴더를 생성합니다.

 

3) 파일 생성

3-1) model > BaseTimeEntity.java 생성

📂 model > BaseTimeEntity.java

더보기
package com.example.simpledms.model;

import lombok.Getter;
import org.hibernate.annotations.SQLDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * packageName : com.example.jpaexam.model
 * fileName : BaseTimeEntity
 * author : kangtaegyung
 * date : 2022/10/16
 * description : JPA 에서 자동으로 생성일자/수정일자 만들어주는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/16         kangtaegyung          최초 생성
 */
@Getter
// @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우
// 필드들(createdDate, modifiedDate)도 칼럼으로 인식하도록 한다.
@MappedSuperclass
// @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에
// Auditing 기능을(자동 생성일, 수정일) 포함시킨다.
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    private String insertTime;

    private String updateTime;

    private String deleteYn;        // 삭제여부

    private String deleteTime;      // 삭제시간

    @PrePersist
        //해당 엔티티 저장하기 전
    void onPrePersist(){
        this.insertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.deleteYn = "N";
    }

    @PreUpdate
        //해당 엔티티 수정 하기 전
    void onPreUpdate(){
        this.updateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.insertTime = this.updateTime;
        this.deleteYn = "N";
    }
}

3-2) model > Dept/Emp.java 생성

📂 Dept.java

더보기
package com.example.simpledms.model;

import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

/**
 * packageName : com.example.modelexam.model
 * fileName : Dept
 * author : kangtaegyung
 * date : 2022/10/12
 * description : 부서 모델 클래스
 * 요약 :
 * Hard Delete : 실제 데이터를 삭제하는 행위
 * Soft Delete는 물리적인 데이터 삭제로 발생할 수 있는 문제를 방지하고 쉽게 복원할 필요가 있거나 삭제된 데이터들을 보관하여 데이터로써 활용할 필요나 가치가 있는 경우에 사용
 * 실무에서는 법적으로 개인자료일 경우 3년 또는 1년이상 데이터를 보관할 의무가 있고 어길수 법적 문제가 생길 수 있음 -> 그래서 soft delete 방식을 대부분 구현하고 있음
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/12         kangtaegyung          최초 생성
 */
@Entity
@Table(name="TB_DEPT")
@SequenceGenerator(
        name = "SQ_DEPT_GENERATOR"
        , sequenceName = "SQ_DEPT"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
// 사용법 : 1) @Where(clause = "DELETE_YN = 'N'") : select 될 때 조건("DELETE_YN = 'N'")을 강제로 붙여줌
//         2) @SQLDelete(sql = "대체 sql문") : delete 될때 대체해서 실행될 쿼리문
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_DEPT SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE DNO = ?")
public class Dept extends BaseTimeEntity {
    //    부서넘버
//    @Id : Primary Key 에 해당
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "SQ_DEPT_GENERATOR"
    )
    @Column
    private Integer dno;
    //    부서이름
    @Column
    private String dname;
    //    부서위치
    @Column
    private String loc;
}

📂 Emp.java

더보기
package com.example.simpledms.model;

import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

@Entity
@Table(name = "TB_EMP")
@SequenceGenerator(
        name = "SQ_EMP_GENERATOR"
        , sequenceName = "SQ_EMP"
        , initialValue = 1
        , allocationSize = 1
)
@Setter
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_EMP SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE ENO = ?")
public class Emp extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "SQ_EMP_GENERATOR"
    )
    @Column
    private Integer eno;

    @Column
    private String ename;

    @Column
    private String job;

    @Column
    private Integer manager;

    @Column
    private String hiredate;

    @Column
    private Integer salary;

    @Column
    private Integer commission;

    @Column
    private Integer dno;
}

3-3) DeptRepository.java 생성

📂 repository > DeptRepository.java

package com.example.simpledms.repository;

import com.example.simpledms.model.Dept;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;


@Repository
public interface DeptRepository extends JpaRepository<Dept, Integer> {
    // dname like 검색 - 쿼리메소드
    List<Dept> findAllByDnameContaining(String dname);
}

3-4) DeptService.java 생성

📂 service > DeptService.java

package com.example.simpledms.service;

import com.example.simpledms.model.Dept;
import com.example.simpledms.repository.DeptRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class DeptService {
    @Autowired
    DeptRepository deptRepository;  // DI
    
    /** 전체조회 */
    public List<Dept> findAll(){
        List<Dept> list = deptRepository.findAll();
        return list;
    }
    
    /** 검색어(dname like) 조회 함수 */
    public List<Dept> findAllByDnameContaining(String dname){
        List<Dept> list = deptRepository.findAllByDnameContaining(dname);
        return list;
    }
    
    
}

3-5) 프론트<->벡엔드 통신을 위해 CORS 보안 허용 설정 파일 생성

📂 config > WebConfig.java

package com.example.simpledms.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * packageName : com.example.dongsungsi.controller
 * fileName : WebConfig
 * author : kangtaegyung
 * date : 2022/06/14
 * description :
 * @Configuration : 자바클래스에 설정 기능을 부여하는 어노테이션 (application.properties 비슷)
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/06/14         kangtaegyung          최초 생성
 */

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
//                아래 url 허용
//                사용법 : .allowedOrigins("http://허용할 ip:허용할Port")
                .allowedOrigins("http://localhost:3000")

//                Todo: 아래 추가해야 update, delete, insert, select 가 cors 문제가 안생김
                .allowedMethods(
                        HttpMethod.GET.name(),
                        HttpMethod.POST.name(),
                        HttpMethod.PUT.name(),
                        HttpMethod.DELETE.name(),
                        HttpMethod.PATCH.name()
                );
    }
}

3-6) DeptController.java 생성

📂 controller > DeptController.java

package com.example.simpledms.controller;

import com.example.simpledms.model.Dept;
import com.example.simpledms.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * packageName : com.example.simpledms.controller
 * fileName : DeptController
 * author : GGG
 * date : 2023-10-19
 * description : 부서 컨트롤러 (@RestController - react용)
 * 요약 :
 * react(3000) <-> springboot(8000) 연동 : axios
 * 인터넷 기본 보안 : ip, port 최초에 지정된 것과 달라지면
 * => 해킹으로 기본 인정 (블러킹 : 단절) : CORS 보안
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-19         GGG          최초 생성
 */
@Slf4j
@RestController
@RequestMapping("/api")
public class DeptController {
    @Autowired
    DeptService deptService;    // DI

    /**
     * 전체 조회 + like 검색
     */
    @GetMapping("/dept")
    public ResponseEntity<Object> getDeptAll(
            @RequestParam(defaultValue = "") String dname) {
        try {
            // 전체 조회 + like 검색
            List<Dept> list = deptService.findAllByDnameContaining(dname);
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

}

 

3-7) REST API 테스트

3-8) 프론트 연동 테스트

반응형
반응형

📖 연관관계 매핑 종류와 방향

연관관계를 맺는 두 엔티티 간에 생성할 수 있는 연관관계의 종류는 아래와 같습니다.

  • One To One : 일 대 일(1:1)
  • One To Many : 일 대 다(1:N)
  • Many To One : 다 대 일(N:1)
  • Many To Many : 다 대 다(N:M)

데이터베이스에서는 두 테이블의 연관관계를 설정하면 외래키(FK)를 통해 서로 조인해서 참조하는 구조로 생성되지만,

JPA를 사용하는 객체지향 모델링에서는 엔티티 간 참조 방향을 설정할 수 있습니다.

데이터베이스와 관계를 일치시키기 위해 양방향으로 설정해도 무관하지만, 비즈니스 로직 관점에서 봤을 때는 단방향 관계만 설정해도 해결되는 경우가 많습니다. 단방향과 양방향 관계에 대해 아래에 간단하게 정리하면 다음과 같습니다.

  • 단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
  • 양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식

연관관계가 설정되면 한 테이블에서 다른 테이블의 기본값을 외래키로 갖게 됩니다.

이런 관계에서는 주인(Owner)이라는 개념이 사용됩니다. 일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행할 수 있습니다.

 

이전 예제에서 부서(Department)와 사원(Employee) 테이블의 관계는 1:N(일대다) 관계이며 예제에서는 일 대 다 관계에 대해 설명하겠습니다.

 

JPA에서 테이블간 조인을 할 경우 어노테이션을 이용하여 조인을 합니다.

*         JPA 조인 : @(어노테이션)을 이용해서 조인함
*         부모(부서)-자식(사원) 관계
*                            1) 1:N - @OneToMany(부서), @ManytoOne(사원)
*                            2) 1:1 - @OneToOne(핸드폰), @OneToOne(사람)
*                            3) N:M - @ManyToMany (x)
 *             추천 : 1) 1 : N  : @OneToMany(부서), @ManyToOne(사원)
 *                       => 양방향 조인(불가피할 때 사용 -> 사용시 여러가지 문제 발생)
 *                 1-1) : 부서쪽에는 어노테이션 x, 사원에는 @ManyToOne 사용
 *                       => 단방향 조인(추천)

📖 일대다, 다대일 매핑

부서 테이블과 사원 테이블은 부서 테이블의 입장에서는 일대다(하나의 부서에는 여러명의 사원 존재), 사원 테이블의 입장에서는 다대일 관계(사원은 하나의 부서에만 존재할 수 있음)로 볼 수 있습니다.

이러한 관계는 어떻게 구현해야 할지 직접 매핑하면서 알아보겠습니다 :)

 

📂 Employee.java

package com.example.jpacustomexam.model.exam04;

import com.example.jpacustomexam.model.BaseTimeEntity;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;

@Entity
@Table(name = "TB_EMPLOYEE")
@SequenceGenerator(
        name = "SQ_EMPLOYEE_GENERATOR"
        , sequenceName = "SQ_EMPLOYEE"
        , initialValue = 1
        , allocationSize = 1
)
@Setter
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
public class Employee extends BaseTimeEntity {
    //    @Id : Primary Key 에 해당
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "SQ_EMPLOYEE_GENERATOR"
    )
    @Column(columnDefinition = "NUMBER")
    private Integer eno;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String ename;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String job;

    @Column(columnDefinition = "NUMBER")
    private Integer manager;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String hiredate;

    @Column(columnDefinition = "NUMBER")
    private Integer salary;

    @Column(columnDefinition = "NUMBER")
    private Integer commission;

//    @Column(columnDefinition = "NUMBER")
//    private Integer dno; // 공통 컬럼(생략, 자동으로 생성해줌)
//    단방향 조인
//    사용법 : @JoinColumn(name = "조인컬럼명")
//    양방향 조인 시 : N + 1 문제 발생
//                1) 즉시 로딩(EAGER) : 기본 설정 -> 지연 로딩(LAZY)
//                                     (fetch = FetchType.LAZY)
//                      optional = false -> 조인방법(inner, outerjoin)
    @ManyToOne
    @JoinColumn(name = "dno")
    private Department department;


}

 

우선 사원테이블에 단방향 조인을 설정합니다.

사용법은 @ManyToOne 어노테이션을 작성합니다.

그리고 조인할 컬럼을 설정해야하는데 @JoinColumn 어노테이션을 사용합니다. 예제에서는 dno(부서번호)가 외래키로 지정되어 있으므로 @JoinColumn(name = "dno")로 설정하였습니다.

 

📂 DepartmentRepository.java

DepartmentRepository 인터페이스를 새로 생성합니다.

package com.example.jpacustomexam.repository.exam04;

import com.example.jpacustomexam.model.exam04.Department;
import com.example.jpacustomexam.model.exam04.Employee;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

/**
 * packageName : com.example.jpacustomexam.repository.exam04
 * fileName : DepartmentRepository
 * author : GGG
 * date : 2023-10-19
 * description : 부서 레포지토리 (CRUD)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-19         GGG          최초 생성
 */
@Repository
public interface DepartmentRepository extends JpaRepository<Department, Integer> {
    @Query(value = "select e from Employee e inner join e.department d ")
    Page<Employee> selectJoinPage2(Pageable pageable);
}

객체쿼리를 사용하여 쿼리문을 작성합니다

 

📂 DepartmentService.java

DepartmentService 파일을 새로생성합니다.

package com.example.jpacustomexam.service.exam04;

import com.example.jpacustomexam.model.exam04.Employee;
import com.example.jpacustomexam.repository.exam04.DepartmentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class DepartmentService {
    @Autowired
    DepartmentRepository departmentRepository;

//    단방향 조인 예제
    public Page<Employee> selectJoinPage2(Pageable pageable){
        Page<Employee> page = departmentRepository.selectJoinPage2(pageable);
        return page;
    }
}

📂 DepartmentController.java

 DepartmentController 파일을 새로 생성합니다.

package com.example.jpacustomexam.controller.exam04;

import com.example.jpacustomexam.dto.DeptEmpDto;
import com.example.jpacustomexam.model.exam04.Employee;
import com.example.jpacustomexam.service.exam04.DepartmentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/exam04")
public class DepartmentController {
    @Autowired
    DepartmentService departmentService;

   /** 단방향 조인 예제 */
    @GetMapping("/dept/join/paging2")
    public ResponseEntity<Object> selectJoinPage2(Pageable pageable) {
        try {
            Page<Employee> page
                    = departmentService.selectJoinPage2(pageable);
//          todo : Map 자료구조 정보 저장 : 1) 부서 객체, 2) 페이징 정보 (3개)
            Map<String, Object> response = new HashMap<>();
            response.put("emp", page.getContent());                         // 사원 객체
            response.put("currentPage", page.getNumber());                  // 현재페이지 번호
            response.put("totalItems", page.getTotalElements());            // 전체 테이블 건수
            response.put("totalPage", page.getTotalPages());                // 전체페이지 수

            if (page.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

📌 실행결과

더보기
GET http://localhost:8000/exam04/dept/join/paging2

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 19 Oct 2023 01:37:45 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "totalItems": 14,
  "totalPage": 1,
  "emp": [
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7369,
      "ename": "SMITH",
      "job": "CLERK",
      "manager": 7902,
      "hiredate": "1980-12-17 00:00:00",
      "salary": 800,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 20,
        "dname": "RESEARCH",
        "loc": "DALLAS"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7499,
      "ename": "ALLEN",
      "job": "SALESMAN",
      "manager": 7698,
      "hiredate": "1981-02-20 00:00:00",
      "salary": 1600,
      "commission": 300,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 30,
        "dname": "SALES",
        "loc": "CHICAGO"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7521,
      "ename": "WARD",
      "job": "SALESMAN",
      "manager": 7698,
      "hiredate": "1981-02-22 00:00:00",
      "salary": 1250,
      "commission": 500,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 30,
        "dname": "SALES",
        "loc": "CHICAGO"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7566,
      "ename": "JONES",
      "job": "MANAGER",
      "manager": 7839,
      "hiredate": "1981-04-02 00:00:00",
      "salary": 2975,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 20,
        "dname": "RESEARCH",
        "loc": "DALLAS"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7654,
      "ename": "MARTIN",
      "job": "SALESMAN",
      "manager": 7698,
      "hiredate": "1981-09-28 00:00:00",
      "salary": 1250,
      "commission": 1400,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 30,
        "dname": "SALES",
        "loc": "CHICAGO"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7698,
      "ename": "BLAKE",
      "job": "MANAGER",
      "manager": 7839,
      "hiredate": "1981-05-01 00:00:00",
      "salary": 2850,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 30,
        "dname": "SALES",
        "loc": "CHICAGO"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7782,
      "ename": "CLARK",
      "job": "MANAGER",
      "manager": 7839,
      "hiredate": "1981-06-09 00:00:00",
      "salary": 2450,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 10,
        "dname": "ACCOUNTING",
        "loc": "NEW YORK"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7788,
      "ename": "SCOTT",
      "job": "ANALYST",
      "manager": 7566,
      "hiredate": "1987-07-13 00:00:00",
      "salary": 3000,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 20,
        "dname": "RESEARCH",
        "loc": "DALLAS"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7839,
      "ename": "KING",
      "job": "PRESIDENT",
      "manager": null,
      "hiredate": "1981-11-17 00:00:00",
      "salary": 5000,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 10,
        "dname": "ACCOUNTING",
        "loc": "NEW YORK"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7844,
      "ename": "TURNER",
      "job": "SALESMAN",
      "manager": 7698,
      "hiredate": "1981-09-08 00:00:00",
      "salary": 1500,
      "commission": 0,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 30,
        "dname": "SALES",
        "loc": "CHICAGO"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7876,
      "ename": "ADAMS",
      "job": "CLERK",
      "manager": 7788,
      "hiredate": "1987-07-13 00:00:00",
      "salary": 1100,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 20,
        "dname": "RESEARCH",
        "loc": "DALLAS"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7900,
      "ename": "JAMES",
      "job": "CLERK",
      "manager": 7698,
      "hiredate": "1981-12-03 00:00:00",
      "salary": 950,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 30,
        "dname": "SALES",
        "loc": "CHICAGO"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7902,
      "ename": "FORD",
      "job": "ANALYST",
      "manager": 7566,
      "hiredate": "1981-12-03 00:00:00",
      "salary": 3000,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 20,
        "dname": "RESEARCH",
        "loc": "DALLAS"
      }
    },
    {
      "insertTime": "2023-10-19 01:37:39",
      "updateTime": null,
      "eno": 7934,
      "ename": "MILLER",
      "job": "CLERK",
      "manager": 7782,
      "hiredate": "1982-01-23 00:00:00",
      "salary": 1300,
      "commission": null,
      "department": {
        "insertTime": "2023-10-19 01:37:39",
        "updateTime": null,
        "dno": 10,
        "dname": "ACCOUNTING",
        "loc": "NEW YORK"
      }
    }
  ],
  "currentPage": 0
}
응답 파일이 저장되었습니다.
> 2023-10-19T103745.200.json

Response code: 200; Time: 623ms (623 ms); Content length: 4092 bytes (4.09 kB)

📖 양방향 조인

📂 Department.java 추가

//    양방향 조인 : 1) 순환참조 문제 - 해결 : @JsonManagedReference(부서)
//                                       @JsonBackReference(사원)
//                2) N + 1 문제
//    사용법 : @OneToMany(mappedBy = "사원_연결속성")
    @OneToMany(mappedBy = "department")
    @JsonManagedReference
    private Set<Employee> employee = new HashSet<>(); // 1:n(사원)
}

📂 Employee.java 추가

package com.example.jpacustomexam.model.exam04;

import com.example.jpacustomexam.model.BaseTimeEntity;
import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;

/**
 * packageName : com.example.jpacustomexam.model.exam04
 * fileName : Employee
 * author : GGG
 * date : 2023-10-19
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-19         GGG          최초 생성
 */
@Entity
@Table(name = "TB_EMPLOYEE")
@SequenceGenerator(
        name = "SQ_EMPLOYEE_GENERATOR"
        , sequenceName = "SQ_EMPLOYEE"
        , initialValue = 1
        , allocationSize = 1
)
@Setter
@Getter
@ToString(exclude = "department")   // 순환참조 방지
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
public class Employee extends BaseTimeEntity {
    //    @Id : Primary Key 에 해당
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "SQ_EMPLOYEE_GENERATOR"
    )
    @Column(columnDefinition = "NUMBER")
    private Integer eno;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String ename;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String job;

    @Column(columnDefinition = "NUMBER")
    private Integer manager;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String hiredate;

    @Column(columnDefinition = "NUMBER")
    private Integer salary;

    @Column(columnDefinition = "NUMBER")
    private Integer commission;

//    @Column(columnDefinition = "NUMBER")
//    private Integer dno; // 공통 컬럼(생략, 자동으로 생성해줌)
//    단방향 조인
//    사용법 : @JoinColumn(name = "조인컬럼명")
//    양방향 조인 시 : N + 1 문제 발생 - 조인이 안되고 각각 SQL문이 생성되는 현상
//                1) 즉시 로딩(EAGER) : 기본 설정 -> 지연 로딩(LAZY)
//                                     (fetch = FetchType.LAZY)
//                      optional = false -> 조인방법(inner, outerjoin)
//                2) inner join fetch : 조인 쿼리 생성
//                3) application.properties :
//                   N + 1 sql -> where in (?,?....?) 값으로 변경하는 옵션
//              spring.jpa.properties.hibernate.default_batch_fetch_size=1000
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "dno")
    @JsonBackReference
    private Department department;


}

📖 지연 로딩과 즉시로딩

JPA에서 지연로딩(lazy loading)과 즉시로딩(eager loading)은 중요한 개념중 하나입니다.

엔티티라는 객체의 개념으로 데이터베이스를 구현했기 때문에 연관관계를 가진 각 엔티티 클래스에는 연관관계가 있는 객체들이 필드에 존재하게 됩니다. 연관관계와 상관업싱 즉각 해당 엔티티의 값만 조회하고 싶거나 연관관계를 가진 테이블의 값도 조회하고 싶은 경우 등 여러 조건들을 만족하기 위해 등장한 개념이 지연로딩과 즉시로딩입니다.

 

📂 DepartmentService.java 추가

//    양방향 조인 예제 : fetch join
public List<Department> selectFetchJoin() {
    List<Department> page = departmentRepository.selectFetchJoin();

    return page;
}

📂 DepartmentRepository.java 추가

    //    양방향 조인 : fetch join -> Paging 안됨
    @Query(value = "select distinct d from Department d inner join fetch d.employee e ")
    List<Department> selectFetchJoin();

📂 DepartmentController.java 추가

    /** 양방향 조인 예제 */
    //    page=현재페이지번호(0 ~ n), size=전체페이지수
    @GetMapping("/dept/fetch/join")
    public ResponseEntity<Object> selectFetchJoin() {
        try {
            List<Department> page
                    = departmentService.selectFetchJoin();

            if (page.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(page, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✅ 실행결과

더보기
GET http://localhost:8000/exam04/dept/fetch/join

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 19 Oct 2023 02:26:02 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7902,
        "ename": "FORD",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7788,
        "ename": "SCOTT",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7566,
        "ename": "JONES",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-04-02 00:00:00",
        "salary": 2975,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7369,
        "ename": "SMITH",
        "job": "CLERK",
        "manager": 7902,
        "hiredate": "1980-12-17 00:00:00",
        "salary": 800,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7876,
        "ename": "ADAMS",
        "job": "CLERK",
        "manager": 7788,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 1100,
        "commission": null
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7900,
        "ename": "JAMES",
        "job": "CLERK",
        "manager": 7698,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 950,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7698,
        "ename": "BLAKE",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-05-01 00:00:00",
        "salary": 2850,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7844,
        "ename": "TURNER",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-08 00:00:00",
        "salary": 1500,
        "commission": 0
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7499,
        "ename": "ALLEN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-20 00:00:00",
        "salary": 1600,
        "commission": 300
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7521,
        "ename": "WARD",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-22 00:00:00",
        "salary": 1250,
        "commission": 500
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7654,
        "ename": "MARTIN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-28 00:00:00",
        "salary": 1250,
        "commission": 1400
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7900,
        "ename": "JAMES",
        "job": "CLERK",
        "manager": 7698,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 950,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7698,
        "ename": "BLAKE",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-05-01 00:00:00",
        "salary": 2850,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7844,
        "ename": "TURNER",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-08 00:00:00",
        "salary": 1500,
        "commission": 0
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7499,
        "ename": "ALLEN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-20 00:00:00",
        "salary": 1600,
        "commission": 300
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7521,
        "ename": "WARD",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-22 00:00:00",
        "salary": 1250,
        "commission": 500
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7654,
        "ename": "MARTIN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-28 00:00:00",
        "salary": 1250,
        "commission": 1400
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7902,
        "ename": "FORD",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7788,
        "ename": "SCOTT",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7566,
        "ename": "JONES",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-04-02 00:00:00",
        "salary": 2975,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7369,
        "ename": "SMITH",
        "job": "CLERK",
        "manager": 7902,
        "hiredate": "1980-12-17 00:00:00",
        "salary": 800,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7876,
        "ename": "ADAMS",
        "job": "CLERK",
        "manager": 7788,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 1100,
        "commission": null
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7900,
        "ename": "JAMES",
        "job": "CLERK",
        "manager": 7698,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 950,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7698,
        "ename": "BLAKE",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-05-01 00:00:00",
        "salary": 2850,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7844,
        "ename": "TURNER",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-08 00:00:00",
        "salary": 1500,
        "commission": 0
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7499,
        "ename": "ALLEN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-20 00:00:00",
        "salary": 1600,
        "commission": 300
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7521,
        "ename": "WARD",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-22 00:00:00",
        "salary": 1250,
        "commission": 500
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7654,
        "ename": "MARTIN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-28 00:00:00",
        "salary": 1250,
        "commission": 1400
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7900,
        "ename": "JAMES",
        "job": "CLERK",
        "manager": 7698,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 950,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7698,
        "ename": "BLAKE",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-05-01 00:00:00",
        "salary": 2850,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7844,
        "ename": "TURNER",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-08 00:00:00",
        "salary": 1500,
        "commission": 0
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7499,
        "ename": "ALLEN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-20 00:00:00",
        "salary": 1600,
        "commission": 300
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7521,
        "ename": "WARD",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-22 00:00:00",
        "salary": 1250,
        "commission": 500
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7654,
        "ename": "MARTIN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-28 00:00:00",
        "salary": 1250,
        "commission": 1400
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 10,
    "dname": "ACCOUNTING",
    "loc": "NEW YORK",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7839,
        "ename": "KING",
        "job": "PRESIDENT",
        "manager": null,
        "hiredate": "1981-11-17 00:00:00",
        "salary": 5000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7782,
        "ename": "CLARK",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-06-09 00:00:00",
        "salary": 2450,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7934,
        "ename": "MILLER",
        "job": "CLERK",
        "manager": 7782,
        "hiredate": "1982-01-23 00:00:00",
        "salary": 1300,
        "commission": null
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7902,
        "ename": "FORD",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7788,
        "ename": "SCOTT",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7566,
        "ename": "JONES",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-04-02 00:00:00",
        "salary": 2975,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7369,
        "ename": "SMITH",
        "job": "CLERK",
        "manager": 7902,
        "hiredate": "1980-12-17 00:00:00",
        "salary": 800,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7876,
        "ename": "ADAMS",
        "job": "CLERK",
        "manager": 7788,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 1100,
        "commission": null
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 10,
    "dname": "ACCOUNTING",
    "loc": "NEW YORK",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7839,
        "ename": "KING",
        "job": "PRESIDENT",
        "manager": null,
        "hiredate": "1981-11-17 00:00:00",
        "salary": 5000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7782,
        "ename": "CLARK",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-06-09 00:00:00",
        "salary": 2450,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7934,
        "ename": "MILLER",
        "job": "CLERK",
        "manager": 7782,
        "hiredate": "1982-01-23 00:00:00",
        "salary": 1300,
        "commission": null
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7900,
        "ename": "JAMES",
        "job": "CLERK",
        "manager": 7698,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 950,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7698,
        "ename": "BLAKE",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-05-01 00:00:00",
        "salary": 2850,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7844,
        "ename": "TURNER",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-08 00:00:00",
        "salary": 1500,
        "commission": 0
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7499,
        "ename": "ALLEN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-20 00:00:00",
        "salary": 1600,
        "commission": 300
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7521,
        "ename": "WARD",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-22 00:00:00",
        "salary": 1250,
        "commission": 500
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7654,
        "ename": "MARTIN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-28 00:00:00",
        "salary": 1250,
        "commission": 1400
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7902,
        "ename": "FORD",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7788,
        "ename": "SCOTT",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7566,
        "ename": "JONES",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-04-02 00:00:00",
        "salary": 2975,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7369,
        "ename": "SMITH",
        "job": "CLERK",
        "manager": 7902,
        "hiredate": "1980-12-17 00:00:00",
        "salary": 800,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7876,
        "ename": "ADAMS",
        "job": "CLERK",
        "manager": 7788,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 1100,
        "commission": null
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7900,
        "ename": "JAMES",
        "job": "CLERK",
        "manager": 7698,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 950,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7698,
        "ename": "BLAKE",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-05-01 00:00:00",
        "salary": 2850,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7844,
        "ename": "TURNER",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-08 00:00:00",
        "salary": 1500,
        "commission": 0
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7499,
        "ename": "ALLEN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-20 00:00:00",
        "salary": 1600,
        "commission": 300
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7521,
        "ename": "WARD",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-02-22 00:00:00",
        "salary": 1250,
        "commission": 500
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7654,
        "ename": "MARTIN",
        "job": "SALESMAN",
        "manager": 7698,
        "hiredate": "1981-09-28 00:00:00",
        "salary": 1250,
        "commission": 1400
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7902,
        "ename": "FORD",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1981-12-03 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7788,
        "ename": "SCOTT",
        "job": "ANALYST",
        "manager": 7566,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 3000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7566,
        "ename": "JONES",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-04-02 00:00:00",
        "salary": 2975,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7369,
        "ename": "SMITH",
        "job": "CLERK",
        "manager": 7902,
        "hiredate": "1980-12-17 00:00:00",
        "salary": 800,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7876,
        "ename": "ADAMS",
        "job": "CLERK",
        "manager": 7788,
        "hiredate": "1987-07-13 00:00:00",
        "salary": 1100,
        "commission": null
      }
    ]
  },
  {
    "insertTime": "2023-10-19 02:20:28",
    "updateTime": null,
    "dno": 10,
    "dname": "ACCOUNTING",
    "loc": "NEW YORK",
    "employee": [
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7839,
        "ename": "KING",
        "job": "PRESIDENT",
        "manager": null,
        "hiredate": "1981-11-17 00:00:00",
        "salary": 5000,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7782,
        "ename": "CLARK",
        "job": "MANAGER",
        "manager": 7839,
        "hiredate": "1981-06-09 00:00:00",
        "salary": 2450,
        "commission": null
      },
      {
        "insertTime": "2023-10-19 02:20:28",
        "updateTime": null,
        "eno": 7934,
        "ename": "MILLER",
        "job": "CLERK",
        "manager": 7782,
        "hiredate": "1982-01-23 00:00:00",
        "salary": 1300,
        "commission": null
      }
    ]
  }
]
응답 파일이 저장되었습니다.
> 2023-10-19T112602.200.json

Response code: 200; Time: 17ms (17 ms); Content length: 13967 bytes (13.97 kB)

 

📖 1 : 1 관계

2) 1:1 - @OneToOne(핸드폰), @OneToOne(사람)
  • Person 과 Phone 모델 클래스 생성

📂 Person.java

package com.example.jpacustomexam.model.exam05;

import com.example.jpacustomexam.model.BaseTimeEntity;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;

/**
 * packageName : com.example.jpacustomexam.model.exam05
 * fileName : Person
 * author : GGG
 * date : 2023-10-19
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-19         GGG          최초 생성
 */
@Entity
@Table(name="TB_PERSON")
@SequenceGenerator(
        name = "SQ_PERSON_GENERATOR"
        , sequenceName = "SQ_PERSON"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
//@ToString(exclude = "emp")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
public class Person extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SQ_PERSON_GENERATOR")
    @Column(columnDefinition = "NUMBER")
    private Integer no;     // 기본키

    @Column(columnDefinition = "VARCHAR2(255)")
    private String name;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String job;

//    1:1 관계 설정 : @OneToOne(사람:부모), @OneToOne(폰:자식)
//    사용법 : @OneToOne(mappedBy = "자식 속성명")
    @OneToOne(mappedBy = "person")
//    순환참조 해결
    @JsonManagedReference
    private Phone phone;

}

📂 Phone.java

package com.example.jpacustomexam.model.exam05;

import com.example.jpacustomexam.model.BaseTimeEntity;
import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;

/**
 * packageName : com.example.jpacustomexam.model.exam05
 * fileName : Phone
 * author : GGG
 * date : 2023-10-19
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-19         GGG          최초 생성
 */
@Entity
@Table(name="TB_PHONE")
@SequenceGenerator(
        name = "SQ_PHONE_GENERATOR"
        , sequenceName = "SQ_PHONE"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
//@ToString(exclude = "emp")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
public class Phone extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SQ_PHONE_GENERATOR")
    @Column(columnDefinition = "NUMBER")
    private Integer pno;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String pname;

    @Column(columnDefinition = "VARCHAR2(255)")
    private String vender;

//    1:1 관계
//    사용법 : @JoinColumn(name ="부모 공통컬럼")
    @OneToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "no")
    @JsonBackReference
    private Person person;

}

 

반응형
반응형
// --------------------------------------------
// JPA 페이징 처리 : 요청 페이징 객체 - Pageable(매개변수)
//                 결과 페이징 객체 - Page     (리턴값)
// --------------------------------------------

📂 DeptRepository.java

✅ 쿼리 메소드 페이징 처리

Page<Dept> findAllByDnameContaining(String dname, Pageable pageable);

📂 DeptService.java

 //--------------------------------------------
    // 페이징 예제
    //--------------------------------------------
    // 1)
    public Page<Dept> findAllByDnameContaining(String dname, Pageable pageable) {
        Page<Dept> page
                = deptRepository.findAllByDnameContaining(dname, pageable);

        return page;
    }

    // 2) 기본 제공되는 전체조회 함수 : 페이징처리 기능 추가
    public Page<Dept> findAllPage(Pageable pageable) {
        Page<Dept> page
                = deptRepository.findAll(pageable);

        return page;
    }

📂 DeptController.java

//--------------------------------------------
    // 페이징 예제
    //--------------------------------------------
    // 1)
    @GetMapping("/dept/dname/{dname}/paging")
    public ResponseEntity<Object> getDeptDnamePage(@PathVariable String dname, Pageable pageable) {
        try {
            Page<Dept> page
                    = deptService.findAllByDnameContaining(dname, pageable);
//          todo : Map 자료구조 정보 저장 : 1) 부서 객체, 2) 페이징 정보 (3개)
            Map<String, Object> response = new HashMap<>();
            response.put("dept", page.getContent());                        // 부서 객체
            response.put("currentPage", page.getNumber());                  // 현재페이지 번호
            response.put("totalItems", page.getTotalElements());            // 전체 테이블 건수
            response.put("totalPage", page.getTotalPages());                // 전체페이지 수

            if (page.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    // 2) findAll() + 페이징
    // page=현재페이지번호(0~n), size=전체페이지수
    @GetMapping("/dept/all/paging")
    public ResponseEntity<Object> getDeptAllPage(Pageable pageable) {
        try {
            Page<Dept> page
                    = deptService.findAllPage(pageable);
//          todo : Map 자료구조 정보 저장 : 1) 부서 객체, 2) 페이징 정보 (3개)
            Map<String, Object> response = new HashMap<>();
            response.put("dept", page.getContent());                        // 부서 객체
            response.put("currentPage", page.getNumber());                  // 현재페이지 번호
            response.put("totalItems", page.getTotalElements());            // 전체 테이블 건수
            response.put("totalPage", page.getTotalPages());                // 전체페이지 수

            if (page.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✅ 결과 - 1번 예제

GET http://localhost:8000/dept/dname/S/paging

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 18 Oct 2023 06:28:37 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "content": [
    {
      "insertTime": "2023-10-18 06:28:21",
      "updateTime": null,
      "dno": 20,
      "dname": "RESEARCH",
      "loc": "DALLAS"
    },
    {
      "insertTime": "2023-10-18 06:28:21",
      "updateTime": null,
      "dno": 30,
      "dname": "SALES",
      "loc": "CHICAGO"
    },
    {
      "insertTime": "2023-10-18 06:28:21",
      "updateTime": null,
      "dno": 40,
      "dname": "OPERATIONS",
      "loc": "BOSTON"
    }
  ],
  "pageable": {
    "sort": {
      "empty": true,
      "sorted": false,
      "unsorted": true
    },
    "offset": 0,
    "pageNumber": 0,
    "pageSize": 20,
    "paged": true,
    "unpaged": false
  },
  "last": true,
  "totalElements": 3,
  "totalPages": 1,
  "size": 20,
  "number": 0,
  "sort": {
    "empty": true,
    "sorted": false,
    "unsorted": true
  },
  "first": true,
  "numberOfElements": 3,
  "empty": false
}
응답 파일이 저장되었습니다.
> 2023-10-18T152837.200.json

Response code: 200; Time: 302ms (302 ms); Content length: 609 bytes (609 B)

✅ 결과 - 2번 예제

GET http://localhost:8000/dept/all/paging

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 18 Oct 2023 07:18:52 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "totalItems": 4,
  "totalPage": 1,
  "dept": [
    {
      "insertTime": "2023-10-18 07:18:02",
      "updateTime": null,
      "dno": 10,
      "dname": "ACCOUNTING",
      "loc": "NEW YORK"
    },
    {
      "insertTime": "2023-10-18 07:18:02",
      "updateTime": null,
      "dno": 20,
      "dname": "RESEARCH",
      "loc": "DALLAS"
    },
    {
      "insertTime": "2023-10-18 07:18:02",
      "updateTime": null,
      "dno": 30,
      "dname": "SALES",
      "loc": "CHICAGO"
    },
    {
      "insertTime": "2023-10-18 07:18:02",
      "updateTime": null,
      "dno": 40,
      "dname": "OPERATIONS",
      "loc": "BOSTON"
    }
  ],
  "currentPage": 0
}
응답 파일이 저장되었습니다.
> 2023-10-18T161852.200.json

Response code: 200; Time: 424ms (424 ms); Content length: 451 bytes (451 B)

✅ @Query 페이징 처리

📂 DeptRepository.java

 // 2) @Query : 페이징 처리(오라클 쿼리 : countQuery 속성 추가)
    @Query(value = "SELECT D.* FROM TB_DEPT D " +
            "WHERE D.DNAME LIKE '%'|| :dname || '%'",
            countQuery = "SELECT COUNT(*) FROM TB_DEPT D " +
                    "WHERE D.DNAME LIKE '%'|| :dname || '%'",
            nativeQuery = true)
    Page<Dept> selectByDnamePage(@Param("dname") String dname, Pageable pageable);

    // 3) @Query 페이징 + 조인(부서 + 사원) : DeptEmpDto
        @Query(value = "SELECT D.*, E.ENO, E.ENAME, E.SALARY " +
                "FROM TB_DEPT D, TB_EMP E " +
                "WHERE D.DNO = E.DNO",
                countQuery = "SELECT COUNT(*) " +
                        "FROM TB_DEPT D, TB_EMP E " +
                        "WHERE D.DNO = E.DNO",
                nativeQuery = true)
        Page<DeptEmpDto> selectNativeJoinPage(Pageable pageable);

📂 DeptService.java

// --------------------------------------------
// @Query : 페이징 처리
// --------------------------------------------
// 2)
public Page<Dept> selectByDnamePage(String dname, Pageable pageable) {
    Page<Dept> page
            = deptRepository.selectByDnamePage(dname, pageable);

    return page;
}

// 3) @Query 페이징 + 조인(부서 + 사원) : DeptEmpDto
public Page<DeptEmpDto> selectNativeJoinPage(Pageable pageable) {
    Page<DeptEmpDto> page
            = deptRepository.selectNativeJoinPage(pageable);

    return page;
}

📂 DeptController.java

// --------------------------------------------
    // @Query : 페이징 처리
    // --------------------------------------------
    // 2)
    @GetMapping("/dept/dname/{dname}/page")
    public ResponseEntity<Object> selectByDnamePage(String dname, Pageable pageable) {
        try {
            Page<Dept> page
                    = deptService.selectByDnamePage(dname, pageable);
//          todo : Map 자료구조 정보 저장 : 1) 부서 객체, 2) 페이징 정보 (3개)
            Map<String, Object> response = new HashMap<>();
            response.put("dept", page.getContent());                        // 부서 객체
            response.put("currentPage", page.getNumber());                  // 현재페이지 번호
            response.put("totalItems", page.getTotalElements());            // 전체 테이블 건수
            response.put("totalPage", page.getTotalPages());                // 전체페이지 수

            if (page.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    // 3) @Query 페이징 + 조인(부서 + 사원) : DeptEmpDto
    @GetMapping("/dept/native/join/page")
    public ResponseEntity<Object> selectNativeJoinPage(Pageable pageable) {
        try {
            Page<DeptEmpDto> page
                    = deptService.selectNativeJoinPage(pageable);
//          todo : Map 자료구조 정보 저장 : 1) 부서 객체, 2) 페이징 정보 (3개)
            Map<String, Object> response = new HashMap<>();
            response.put("dept", page.getContent());                        // 부서 객체
            response.put("currentPage", page.getNumber());                  // 현재페이지 번호
            response.put("totalItems", page.getTotalElements());            // 전체 테이블 건수
            response.put("totalPage", page.getTotalPages());                // 전체페이지 수

            if (page.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(response, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

 

반응형
반응형

💡 SQL을 추상화한 JPQL

JPA : ORM(Object-Relational Mapping) 프레임워크

JPQL(Java Persistence Query Language)

JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공합니다.

따라서 테이블을 대상으로 쿼리 하는 것이 아닌 엔티티 객체를 대상으로 쿼리합니다.

JPQL은 SQL을 추상화했기 때문에 특정 데이터베이스 SQL에 의존하지 않는 장점이 있습니다.

JPQL은 SQL과 문법이 유사하며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN을 지원합니다.


💡 쿼리메소드를 사용한 쿼리문 만들기

✅ TODO 1) 전체 조회 + 정렬(내림차순)

📂 DeptRepository.java

package com.example.jpacustomexam.repository;

import com.example.jpacustomexam.model.Dept;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * packageName : com.example.jpacustomexam.repository
 * fileName : DeptRepository
 * author : GGG
 * date : 2023-10-17
 * description : 부서 레포지토리 (기본 CRUD 함수)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-17         GGG          최초 생성
 */
@Repository
public interface DeptRepository extends JpaRepository<Dept, Integer> {
    // 개발자가 직접 SQL 작성 하는 기능(JPQL) : 1) 쿼리메소드
    //                                      2) @Query 쓰는 방법
    // todo: 1) 전체조회 + 정렬(내림차순)
    List<Dept> findAllByOrderByDnoDesc();
}

📂 DeptService.java

package com.example.jpacustomexam.service;

import com.example.jpacustomexam.model.Dept;
import com.example.jpacustomexam.repository.DeptRepository;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * packageName : com.example.jpacustomexam.service
 * fileName : DeptService
 * author : GGG
 * date : 2023-10-17
 * description : 부서 서비스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-17         GGG          최초 생성
 */
@Service
public class DeptService {
    @Autowired
    DeptRepository deptRepository;

    /** 전체조회 + 정렬(dno 내림차순) */
    // todo) 쿼리메소드를 사용한 함수
    public List<Dept> findAllByOrderByDnoDesc(){
        List<Dept> list = deptRepository.findAllByOrderByDnoDesc();
        return list;
    }
}

 

📂 DeptController.java

package com.example.jpacustomexam.controller;

import com.example.jpacustomexam.model.Dept;
import com.example.jpacustomexam.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * packageName : com.example.jpacustomexam.controller
 * fileName : DeptController
 * author : GGG
 * date : 2023-10-17
 * description : 부서 컨트롤러 (리액트 용)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-17         GGG          최초 생성
 */
@Slf4j
@RestController
public class DeptController {
    @Autowired
    DeptService deptService;    // DI

    /** 전체 조회 + 정렬(dno 내림차순) */
    @GetMapping("/dept/desc")
    public ResponseEntity<Object> getDeptAllDesc(){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list = deptService.findAllByOrderByDnoDesc();
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

 

✔ 서버 재시작 후 API 테스트 

부서번호가 내림차순으로 정렬된 것을 알 수 있습니다.

1) 쿼리메소드 : 함수이름으로 sql문 생성

예) findAllByOrderByDnoDesc

findAllByOrderByDnoDesc
findAll 전체조회
OrderBy 정렬
Dno 대상컬럼명
Desc 내림차순

 

💡 JPA의 쿼리메소드 샘플은 다음과 같습니다.

출처:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods         

Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

✅ TODO 2) 전체 조회 + 정렬(내림차순) - 부서명으로 내림차순, 부서번호 오름차순

📂 DeptRepository.java 추가

    // todo: 2) 전체조회 + 정렬(부서명 내림차순)
    List<Dept> findAllByOrderByDnameDesc();

    // todo: 3) 전체조회 + 정렬(부서번호 오름차순) : Asc 생략가능
    List<Dept> findAllByOrderByDno();
}

📂 DeptService.java 추가

/** 전체조회 + 정렬(dname 내림차순) : 쿼리메소드 */
public List<Dept> findAllByOrderByDnameDesc(){
    List<Dept> list = deptRepository.findAllByOrderByDnameDesc();
    return list;
}

/** 전체조회 + 정렬(dno 오름차순) : 쿼리메소드 */
public List<Dept> findAllByOrderByDno(){
    List<Dept> list = deptRepository.findAllByOrderByDno();
    return list;
}

📂 DeptController.java 추가

 /** 전체 조회 + 정렬(dname 내림차순) */
    @GetMapping("/dept/dname/desc")
    public ResponseEntity<Object> findAllByOrderByDnameDesc(){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list = deptService.findAllByOrderByDnameDesc();
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    /** 전체 조회 + 정렬(dno 오름차순) */
    @GetMapping("/dept/dno/asc")
    public ResponseEntity<Object> findAllByOrderByDno(){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list = deptService.findAllByOrderByDno();
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✔ 서버 재시작 후 API 테스트 

1) 전체조회 - 부서이름으로 내림차순

2) 전체조회 - 부서번호 오름차순

✅ TODO 3) 전체조회 + dname(부서명) like 검색 + dname 내림차순 조회

📂 DeptRepository.java 추가

// todo: 4) dname like 검색(DnameContaining)+ dname 내림차순 조회
List<Dept> findAllByDnameContainingOrderByDnameDesc(String dname);

📂 DeptService.java 추가

/** dname(부서명) like 검색 + dname 내림차순 조회 */
public List<Dept> findAllByDnameContainingOrderByDnameDesc(String dname){
    List<Dept> list 
            = deptRepository.findAllByDnameContainingOrderByDnameDesc(dname);
    return list;
}

📂 DeptController.java 추가

 /** 전체조회 + dname(부서명) like 검색 + dname 내림차순 조회 */
    @GetMapping("/dept/dname/containing/desc/{dname}")
    public ResponseEntity<Object> findAllByDnameContainingOrderByDnameDesc(
            @PathVariable String dname
    ){
        try {
            // todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
            List<Dept> list
                    = deptService
                    .findAllByDnameContainingOrderByDnameDesc(dname);
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✔ 서버 재시작 후 API 테스트 

1) 전체조회 - 부서명 like 검색 + dname 내림차순 조회

✅ TODO 4) 쿼리메소드 응용 연습문제

// todo) 응용연습
//    todo: 응용 연습문제 
//    todo: 연습 4) EMP 테이블에서 Job 이 manager 이고
//              매개변수로 부서번호(dno)를 받는 함수를 작성하세요.

//    todo: 연습 5) Emp 테이블에서 salary 가 1000 ~ 1500 사이의 값을 같는
//     사원을 조회하려고 합니다.  함수를 작성해 주세요
더보기

📂 EmpRepository.java

 

List<Emp> findAllByJobAndDno(String job, int dno);

List<Emp> findAllBySalaryBetween(int first, int last);

📂 EmpService.java

//    todo: 연습 4) EMP 테이블에서 Job 이 manager 이고
//              매개변수로 부서번호(dno)를 받는 함수를 작성하세요.
public List<Emp> findAllByJobAndDno(String job, int dno){
    List<Emp> list = empRepository.findAllByJobAndDno(job, dno);
    return list;
}

//    todo: 연습 5) Emp 테이블에서 salary 가 1000 ~ 1500 사이의 값을 같는
//     사원을 조회하려고 합니다.  함수를 작성해 주세요
public List<Emp> findAllBySalaryBetween(int first, int last){
    List<Emp> list = empRepository.findAllBySalaryBetween(first, last);
    return list;
}

📂 EmpController.java

  /** 연습 4) : 쿼리메소드 */
    @GetMapping("/emp/dno/{dno}")
    public ResponseEntity<Object> findAllByEnameContaining(
            @PathVariable int dno
    ) {
        try {
//         전체 조회 + 정렬(dno 내림차순) 호출
            List<Emp> list
                    = empService.findAllByJobAndDno("MANAGER", dno);

            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /** 연습5 */

    @GetMapping("emp/salary/{first}/{last}")
    public ResponseEntity<Object> findAllBySalaryBetween(@PathVariable int first, @PathVariable int last){
        try {
            List<Emp> list
                    = empService.findAllBySalaryBetween(first, last);
            if(list.isEmpty() == false){
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✔ 서버 재시작 후 API 테스트 

연습 4)

 

연습 5)

[
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7521,
    "ename": "WARD",
    "job": "SALESMAN",
    "manager": 7698,
    "hiredate": "1981-02-22 00:00:00",
    "salary": 1250,
    "commission": 500,
    "dno": 30
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7654,
    "ename": "MARTIN",
    "job": "SALESMAN",
    "manager": 7698,
    "hiredate": "1981-09-28 00:00:00",
    "salary": 1250,
    "commission": 1400,
    "dno": 30
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7844,
    "ename": "TURNER",
    "job": "SALESMAN",
    "manager": 7698,
    "hiredate": "1981-09-08 00:00:00",
    "salary": 1500,
    "commission": 0,
    "dno": 30
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7876,
    "ename": "ADAMS",
    "job": "CLERK",
    "manager": 7788,
    "hiredate": "1987-07-13 00:00:00",
    "salary": 1100,
    "commission": null,
    "dno": 20
  },
  {
    "insertTime": "2023-10-17 06:38:45",
    "updateTime": null,
    "eno": 7934,
    "ename": "MILLER",
    "job": "CLERK",
    "manager": 7782,
    "hiredate": "1982-01-23 00:00:00",
    "salary": 1300,
    "commission": null,
    "dno": 10
  }
]
응답 파일이 저장되었습니다.
> 2023-10-17T153953.200.json

Response code: 200; Time: 25ms (25 ms); Content length: 933 bytes (933 B)

💡 @Query 이용한 쿼리문 작성

데이터베이스에서 값을 가져올 때는 위의 코드처럼 메서드의 이름만으로 쿼리 메서드를 생성할 수 도 있습니다.

이번에는 @Query 어노테이션을 사용해 직접 JPQL을 작성해봅시다.

 

📂 DeptRepository.java

    //    ----------------------------------------
    //    @Query 예제
    //    1) 오라클 쿼리
    //    2) 객체 쿼리
    //    ----------------------------------------
    //    todo 1) ename like 검색
    @Query(value = "SELECT TD.* FROM TB_EMP TD WHERE TD.ENAME LIKE '%' || :dname || '%'", nativeQuery = true)
    List<Emp> selectByEname(@Param("ename") String ename);

위 코드를 다르게 작성하는 방법(JDK 버전에 따라 사용 불가능할 수도 있음)

//    todo: 1-1) 위의 코드를 다르게 코딩
    @Query(value = "SELECT TD.* FROM TB_DEPT TD WHERE TD.DNAME LIKE '%'||:dname||'%'",
            nativeQuery = true)
    List<Dept> selectByDname(String dname);
//    todo: 1-2) 위의 코드를 다르게 코딩 : 참고
@Query(value = "SELECT TD.* FROM TB_DEPT TD WHERE TD.DNAME LIKE '%'|| ?1 ||'%'",
        nativeQuery = true)
List<Dept> selectByDname(String dname);

✅ SQL에서 쿼리문 결과가 올바른지 확인

📂 DeptService.java

/** 전체조회 + dname like 검색 : @Query */
public List<Dept> selectByDname(String dname) {
    List<Dept> list
            = deptRepository.selectByDname(dname);

    return list;
}

📂 DeptController.java

 /** 전체조회 + dname like 검색 : @Query */
    @GetMapping("/dept/dname/{dname}")
    public ResponseEntity<Object> selectByDname(
            @PathVariable String dname
    ) {
        try {
//         전체 조회 + 정렬(dno 오름차순) 호출
            List<Dept> list
                    = deptService.selectByDname(dname);

            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

✅ 실행결과

부서명 중에 'S'가 들어가는 부서명 검색하기

GET http://localhost:8000/dept/dname/S

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 18 Oct 2023 00:11:16 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "insertTime": "2023-10-18 00:11:06",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS"
  },
  {
    "insertTime": "2023-10-18 00:11:06",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO"
  },
  {
    "insertTime": "2023-10-18 00:11:06",
    "updateTime": null,
    "dno": 40,
    "dname": "OPERATIONS",
    "loc": "BOSTON"
  }
]
응답 파일이 저장되었습니다.
> 2023-10-18T091116.200.json

Response code: 200; Time: 277ms (277 ms); Content length: 295 bytes (295 B)

✅ 객체쿼리를 이용한 쿼리문 작성

//    todo: 2) dname like 검색 (객체 쿼리)
    // 객체 쿼리 만드는 방법 : 테이블 -> 클래스명
    //                      컬럼명 -> 속성명
    //                      * -> 사용하지 않음, 클래스명의 별칭을 붙여서 사용
    //                      (대소문자를 구분함, nativeQuery=true(생략))
    @Query(value = "SELECT TD FROM Dept TD WHERE TD.dname LIKE '%'||:dname||'%'")
    List<Dept> selectByDname(@Param("dname") String dname);

 

✅ TODO) 부서 테이블에서 부서명(dname)과 위치(loc)를 매개변수로 받아 조회하는 함수 만들기

📂 DeptRepository.java

@Query(value = "SELECT D.* FROM TB_DEPT D " +
        "WHERE D.DNAME = :dname " +
        "AND   D.LOC = :loc ", nativeQuery = true)
List<Dept> selectByDnameAndLoc(@Param("dname") String dname,@Param("loc") String loc);

📂 DeptService.java

/** 전체조회 + dname like 검색 : @Query */
public List<Dept> selectByDnameAndLoc(String dname, String loc) {
    List<Dept> list
            = deptRepository.selectByDnameAndLoc(dname, loc);

    return list;
}

📂 DeptController.java

    /** 전체조회 + dname like 검색 : @Query */
    @GetMapping("/dept/dname/{dname}/loc/{loc}")
    public ResponseEntity<Object> selectByDnameAndLoc(
            @PathVariable String dname,
            @PathVariable String loc
    ) {
        try {
//         전체 조회 + 정렬(dno 오름차순) 호출
            List<Dept> list
                    = deptService.selectByDnameAndLoc(dname, loc);

            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

        } catch (Exception e){
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

 

반응형

'Spring Boot > 스프링부트 예제' 카테고리의 다른 글

JPA - 연관관계 매핑  (0) 2023.10.19
JPA 페이징 처리  (1) 2023.10.18
JPA 상세조회, 저장함수, 수정함수, 삭제함수  (0) 2023.10.17
JPA를 활용한 CRUD 구현하기  (0) 2023.10.16
JSTL 라이브러리  (0) 2023.10.06
반응형

이전 블로그 글 : https://devjunyeok.tistory.com/181

 

JPA를 활용한 CRUD 구현하기

안녕하세요! 이번 시간에는 JPA 를 활용한 예제 프로젝트를 생성해보겠습니다. 들어가기 전에 JPA의 특징으로는 SQL문을 자동생성하는 것이 특징입니다. 💡 프로젝트 준비 및 환경설정 프로젝트

devjunyeok.tistory.com

📂 DeptService.java

package com.example.jpaexam.service;

import com.example.jpaexam.model.Dept;
import com.example.jpaexam.repository.DeptRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
 * packageName : com.example.jpaexam.service
 * fileName : DeptService
 * author : GGG
 * date : 2023-10-16
 * description : 부서 업무 서비스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Service
public class DeptService {

    @Autowired
    DeptRepository deptRepository; // DI 객체 가져오기

    /** 전체조회 */
    public List<Dept> findAll() {
        List<Dept> list = deptRepository.findAll(); // db 전체조회 함수 호출
        return list;
    }

    /** 상세조회(1건조회) */
    public Optional<Dept> findById(int dno) {
        Optional<Dept> optionalDept = deptRepository.findById(dno);

        return optionalDept;
    }

    /** 저장(수정)함수 */
    public Dept save(Dept dept) {
//      todo: jpa 저장함수 호출 ( 기본키 없으면 insert, 있으면 update )
        Dept dept2 = deptRepository.save(dept);

        return dept2; // 저장된 부서객체
    }

    /** 삭제함수 */
    public boolean removeById(int dno){
        // todo : existsById : jpa 함수 - 리턴값 있으면 true, 없으면 false
        if(deptRepository.existsById(dno)) {
            deptRepository.deleteById(dno); // db 삭제(dno)
            return true;
        }
        return false;
    }


}

📂 DeptController.java

package com.example.jpaexam.controller.exam01;

import com.example.jpaexam.model.Dept;
import com.example.jpaexam.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

/**
 * packageName : com.example.jpaexam.controller.exam01
 * fileName : DeptController
 * author : GGG
 * date : 2023-10-16
 * description : 부서 컨트롤러 : @RestController 사용
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Slf4j
@RestController
@RequestMapping("/exam01")
public class DeptController {

    @Autowired
    DeptService deptService;    // 객체 가져오기 (DI)

    /** 전체조회 함수 */
    @GetMapping("/dept")
    public ResponseEntity<Object> getDeptAll() {
        try {
            // todo) 전체조회 함수 호출
            List<Dept> list = deptService.findAll();
            if (list.isEmpty() == false) {
                // todo) 성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
                // todo) 데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /** 상세 조회 함수 */
    @GetMapping("/dept/{dno}")
    public ResponseEntity<Object> getDeptId(@PathVariable int dno){
        try {
            // todo) 전체조회 함수 호출
            Optional<Dept> optionalDept = deptService.findById(dno);
            if (optionalDept.isEmpty() == false) {
                // todo) 성공
                return new ResponseEntity<>(optionalDept.get(), HttpStatus.OK);
            } else {
                // todo) 데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /** 저장함수 */
    @PostMapping("/dept")
    public ResponseEntity<Object> createDept(@RequestBody Dept dept){
        try {
            // todo : jap 서비스 저장함수 호출 : dept2(DB 저장된 객체)
            Dept dept2 = deptService.save(dept);
            return new ResponseEntity<>(dept2, HttpStatus.OK);
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /** 수정함수 */
    @PutMapping("/dept/edit/{dno}")
    public ResponseEntity<Object> updateDept(@RequestBody Dept dept, @PathVariable int dno){
        try {
            // todo : jap 서비스 수정함수 호출 : dept2(DB 저장된 객체)
            Dept dept2 = deptService.save(dept);
            return new ResponseEntity<>(dept2, HttpStatus.OK);
        } catch (Exception e) {
            log.debug(e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /** 삭제함수 */
    @DeleteMapping("/dept/delete/{dno}")
    public ResponseEntity<Object> deleteDept(
            @PathVariable int dno
    ){
        try {
            // todo) 삭제 함수 호출
            boolean bSuccess = deptService.removeById(dno);
            if (bSuccess == true) {
                // todo) 삭제성공
                return new ResponseEntity<>(HttpStatus.OK);
            } else {
                // todo) 0건 삭제(삭제할 대상이 없을경우)
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

}

📌 실행해보기

1) 상세조회

부서번호 10번을 조회

2) 저장함수

임의의 값을 지정해서 저장하기

저장함수 테스트시 기본키(dno)와 insertTime, updateTime 제외하고 입력

3) 수정함수

부서번호 10번 조회 - updateTime과 dname, loc가 수정된 것을 알 수 있습니다.

 

4) 삭제함수 - 부서번호 10번 삭제하기

전체조회해서 10번 부서에 해당하는 데이터가 삭제되었는지 확인

 

반응형
반응형

안녕하세요!

이번 시간에는 JPA 를 활용한 예제 프로젝트를 생성해보겠습니다.

 

들어가기 전에

JPA의 특징으로는 SQL문을 자동생성하는 것이 특징입니다.

 

💡 프로젝트 준비 및 환경설정


프로젝트 준비시 스프링부트의 종속성을 추가합니다.

생성 후 시간이 소요됩니다.

 

한글 깨짐 방지를 위해 설정 - 에디터 - 파일 인코딩 부분에 UTF-8로 변경해줍니다.

build.gradle 파일의 dependencies 부분에  오라클과 logback, log4jdbc 종속성을 추가해줍니다.

	//   todo: 오라클 라이브러리( 19c )
	implementation 'com.oracle.database.jdbc:ucp:19.14.0.0'
	implementation 'com.oracle.database.security:oraclepki:19.14.0.0'
	implementation 'com.oracle.database.security:osdt_cert:19.14.0.0'
	implementation 'com.oracle.database.security:osdt_core:19.14.0.0'
	//   todo: logback , log4jdbc 설정
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
	implementation 'ch.qos.logback:logback-classic:1.2.11'
	implementation 'org.slf4j:slf4j-api:1.7.36'
	implementation 'org.slf4j:jcl-over-slf4j:1.7.36'

 

 

그리고 main > resources > application.properties 파일에 설정을 추가합니다.

# 서버 포트
server.port=8000


# todo : docker db 설정
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy

# todo : spring.datasource.url=jdbc:log4jdbc:oracle:thin:@ip주소:db포트번호/db이름
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:1521/xepdb1

spring.datasource.username=scott
spring.datasource.password=!Ds1234567890

# todo : jpa 설정
# todo : ddl-auto - create(모든 테이블 재생성)/update(수정된 테이블만 생성)/none(안함)
spring.jpa.hibernate.ddl-auto=create
#spring.jpa.hibernate.ddl-auto=update
#spring.jpa.hibernate.ddl-auto=none

# todo : db 제품 연결 ( oracle 12 이상 : Oracle12cDialect )
spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect

# todo : generate-ddl=true (테이블 자동 생성 옵션)
spring.jpa.generate-ddl=true

# todo : sql log 보기 (true/false)
spring.jpa.show-sql=true

# 1) resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
# todo : DML, DDL 스크립트 (실습용 샘플 테이블) 실행을 위한 옵션
spring.jpa.defer-datasource-initialization=true

# todo :  sql log 좀 더 정리된 형태로 찍기
spring.jpa.properties.hibernate.format_sql=true

# todo : 로깅 레벨 : error < info < debug < trace (정보 많은 순서)
logging.level.org.hibernate=info

# 2)  resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.sql.init.mode=always

# sql 에러 무시하고 스프링 서버 로딩
spring.sql.init.continue-on-error=true


# 자바 소스 변경시 스프링 서버 자동 재시작
spring.devtools.restart.enabled=true

# PUT , DELETE 방식도 form 에서 사용할 수 있게 만들어줌 : jsp에서 사용함
spring.mvc.hiddenmethod.filter.enabled=true

실습을 위해 resources 폴더 아래에 4가지 파일을 추가합니다.

 

📂 data.sql

INSERT INTO TB_DEPT VALUES (10,'ACCOUNTING','NEW YORK',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_DEPT VALUES (20,'RESEARCH','DALLAS',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_DEPT VALUES (30,'SALES','CHICAGO',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_DEPT VALUES (40,'OPERATIONS','BOSTON',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);

INSERT INTO TB_EMP VALUES (7369,'SMITH','CLERK',    7902,TO_CHAR(to_date('17-12-1980','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),800, NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7499,'ALLEN','SALESMAN', 7698,TO_CHAR(to_date('20-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1600, 300,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7521,'WARD','SALESMAN',  7698,TO_CHAR(to_date('22-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1250, 500,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7566,'JONES','MANAGER',  7839,TO_CHAR(to_date('2-4-1981',  'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),2975,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7654,'MARTIN','SALESMAN',7698,TO_CHAR(to_date('28-9-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1250,1400,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7698,'BLAKE','MANAGER',  7839,TO_CHAR(to_date('1-5-1981',  'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),2850,NULL,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7782,'CLARK','MANAGER',  7839,TO_CHAR(to_date('9-6-1981',  'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),2450,NULL,10, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7788,'SCOTT','ANALYST',  7566,TO_CHAR(to_date('13-07-1987','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),3000,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7839,'KING','PRESIDENT', NULL,TO_CHAR(to_date('17-11-1981','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),5000,NULL,10, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7844,'TURNER','SALESMAN',7698,TO_CHAR(to_date('8-9-1981',  'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1500,   0,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7876,'ADAMS','CLERK',    7788,TO_CHAR(to_date('13-07-1987','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1100,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7900,'JAMES','CLERK',    7698,TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),950, NULL,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7902,'FORD','ANALYST',   7566,TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),3000,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7934,'MILLER','CLERK',   7782,TO_CHAR(to_date('23-1-1982', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1300,NULL,10, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);


INSERT INTO TB_FAQ VALUES (1,'제목','해결방법',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_FAQ VALUES (2,'제목2','해결방법2',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_FAQ VALUES (3,'제목3','해결방법3',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_FAQ VALUES (4,'제목4','해결방법4',  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);

COMMIT;

📂log4jdbc.log4j2.properties

# sql \uB85C\uADF8 \uC124\uC815
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
# \uCD5C\uB300 \uBA87 \uB77C\uC778\uAE4C\uC9C0 \uCD9C\uB825\uD560 \uAC83\uC778\uAC00\uB97C \uACB0\uC815 : 0 \uC774\uBA74 \uC81C\uD55C\uC5C6\uC74C
log4jdbc.dump.sql.maxlinelength=0

📂 logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

    <!-- Appenders -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
       <encoder>
          <Pattern>%d %5p [%c] %m%n</Pattern>
       </encoder>
    </appender>

    <appender name="console-infolog" class="ch.qos.logback.core.ConsoleAppender">
       <encoder>
          <Pattern>%d %5p %m%n</Pattern>
       </encoder>
    </appender>

    <!-- Logger -->
    <logger name="com.example.jpaexam" level="DEBUG" appender-ref="console" />
    <logger name="jdbc.sqlonly" level="INFO" appender-ref="console-infolog" />
    <logger name="jdbc.resultsettable" level="INFO" appender-ref="console-infolog" />

    <!-- Root Logger : 기본 OFF -->
<!--    하위 로거에서 재정의해서 사용함 : INFO, DEBUG 등으로 -->
    <root level="off">
       <appender-ref ref="console" />
    </root>
</configuration>

📂 shema.sql

DROP SEQUENCE SQ_DEPT;
CREATE SEQUENCE SQ_DEPT START WITH 50 INCREMENT BY  10;

DROP SEQUENCE SQ_EMP;
CREATE SEQUENCE SQ_EMP START WITH 8000 INCREMENT BY  1;

DROP SEQUENCE SQ_FAQ;
CREATE SEQUENCE SQ_FAQ START WITH 1 INCREMENT BY  1;

DROP TABLE TB_EMP CASCADE CONSTRAINT;
DROP TABLE TB_DEPT CASCADE CONSTRAINT;
DROP TABLE TB_FAQ CASCADE CONSTRAINT;

CREATE TABLE TB_DEPT (
                         DNO NUMBER NOT NULL PRIMARY KEY,
                         DNAME VARCHAR2(255),
                         LOC VARCHAR2(255),
                         INSERT_TIME VARCHAR2(255),
                         UPDATE_TIME VARCHAR2(255)
);

CREATE TABLE TB_EMP
(ENO NUMBER NOT NULL PRIMARY KEY,
 ENAME VARCHAR2(255),
 JOB   VARCHAR2(255),
 MANAGER  NUMBER,
 HIREDATE VARCHAR2(255),
 SALARY NUMBER,
 COMMISSION NUMBER,
 DNO NUMBER,
 INSERT_TIME VARCHAR2(255),
 UPDATE_TIME VARCHAR2(255)
);

CREATE TABLE TB_FAQ (
                        NO NUMBER NOT NULL PRIMARY KEY,
                        TITLE VARCHAR2(255),
                        CONTENT VARCHAR2(255),
                        INSERT_TIME VARCHAR2(255),
                        UPDATE_TIME VARCHAR2(255)
);

 

그리고 java > com.example.jpaexam 폴더 아래에 다음과 같이 폴더를 생성합니다.

 

💡 model 폴더에 공통모델 파일 생성


 

model 폴더 아래에 생성일자/수정일자를 만들어주는 추상클래스를 생성합니다.

실습에서는 파일명을 BaseTimeEntity 로 생성하였습니다.

📂 model > BaseTimeEntity.java

1) 먼저 추상클래스 위에 어노테이션 3개를 추가합니다.

package com.example.jpaexam.model;

import lombok.Getter;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

/**
 * packageName : com.example.jpaexam.model
 * fileName : BaseTimeEntity
 * author : GGG
 * date : 2023-10-16
 * description : JPA 에서 자동으로 생성일자/수정일자를 만들어주는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Getter
// todo : 자동으로 생성일자/수정일자 컬럼을 sql 문에 추가시키는 어노테이션 2개
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    // todo) 공통 속성
    private  String insertTime;
    private  String updateTime;
}

 

여기서 @MappedSuperclass 어노테이션의 의미는

생성일자/수정일자를 모든 엔티티에 공통으로 가져가야하는 상황에서 위의 코드처럼

BaseTimeEntity를 정의해서 활용할 때 사용합니다.

 

2) 공통 속성의 형태를 변경하는 함수 추가

위의 코드는 time의 형태가 yyyy-MM-dd HH:mm:ss 형태가 아닌 기본 형태로 보이게 됩니다.

가독성을 좋게하기 위해 공통속성 코드 아래에 함수를 추가합니다.

 

...
private String updateTime;

// todo) 해당 테이블에 데이터가 만들어 질 때(insert 문) 실행되는 이벤트 함수
@PrePersist
void OnPerPersist(){
    this.insertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

}
// todo) 해당 테이블에 데이터가 수정될 때(update 문) 실행되는 이벤트 함수
@PreUpdate
void OnPreUpdate(){
    this.updateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    this.insertTime = this.updateTime; // 생성일시 == 수정일시 동일하게 처리
}

 

💡 Dept 모델 생성


model 폴더 아래에 Dept 클래스를 생성합니다.

JPA에서 사용하는 어노테이션의 설명은 아래 코드에 todo로 표시되어있습니다.

📂 model > Dept.java

package com.example.jpaexam.model;

import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;

/**
 * packageName : com.example.jpaexam.model
 * fileName : Dept
 * author : GGG
 * date : 2023-10-16
 * description : 부서 모델 클래스 ( 엔티티(entity) )
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
// todo) @Entity - JPA 기능을 클래스에 부여하는 어노테이션
@Entity
// todo) @Table(name = "생성될 테이블명")
@Table(name = "TB_DEPT")
// todo 사용법 : @SequenceGenerator(
//        name = "시퀀스 함수 이름"
//        , sequenceName = "DB에 생성된 시퀀스 이름"
//        , initialValue = 시작값
//        , allocationSize = jpa에서 관리용숫자(성능지표)
//  )
@SequenceGenerator(
        name = "SQ_DEPT_GENERATOR"
        , sequenceName = "SQ_DEPT"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
// todo) JPA 어노테이션 SQL 자동 생성시 null 값 컬럼은 제외하고 생성하는 어노테이션
//  예) insert into 테이블명(컬럼1, 컬럼2, 컬럼3) values(1, 2, null)
//    => insert into 테이블명(컬럼1, 컬럼2) values(1, 2)
@DynamicInsert
@DynamicUpdate
public class Dept extends BaseTimeEntity{
    @Id // todo) 기본키임을 알려주는 어노테이션
    // todo) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "시퀀스함수이름")
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SQ_DEPT_GENERATOR") // 시퀀스 기능 부여
    // todo : @Column(columnDefinition = "DB컬럼 자료형")
    @Column(columnDefinition = "NUMBER")
    private Integer dno;    // 부서번호(기본키) - 시퀀스 기능 부여

    @Column(columnDefinition = "VARCHAR2(255)")
    private String dname;   // 부서명

    @Column(columnDefinition = "VARCHAR2(255)")
    private String loc;     // 부서위치
}

 

repository 파일 만들기

DB 접속 함수들(CRUD)을 만들어주기 위한 인터페이스 파일을 생성합니다.

📂 repository > DeptRepository.java 

package com.example.jpaexam.repository;

import com.example.jpaexam.model.Dept;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * packageName : com.example.jpaexam.repository
 * fileName : DeptRepository
 * author : GGG
 * date : 2023-10-16
 * description : JPA 레포지토리 인터페이스 (DB 접속 함수들(CRUD) 있음)
 *                  == DAO랑 비슷함
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
// todo) @Repository - 클래스위에 붙이고, 스프링서버가 실행될 때 자동으로 객체 1개를 만들어줌( IOC )
//    사용법 : 인터페이스명 extends JpaRepository<모델클래스명, 기본키의 자료형>
@Repository
public interface DeptRepository extends JpaRepository<Dept, Integer> {
}

JpaRepository를 상속받으면 DB접속에 사용할 수 있는 CRUD함수 기능을 사용할 수 있습니다.

 

💡 업무 service 클래스 생성

📂 service > DeptService.java

package com.example.jpaexam.service;

import com.example.jpaexam.model.Dept;
import com.example.jpaexam.repository.DeptRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * packageName : com.example.jpaexam.service
 * fileName : DeptService
 * author : GGG
 * date : 2023-10-16
 * description : 부서 업무 서비스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Service
public class DeptService {
    
    @Autowired
    DeptRepository deptRepository;  // DI 객체 가져오기
    
    /** 전체 조회 */
    public List<Dept> findAll(){
        List<Dept> list = deptRepository.findAll(); // db 전체조회 함수 호출
        return list;
    }
    
}

 

💡 controller 생성

📂 controller > DeptController

package com.example.jpaexam.controller.exam01;

import com.example.jpaexam.model.Dept;
import com.example.jpaexam.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * packageName : com.example.jpaexam.controller.exam01
 * fileName : DeptController
 * author : GGG
 * date : 2023-10-16
 * description : 부서 컨트롤러 : @RestController 사용
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Slf4j
@RestController
@RequestMapping("/exam01")
public class DeptController {

    @Autowired
    DeptService deptService;    // 객체 가져오기 (DI)

    /** 전체조회 함수 */
    @GetMapping("/dept")
    public ResponseEntity<Object> getDeptAll() {
        try {
            // todo) 전체조회 함수 호출
            List<Dept> list = deptService.findAll();
            if (list.isEmpty() == false) {
                // todo) 성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
                // todo) 데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

REST API 테스트를 위해 아래 그림의 빨간색 박스를 누릅니다.

테스트 창에서 ▶ 버튼을 눌러 실행합니다.

실행결과

GET http://localhost:8000/exam01/dept

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 16 Oct 2023 08:15:45 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "insertTime": "2023-10-16 08:15:40",
    "updateTime": null,
    "dno": 10,
    "dname": "ACCOUNTING",
    "loc": "NEW YORK"
  },
  {
    "insertTime": "2023-10-16 08:15:40",
    "updateTime": null,
    "dno": 20,
    "dname": "RESEARCH",
    "loc": "DALLAS"
  },
  {
    "insertTime": "2023-10-16 08:15:40",
    "updateTime": null,
    "dno": 30,
    "dname": "SALES",
    "loc": "CHICAGO"
  },
  {
    "insertTime": "2023-10-16 08:15:40",
    "updateTime": null,
    "dno": 40,
    "dname": "OPERATIONS",
    "loc": "BOSTON"
  }
]
응답 파일이 저장되었습니다.
> 2023-10-16T171545.200.json

Response code: 200; Time: 297ms (297 ms); Content length: 397 bytes (397 B)

JPA에서 자동으로 만든 select 문을 확인해봅시다.

Hibernate: 
    select
        dept0_.dno as dno1_0_,
        dept0_.insert_time as insert_time2_0_,
        dept0_.update_time as update_time3_0_,
        dept0_.dname as dname4_0_,
        dept0_.loc as loc5_0_ 
    from
        tb_dept dept0_

 

반응형
반응형

💡 Core 라이브러리

JSTL의 core 라이브러리를 사용하기 위해서는 JSP 파일 상단에 코드를 선언합니다.

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

📖 <c:set> : 변수에 값을 저장

변수저장소(사용범위)
page(현재페이지) < request < session < application(전체프로그램)
사용법 : <c:set var="변수명" value="값" scope="page"(scope는 생략가능, 기본이 page)/>

📌 사용 예제

<%--     TODO : jstl 표현식--%>
        <p><c:set var="num" value="10" scope="page"/></p>
        <p><c:set var="num2" value="10" scope="page"/></p>
        <p><c:set var="total" value="${num + num2}" scope="page"/></p>
<%--     TODO : el 표현식--%>
        <p>${num}</p>
        <p>${num2}</p>
        <p>${total}</p>

📌 결과

📖 <c:forTokens> : 문자열 구분자로 구분하여 전체 출력하기

사용법 : 
<c:forTokens items="문자열" delims="구분자" var="변수명">
         실행문
</c:forTokens>

📌 사용예제

<c:forTokens items="1/2/3" delims="/" var="number">
    <h2>토큰 : ${number}</h2>
</c:forTokens>

📌 실행결과

📖 <c:catch> : try ~ catch 예외처리

사용법 : 
<c:catch var="에러변수"> 실행문 </c:catch>
실행문이 에러 발생하면 에러변수에 메세지가 들어감

📌 사용예제

<c:set var="num3" value="20" />
<c:catch var="err">
    <h2>${num3}</h2>
</c:catch>
<c:if test="${not empty err}">
    에러 메세지 : ${err.message}
</c:if>

📌 실행결과

📖 <c:redirect> : 페이지 강제이동

<c:redirect url="이동할_url주소" />

 

📖 <c:import> : 공통페이지 끼워 넣기

<c:import url="보여줄페이지" />

📌 사용예제

📖 <c:url> : url로 페이지 이동하기

<c:url value="url주소" var="변수명" />
[사용]
<a href="${변수명}">표시할내용</a>

📌 사용예제

 

💡 Function 라이브러리

fn 라이브러리를 사용하기전 상단 코드 선언

fn 라이브러리는 el 표현식과 함께 사용합니다.

<%--TODO: fn lib import--%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>

<%-- TODO: fmt lib import--%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

📌 변수 선언

<c:set var="chars" value="안녕하세요 홍길동입니다. Bye"/>
<c:set var="name" value="홍길동"/>

 

📖 fn:contains(string, substr)

string이 substr을 포함하면 return true 없으면 false return

<h2>indexOf : ${fn:indexOf(chars,"홍길동")}</h2>

📖 fn:indexOf(string, substr)

string 문자열에 "substr" 값이 있는 index 번호를 return

<h2>indexOf : ${fn:indexOf(chars,"홍길동")}</h2>

📖 fn:length(string)

전체 길이를 리턴

<h2>length : ${fn:length(chars)}</h2>

📖 fn:replace(string, "기존문자열", "바꿀문자열")

string 문자열에서 기존문자열을 바꿀문자열로 변경

<h2>replace : ${fn:replace(chars,"홍길동","장길산")}</h2>

📖 fn:split(string, 구분자)

string에서 구분자로 문자열 자르기, 리턴은 배열

<h2><c:set var="sChar" value='${fn:split(chars, "")}'/></h2>
<h2>${sChar[0]}</h2>
<h2>${sChar[1]}</h2>
<h2>${sChar[2]}</h2>

📖 fn:substring(string, 시작인덱스번호, 끝인덱스번호)

string 문자열에서 시작인덱스에서 끝인덱스번호-1까지 자르기

<h2>substring : ${fn:substring(chars,0 ,6)}</h2>

📖 fn:toLowerCase/toUpperCase(string)

대소문자 바꾸기

<h2>toLowerCase : ${fn:toLowerCase(chars)}</h2>
<h2>toUpperCase : ${fn:toUpperCase(chars)}</h2>

 

💡 fmt(format) 라이브러리

fmt 라이브러리는 날짜,시간, 숫자 텍스트를 포멧하고 표시하는 fomatter 기능을 제공합니다.

 

📌 변수선언

<c:set var="numtest" value="1234567890"/>

📖 fmt:formatNumber : 세 자리마다 쉼표(,) 추가하여 출력

<h2><fmt:formatNumber value="${numtest}"/> </h2>

1) 숫자에 통화 추가 : value 뒤에 type="currency" 추가

<h2>formatNumber : <fmt:formatNumber value="${numtest}" type="currency"/></h2>

📖 날짜 포멧 주기

날짜 포멧을 주기위해서는 스크립틀릿을 사용합니다. 스크립틀릿은 과거 코딩기법이라 사용은 하지 않습니다.

<%

  자바코딩 가능

 %>

 <%
        Date date = new java.util.Date();
    %>
<%--       TODO: 화면 출력시 : <%=변수명%>--%>
    <h2><%=date%></h2>
    <c:set var="today" value="<%=new java.util.Date()%>" />
    <h2><fmt:formatDate value="${today}" pattern="yyyy-MM-dd hh:mm:ss" /> </h2>

반응형
반응형

💡 파라미터 방식

앞 전 예제에서는 쿼리스트링 방식으로 매개변수를 전달하였습니다. 이번에는 쿼리스트링 방식을 개선한 파라미터 방식의 매개변수 전달 방법에 대해 알아보겠습니다.

 

📂PathVariableController.java

@Controller
@RequestMapping("/exam05")
public class PathVariableController {
//    TODO : url 테스트 파라미터 방식 : http://localhost:8000/exam05/path-variable/LeeJunHyuk
//                                  => @GetMapping("/path-variable/{웹매개변수명}")
//                                  => 웹 브라우저 주소창 사용 : url/값
//    TODO : url 테스트 쿼리스트링 방식 : http://localhost:8000/exam05/path-variable?name=LeeJunHyuk
    @GetMapping("/path-variable/{name}")
    public String getPathVariable(
            @PathVariable String name,
            Model model)
    {
        model.addAttribute("name", name);
        return "exam05/path_variable.jsp";
    }
}

📂path_variable.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%--TODO : JSTL 표현식 사용을 위한 import--%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<%-- TODO : el 표현식--%>
    <p>${name}</p>
</body>
</html>

파라미터 형식의 매개변수 전달방법은

@GetMapping("url/{웹매개변수명}") 으로 사용하고 메소드 매개변수로 @PathVariable를 사용한 것을 알 수 있습니다.

 

📌 실행결과

반응형

+ Recent posts