1️⃣ 수정/조회 API
PostApiController 작성
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.service.posts.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
return postsService.update(id, requestDto);
}
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById (@PathVariable Long id){
return postsService.findById(id);
}
}
PostResponseDto 작성
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Getter;
@Getter
public class PostsResponseDto {
private Long id;
private String title;
private String content;
private String author;
public PostsResponseDto(Posts entity){
this.id=entity.getId();
this.content=entity.getContent();
this.title=entity.getTitle();
this.author=entity.getAuthor();
}
}
PostsUpdateRequestDto 작성
package com.jojoldu.book.springboot.web.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content){
this.title=title;
this.content=content;
}
}
Posts 작성
package com.jojoldu.book.springboot.domain.posts;
import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String author;
@Builder
public Posts(String title, String content, String author){
this.title=title;
this.content=content;
this.author=author;
}
public void update(String title, String content){
this.title=title;
this.content=content;
}
}
PostService 작성
package com.jojoldu.book.springboot.service.posts;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsResponseDto;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
@Transactional
public Long update(Long id, PostsUpdateRequestDto responseDto){
Posts post=postsRepository.findById(id).
orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
post.update(responseDto.getTitle(), responseDto.getContent());
return id;
}
public PostsResponseDto findById (Long id){
Posts entity=postsRepository.findById(id)
.orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
return new PostsResponseDto(entity);
}
}
PostApiControllerTest 작성
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import com.jojoldu.book.springboot.web.dto.PostsUpdateRequestDto;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private PostsRepository postsRepository;
@After
public void tearDown() throws Exception{
postsRepository.deleteAll();
}
@Test
public void Posts_등록된다() throws Exception{
//given
String title="title";
String content="content";
PostsSaveRequestDto requestDto=PostsSaveRequestDto.builder()
.title(title)
.content(content)
.author("author")
.build();
String url="http://localhost:" + port + "/api/v1/posts";
//when
ResponseEntity<Long> responseEntity=restTemplate.postForEntity(url, requestDto, Long.class);
//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all=postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
@Test
public void Posts_수정된다() throws Exception{
//given
Posts savedPosts=postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updateId=savedPosts.getId();
String expectedTitle="title2";
String expectedContent="content2";
PostsUpdateRequestDto requestDto= PostsUpdateRequestDto.builder()
.title(expectedTitle)
.content(expectedContent)
.build();
String url="http://localhost:" + port + "/api/v1/posts/"+updateId;
HttpEntity<PostsUpdateRequestDto> requestEntity=new HttpEntity<>(requestDto);
//when
ResponseEntity<Long> responseEntity=restTemplate.
exchange(url, HttpMethod.PUT, requestEntity, Long.class);
//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all=postsRepository.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
2️⃣ 로컬에서 데이터베이스로 조회 기능 확인하기
로컬 환경에서는 데이터베이스로 H2를 사용함
웹 콘솔을 사용해야 함
application.proerties에 옵션 추가
spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
이후 Application 실행하고 http://localhost:8080/h2-console 들어가면 되는데
위에 내가 쓴 코드대로 안하고 책처럼 하면 백퍼 이 오류가 발생할것임 (버전 높은 사람들)
Database "mem:testdb" not found, either pre-create it or allow remote database creation (not recommended in secure environments)
spring.datasource.url=jdbc:h2:mem:testdb
application.proerties에 코드 추가할 때 이 부분이 핵심입니다. 이거 꼭 넣어주세요~!
이후 저는
Table "POSTS" not found; SQL statement: insert into posts (author, content, title) values ('author','content','title')
org.springframework.dao.InvalidDataAccessResourceUsageException: could not prepare statement; SQL [insert into posts (created_date, modified_date, author, content, title) values (?, ?, ?, ?, ?)]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement
이라는 오류와 또 한 세시간 싸웠는데요... 여러분은 그러지 마시라고...
이거 보고 해결했습니다.
해결방법
test package의 application.properties에 db dialect를 h2용으로 설정하니 해결되었다!
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
참고로 저는 test package의 application.properties를 걍 main application.properties로 착각해서 계속 그거만 수정해놓고
왜않되!!!!!!! 상태로 두시간을 보냈습니다. 여러분은 그러지 마세요...
Connect 버튼 누르면 이렇게 POSTS가 제대로 나와야함요
함튼 이거 하시고 책대로 쿼리 실행하고 생성하고 주소 쳐서 들어가서 확인하시면 됩니다
3️⃣ JPA Auditing으로 생성시간/수정시간 자동화하기
- 보통 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함한다.
- 언제 만들어졌는지, 언제 수정되었는지 등은 차후 유지보수에 중요한 정보이기 때문
- 생성시간/수정시간 정보를 매번 DB에 삽입하기 전, 갱신하기 전에 코드로 직접 등록/수정할 수도 있지만
JPA Auditing를 이용하면 이러한 생성시간/수정시간을 자동화할 수 있다.
💚 LocalDate 사용
domain 패키지에 BaseTimeEntity 클래스 생성
package com.jojoldu.book.springboot.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
BaseTimeEntity 클래스는 모든 Entity의 상위 클래스가 되어 Entity들의 createdDate, modifiedDate를 자동으로 관리하는 역할을 한다.
📌 @MappedSuperclass
JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드를 칼럼으로 인식하도록 한다.
📌 @EntityListeners(AuditingEntityListener.class)
BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.
📌 @CreatedDate
Entity가 생성되어 저장될 때 시간이 자동으로 저장된다.
📌 @LastModifiedDate
조회한 Entity의 값을 변경할 때 시간이 자동으로 저장된다.
Posts 클래스가 BaseTimeEntity 상속받도록 변경
public class Posts extends BaseTimeEntity {
Application에 JPA Auditing 활성화 어노테이션 추가하기
package com.jojoldu.book.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing //JPA Auditing 활성화
@SpringBootApplication
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
}
JPA Auditing 테스트 코드 작성하기
위에 있습니다...
오류잡다가 필자잡을뻔했음
버전 내리고 올리고 gradle 뒤집고... 난리쳤던 챕터... 다시는 이러고싶지않다...
gradle은 크게 건들지 말자는 교훈을 얻었음!
💻 Reference
'Server > Spring Boot' 카테고리의 다른 글
[Spring Boot] Toy Project 게시판 만들기 (2) - 생성, 수정 (0) | 2023.12.17 |
---|---|
[Chapter 06] AWS 서버 환경을 만들어보자 - AWS EC2 (1) | 2023.12.17 |
[Spring Boot] 스프링의 계층구조, 요청 응답 과정 (1) | 2023.12.17 |
[Spring Boot] 게시판 만들기 (1) - ERD 설계, MySQL 연결 (0) | 2023.11.13 |
[Chapter 05] 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (0) | 2023.10.16 |