CMD 창 열기
C:\Users\USER>sqlplus system/oracle
create USER kimgura IDENTIFIED BY kimgura1234;
GRANT CONNECT, RESOURCE TO kimgura;
Spring09_JPA 프로젝트 생성

spring.application.name=Spring09_JPA
server.port=9000
# oracle DataSource Setting ( Connection Pool )
#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
#spring.datasource.username=kimgura
#spring.datasource.password=kimgura1234
playground 폴더 안에 h2db 폴더 생성
# H2 DataBase 접속 정보
# http://localhost:9000/h2-console/
# 위의 주소로 h2 database 콘솔에 접속할수 있다.
# JDBC URL 에 아래의 url 정보를 붙여 넣기해서 접속한다.
spring.datasource.url=jdbc:h2:file:C:/playground/h2db
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=user
spring.datasource.password=
spring.h2.console.enabled=true
step09.entity 패키지 생성
Member 클래스 생성
entity 클래스에는 @Data 어노테이션을 붙이면 나중에 문제가 발생
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity(name="MEMBER_INFO")
public class Member {
// num 이라는 필드에 대해서 2개의 어노테이션 추가
// PK 개념
@Id
// 자동으로 번호를 넣을 수 있도록 (sequence 개념)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer num;
private String name;
private String addr;
}
EntityManagerFactory 객체 주입dependency 로 추가하고 application.properties 파일에 적절한 jpa 설정을 해 놓으면 EntityManagerFactory 가 bean 으로 관리된다@SpringBootApplication
public class Spring09JpaApplication {
@Autowired
EntityManagerFactory emf;
public static void main(String[] args) {
SpringApplication.run(Spring09JpaApplication.class, args);
}
}
helloJPA 메소드 생성EntityManager 객체의 persist() 메소드를 이용해서 객체에 저장된 정보를 영속화 (영구저장)@PostConstruct
public void helloJPA() {
// EntityManager 객체를 얻어와서
EntityManager em = emf.createEntityManager();
// 트랜잭션을 시작
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// Entity 객체를 생성하고
Member m1 = Member.builder().name("김구라").addr("노량진").build();
Member m2 = Member.builder().name("해골").addr("행신동").build();
Member m3 = Member.builder().name("원숭이").addr("동물원").build();
em.persist(m1);
em.persist(m2);
em.persist(m3);
tx.commit(); // commit 하는 시점에 저장
} catch (Exception e) {
e.printStackTrace();
tx.rollback(); // 예외가 발생한 경우 저장하지 않고 취소 할 수 있다.
} finally {
em.close();
}
}
Member.class type 객체에 담겠다는 의미.getResultList() 메소드를 호출해서 결과를 얻어낸다Member.class type 을 전달했기 때문에 List 의 generic 이 Member type 이 된다EntityManager em2 = emf.createEntityManager();
List<Member> members = em2.createQuery("SELECT m FROM MEMBER_INFO m", Member.class)
.getResultList();
for (Member mem : members) {
System.out.println(mem.getNum() + " : " + mem.getName() + " : " + mem.getAddr());
}
em2.close();
ddl-auto 옵션
create : 실행 시마다 기존 테이블 삭제 후 새로 생성 (데이터 삭제됨)create-drop : 종료 시점에 테이블도 삭제update : 기존 데이터 유지하며 필요한 테이블/컬럼만 추가/수정application.properties 에 jpa 설정 추가
# jpa 설정
spring.jpa.show-sql=true
# update 는 테이블 유지, create 는 시작할 때 테이블 삭제 및 다시 만들기
# create-drop 은 시작할 때 테이블 생성, 종료될 때 테이블 삭제
spring.jpa.hibernate.ddl-auto=create-drop
주소를 웹 브라우저 주소창에 붙여넣기
H2 Console
URL 정보를 복사해서 JDBC URL 에 붙여넣기
jdbc:h2:file:C:/playground/h2db
설정된 userName을 넣고 userName : user
Connect click

MEMBER_INFO 테이블 click
자동으로 생성된 SQL 문 실행 (Run 버튼 click)
해당 결과 확인

➜ 자동으로 sequence 도 생성되어 있다
step09.repository 패키지 생성
JpaRepository 인터페이스를 상속받은 인터페이스를 정의하는 것 만으로 구현 클래스가 만들어지고 해당 클래스로 생성된 객체가 bean 으로 관리가 된다Dao 가 자동으로 만들어 진다고 생각하면 된다extends JpaRepository<Entity 클래스명, 해당 Entity에서 PK의 data typepublic interface MemberRepository extends JpaRepository<Member, Integer>{
}
➜ entity 만으로 DML 작업 가능
MemberRepository 의존 객체 주입@Autowired
MemberRepository memberRepo;
finally{
// MemberRepository 객체를 이용해서 select 작업하기
List<Member> list = memberRepo.findAll();
for(Member tmp : list) {
System.out.println(tmp.getNum() + "|" + tmp.getName() + "|" + tmp.getAddr());
}
}
1|김구라|노량진
2|해골|행신동
3|원숭이|동물원
Spring04 프로젝트의 templates 안에 있는 member 폴더와 home.html 복사해서 붙여넣기
spring09.controller 패키지 생성
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
List<String> notice = List.of("공지사항 입니다", "공지사항1", "공지사항2");
model.addAttribute("notice", notice);
return "home";
}
}
@Controller
@RequiredArgsConstructor
public class MemberController {
// 필요한 의존 객체를 주입 받는다
private final MemberService service;
@PostMapping("/member/update")
public String update(MemberDto dto) {
// 수정할 회원의 정보가 MemberDto 객체에 담겨서 전달된다
service.updateMember(dto);
return "member/update";
}
@GetMapping("/member/edit")
public String edit(int num, Model model) {
// 수정할 회원의 정보를 얻어와서
MemberDto dto = service.getMember(num);
// Model 객체에 담고
model.addAttribute("dto", dto);
// view page 로 forward 이동해서 응답
return "member/edit";
}
@GetMapping("/member/delete")
public String delete(int num) {
/*
* 매개변수에 int num 을 선언하면 요청 파라미터 중에서 num 이라는 파라미터 명으로 전달되는
* 문자열을 자동추출해서 Integer.parseInt() 를 수행해서 실제 int 값으로 바꾼 다음
* 해당 값을 매개 변수에 전달해 준다
* int 값으로 바꿀 수 없는 문자열이 넘어오면 에러가 발생한다
* */
service.deleteMember(num);
return "member/delete";
}
@PostMapping("/member/save")
public String save(MemberDto dto) {
/*
* 매개변수에 MemberDto 를 선언하면 폼 전송되는 파라미터가 자동 추출되어서
* MemberDto 객체에 담긴 채로 전달된다
* 단 MemberDto 클래스의 필드명과 폼 전송되는 파라미터명이 같아야 한다
* private String name <=> <input type="text" name="name">
* private String addr <=> <input type="text" name"addr">
* */
service.addMember(dto);
// 회원 목록 보기 "/member/list" 요청을 다시 하라는 redirect 응답 하기
// "redirect:리다이렉트 경로" 처럼 redirect: 으로 시작하는 문자열을 리턴하면 된다
return "redirect:/member/list";
}
@GetMapping("/member/new-form")
public String newForm() {
// 현재 여기서 수행할 로직은 없고 view page 의 위치만 리턴해준다
return "member/new-form";
}
@GetMapping("/member/list")
public String list(Model model) {
// 회원 목록
List<MemberDto> list = service.getAll();
// 응답에 필요한 객체를 Model 객체에 담는다
model.addAttribute("list", list);
// "/WEB-INF/views/member/list.jsp" 에서 응답하기
return "member/list";
}
}
spring09.dto 패키지 생성
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class MemberDto {
private int num;
private String name;
private String addr;
}
⭐ Entity ⟷ Dto
Entity : DB 테이블과 연관
Dto : 요청과 응답에 사용
spring09.service 패키지 생성
public interface MemberService {
public List<MemberDto> getAll();
public MemberDto getMember(int num);
public void addMember(MemberDto dto);
public void updateMember(MemberDto dto);
public void deleteMember(int num);
}
MemberRepository JPA Repository 객체 주입@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepo;
@Override
public List<MemberDto> getAll() {
return null;
}
@Override
public MemberDto getMember(int num) {
return null;
}
@Override
public void addMember(MemberDto dto) {
}
@Override
public void updateMember(MemberDto dto) {
}
@Override
public void deleteMember(int num) {
}
}
getAll() 메소드 완성Entity 와 Dto 를 구분해야 하기 때문에 반복문 돌면서 Entity 의 list 를 dto 의 list 로 변환 (Entity 하나당 Dto 를 생성해서 넣기)
잘 설계한다면 해당 코드들을 1줄 coding 으로 간단히 해결 가능
select 전용 메소드는 @Transactinal(readOnly-true) 를 붙이면 안전하다
의도치 않은 수정, 삭제를 방지하기 위해
@Transactional(readOnly = true)
@Override
public List<MemberDto> getAll() {
// 전체 회원 목록을 얻어낸다 (Entity 의 목록)
List<Member> list = memberRepo.findAll();
List<MemberDto> dtoList = new ArrayList<>();
for(Member tmp : list) {
MemberDto dto = MemberDto.builder()
.num(tmp.getNum())
.name(tmp.getName())
.addr(tmp.getAddr())
.build();
dtoList.add(dto);
}
return dtoList;
}
MembeDto 에 toDto static 메소드 생성
Entity 를 매개변수로 전달하면 dto 를 리턴하는 static 메소드 만들어두기
public static MemberDto toDto(Member entity) {
return MemberDto.builder()
.num(entity.getNum())
.name(entity.getName())
.addr(entity.getAddr())
.build();
}
getAll 메소드 안에 코드 한 줄 코딩으로 수정stream 으로 만들어서 map() 함수를 이용해서 stream 에 저장된 Entity 를 dto 로 변경한 다음 List 로 변경한다클래스명 :: static 메소드명 : 클래스 안에 만들어진 static 메소드를 참조하는 표현식List<MemberDto> dtoList = memberRepo.findAll().stream().map(MemberDto :: toDto).toList();
Entity 로 변경하는 toEntity 메소드 생성.save() 할 때 새로운 row 가 추가된다null 을 넣어주어야 .save() 가 우리가 의도하는 바 대로 동작한다public static Member toEntity(MemberDto dto) {
return Member.builder()
.num(dto.getNum() == 0 ? null : dto.getNum())
.name(dto.getName())
.addr(dto.getAddr())
.build();
}
MemberServiceImple 에 getMember 메소드 완성
id 칼럼에 대해서 default 로 오름차순 정렬된 List 가 반환된다
@Transactional(readOnly = true)
@Override
public MemberDto getMember(int num) {
// 번호를 이용해서 Member entity 객체를 얻어내서
// Member entity = memberRepo.findById(num).get();
// num 에 해당하는 Member entity 를 얻어내는데 만일 존재하지 않는다면 예외 발생 시키기
Member entity = memberRepo.findById(num)
.orElseThrow(()-> new IllegalArgumentException("회원이 존재하지 않습니다 num = "+num));
// entity 를 dto 로 변경해서 리턴한다
return MemberDto.toDto(entity);
}
addMember 메소드 완성save() 메소드에 전달하면 된다insert 된다update 된다@Transactional
@Override
public void addMember(MemberDto dto) {
memberRepo.save(Member.toEntity(dto));
}
updateMember 메소드 완성@Transactional
@Override
public void updateMember(MemberDto dto) {
// memberRepo.save(Member.toEntity(dto));
// 위의 경우 잘못된 데이터가 DB 에 insert 될 위험이 있기 때문에 아래의 방식을 많이 사용
Member entity = memberRepo.findById(dto.getNum())
.orElseThrow(() -> new IllegalArgumentException("수정할 회원이 존재하지 않습니다"));
// 존재할 경우 entity 객체를 수정하면 DB 에 자동 반영된다
entity.setName(dto.getName()); // 객체를 수정하는 것만으로 DB 가 수정되는 것을 확인한다
entity.setAddr(dto.getAddr());
}
deleteMember 메소드 완성@Transactional
@Override
public void deleteMember(int num) {
// 만일 삭제할 Entity 가 존재하지 않으면
if(!memberRepo.existsById(num)) {
throw new IllegalArgumentException("삭제할 회원이 존재하지 않습니다 num = "+num);
}
// 번호를 이용해서 삭제 (실패 시 예외가 발생하지 않는다)
memberRepo.deleteById(num);
}
getMember 메소드 수정Sort.Direction.ASC or Sort.Direction.DESCList<MemberDto> dtoList = memberRepo.findAll(Sort.by(Sort.Direction.DESC, "num"))
.stream().map(MemberDto :: toDto).toList();
MemberRepository findAllByOrderByNumDesc & findAllByOrderByNumAsc 메소드 생성
미리 정해진 형식으로 메소드를 만들면 알아서 정렬된다
칼럼명을 Camel Case 로 작성하면 된다
findAllByOrderBy칼럼명Asc()findAllByOrderBy칼럼명Desc()public List<Member> findAllByOrderByNumDesc();
public List<Member> findAllByOrderByNumAsc();
MemberServiceImpl 에 getMember 메소드 수정
repository 인터페이스에 정해진 형식의 메소드를 만들어서 정렬된 결과를 얻어낼 수 있다
List<MemberDto> dtoList = memberRepo.findAllByOrderByNumAsc()
.stream().map(MemberDto :: toDto).toList();
Java Persistence Query Language
1. sql 처럼 생겼지만 entity 중심으로 작성하는 객체 지향 쿼리 언어
2. DB 종류에 종속되지 않는다
getMember 메소드 수정@Query("SELECT m FROM MEMBER_INFO m ORDER BY m.num DESC")
public List<Member> findAllQuery();
MemberServiceImpl 에 getMember 메소드 수정
repository 인터페이스에 작성한 JPQL 를 이용해서 정렬된 결과 얻어내기
List<MemberDto> dtoList = memberRepo.findAllQuery()
.stream().map(MemberDto :: toDto).toList();
findAllNativeQuery 메소드 생성nativeQuery = true 옵션을 주면 된다@Query(value = "SELECT num, name, addr FROM MEMBER_INFO ORDER BY num DESC", nativeQuery = true)
public List<Member> findAllNativeQuery();
MemberServiceImpl 에 getMember 메소드 수정
repository 인터페이스에 작성된 Native Query 문을 이용해서 정렬된 결과 얻어내기
List<MemberDto> dtoList = memberRepo.findAllNativeQuery()
.stream().map(MemberDto :: toDto).toList();
도서(book) 정보
를 만들어 구현해 보세요
<li><a th:href="@{/book/list}">도서 목록</a></li>
@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity(name="BOOK_INFO")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String author;
public static Book toEntity(BookDto dto) {
return Book.builder()
.id(dto.getId() == 0 ? null : dto.getId())
.name(dto.getName())
.author(dto.getAuthor())
.build();
}
}
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class BookDto {
private long id;
private String name;
private String author;
public static BookDto toDto(Book entity) {
return BookDto.builder()
.id(entity.getId())
.name(entity.getName())
.author(entity.getAuthor())
.build();
}
}
public interface BookRepository extends JpaRepository<Book, Long>{
@Query("SELECT b FROM BOOK_INFO b ORDER BY b.id ASC")
public List<Book> findAllQuery();
}
public interface BookService {
public List<BookDto> getAll();
public BookDto getBook(long id);
public void addBook(BookDto dto);
public void updateBook(BookDto dto);
public void deleteBook(long id);
}
@Service
@RequiredArgsConstructor
public class BookServiceImpl implements BookService{
private final BookRepository bookRepo;
@Transactional(readOnly = true)
@Override
public List<BookDto> getAll() {
List<BookDto> dtoList = bookRepo.findAllQuery()
.stream().map(BookDto :: toDto).toList();
return dtoList;
}
@Transactional(readOnly = true)
@Override
public BookDto getBook(long id) {
Book entity = bookRepo.findById(id)
.orElseThrow(()-> new IllegalArgumentException("도서가 존재하지 않습니다"));
return BookDto.toDto(entity);
}
@Transactional
@Override
public void addBook(BookDto dto) {
bookRepo.save(Book.toEntity(dto));
}
@Transactional
@Override
public void updateBook(BookDto dto) {
Book entity = bookRepo.findById(dto.getId())
.orElseThrow(()-> new IllegalArgumentException("수정할 도서가 존재하지 않습니다"));
entity.setName(dto.getName());
entity.setAuthor(dto.getAuthor());
}
@Transactional
@Override
public void deleteBook(long id) {
if(!bookRepo.existsById(id)) {
throw new IllegalArgumentException("삭제할 도서가 존재하지 않습니다 id = "+id);
}
bookRepo.deleteById(id);
}
}
/book/list , /book/new , /book/insert , /book/edit , /book/update , /book/delete 요청 처리할 컨트롤러 메소드 생성@Controller
@RequiredArgsConstructor
public class BookController {
private final BookService service;
@GetMapping("/book/list")
public String list(Model model) {
List<BookDto> list = service.getAll();
model.addAttribute("list", list);
return "book/list";
}
@GetMapping("/book/new")
public String newForm() {
return "book/new";
}
@PostMapping("/book/insert")
public String insert(BookDto dto) {
service.addBook(dto);
return "redirect:/book/list";
}
@GetMapping("/book/edit")
public String edit(long id, Model model) {
BookDto dto = service.getBook(id);
model.addAttribute("dto", dto);
return "book/edit";
}
@PostMapping("/book/update")
public String update(BookDto dto) {
service.updateBook(dto);
return "book/update";
}
@GetMapping("/book/delete")
public String delete(long id) {
service.deleteBook(id);
return "book/delete";
}
}
<div class="container">
<a th:href="@{/book/new}">도서 추가</a>
<h1>도서 목록</h1>
<table>
<thead>
<tr>
<th>번호</th>
<th>이름</th>
<th>저자</th>
<th>수정</th>
<th>삭제</th>
</tr>
</thead>
<tbody>
<tr th:each="tmp : ${list}">
<td th:text="${tmp.id}"></td>
<td th:text="${tmp.name}"></td>
<td th:text="${tmp.author}"></td>
<td>
<a th:href="@{/book/edit(id=${tmp.id})}">수정</a>
</td>
<td>
<a th:href="@{/book/delete(id=${tmp.id})}">삭제</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="container">
<h1>도서 추가 양식</h1>
<form th:action="@{/book/insert}" method="post">
<div>
<label for="name">이름</label>
<input type="text" name="name" id="name"/>
</div>
<div>
<label for="addr">저자</label>
<input type="text" name="author" id="author"/>
</div>
<button type="submit">저장</button>
</form>
</div>
<div class="container">
<h1>도서 수정 폼</h1>
<form th:action="@{/book/update}" method="post">
<input type="hidden" name="id" th:value="${dto.id}"/>
<div>
<label for="name">이름</label>
<input type="text" name="name" id="name" th:value="${dto.name}"/>
</div>
<div>
<label for="addr">저자</label>
<input type="text" name="author" id="author" th:value="${dto.author}"/>
</div>
<button type="submit">수정 확인</button>
<button type="reset">취소</button>
</form>
</div>
<div class="container">
<p>
<strong th:text="${param.name}">[[${param.name}]]</strong>의 도서 정보 수정 완료
<a th:href="@{/book/list}">목록 보기</a>
</p>
</div>
<script th:inline="javascript">
alert([[${param.id}]] + " 번 도서의 정보를 삭제 했습니다");
location.href = [[@{/book/list}]];
</script>
내일 할 내용
@Entity // 테이블명을 지정하지 않으면 클래스명과 동일하게 테이블이 만들어진다
public class Client {
// 고객 번호
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long num;
// 고객의 이름
@Column(nullable = false, length = 20) // null 허용하지 않음, 최대 길기 20 글자
private String userName;
// 등록일
@CreationTimestamp // 최초 저장되는 시점의 시간이 자동으로 들어 가도록
private LocalDateTime createdAt;
// 수정일
@UpdateTimestamp // 수정되는 시점의 시간이 자동으로 들어 가도록
private LocalDateTime updatedAt;
// 생일
@Column(nullable = true) // 처음에는 바워두었다가 나중에 입력 가능하도록
private LocalDate birthday; // 생일은 시간을 입력하지 않으므로 LocalDate type 으로
}