1. JPA 소개
- 개발자는 객체지향적으로 프로그래밍하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행함.
- 개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 SQL에 종속적인 개발을 하지 않아도 됨
1) Spring Data JPA
- JPA : 인터페이스로서 자바 표준명세서이며, 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요함
ex) Hibernate, Eclipse, Link 등이 있음
- Spring Data JPA : 구현체들을 보다 쉽게 사용하고자 추상화 시킨 모듈로서, 관계는 아래와 같음.
(JPA <- Hibernate <- Spring Data JPA)
구현체 교체의 용이성 : Hibernate 외에 다른 구현체로 쉽게 교체하기 위함
저장소 교체의 용이성 : 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함
(Spring Data JPA, Spring Data Redis, Spring Data MongoDB 등등)
2) 요구사항 분석
게시판 기능 | 회원 기능 |
게시글 조회 | 구글/네이버 로그인 |
게시글 등록 | 로그인한 사용자 글 작성 권한 |
게시글 수정 | 본인 작성 글에 대한 권한 권리 |
게시글 삭제 |
2. Spring Data JPA 적용
1) build.gradle
- spring-boot-starter-data-jpa : 스프링 부트용 Spring Data Jpa 추상화 라이브러리이며,
스프링 부트 버젼에 맞춰 자동으로 JPA관련 라이브러리들의 버젼을 관리해줌
- h2 : 인메모리 관계형 데이터베이스이며, 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있음.
메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화 된다는 점을 이용하여 테스트 용도로 많이 사용됨
- 도메인 : 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역
buildscript {
ext{ //build.gradle에서 사용하는 전역변수를 설정하겠다는 의미
springBootVersion = '2.1.7.RELEASE' //spring-boot-gradle-plugin라는
// 스프링부트그레이들 플러그인의 2.1.7RELEASE를 의존성으로 받겠다.
}
repositories { //각종 의존성(라이브러리)들을 어떤 원격 저장소에서 받을지 정한다.
mavenCentral()
jcenter()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin : 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management' //스프링부트의 의존성을 관리해주는 플러그인!!!중요!!
group 'com.book'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
test {
useJUnitPlatform()
}
2) domain/posts/Posts
- @Entity : 테이블과 링크될 클래스임을 나타내며, 기본값으로 클래스의 카멜케이스 이름을
언더스코어 네이밍(_)으로 테이블 이름을 매칭함
JPA의 어노테이션
- @Id : 해당 테이블의 PK 필드를 나타냄
- @GeneratedValue : PK의 생성 규칙을 나타냄
- @Column : 테이블의 칼럼을 나타내며, 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 됨
기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용함(사이즈나 타입 변경 등)
- @NoArgsConstructor : 기본 생성자 자동 추가, public Posts() {} 와 같은 효과, 롬복의 어노테이션
- @Getter : 클래스 내 모든 필드의 Getter 메소드를 자동생성, 롬복의 어노테이션
- @Builder : 해당 클래스의 빌드 패턴 클래스를 생성하며, 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함
- 이 클래스는 Setter 메소드가 없고, 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 함
package com.jojoldu.book.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@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.author = author;
this.content = content;
}
}
3) domain/posts/PostsRepository
- MyBatis 에서 DAO라 불리는 DB Layer 접근자이고, JPA에선 Repository라고 부르며, 인터페이스를 생성함
- JpaRepository<Entity 클래스, PK 타입>를 상속하면 기본적인 CRUD 메소드가 자동으로 생성됨
- @Repository를 추가할 필요 없음
- Entity 클래스와 기본 Entity Repository는 함께 위치해야 함
package com.jojoldu.book.springboot.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {
//
}
3. Spring Data JPA 테스트 코드 작성
1) domain/posts/PostsRepositoryTest
- @After : junit에서 단위 테스트가 끝날때마다 수행되는 메소드를 지정.
보통은 배포 전 전체 테스트를 수행할 때 테스트 간 데이터 침범을 막기 위해 사용함
여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어,
다음 테스트 실행 시 테스트가 실패할 수 있음
- @postsRepository.save : 테이블 posts에 insert/update 쿼리를 실행함
id값이 있다면, update가 없다면 insert 쿼리가 실행됨
- @posts : 테이블 posts에 있는 모든 데이터를 조회해오는 메소드
//PostsRepositoryTest.java
package com.jojoldu.book.springboot.domain.posts;
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.test.context.junit4.SpringRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After
public void cleanup() {
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기(){
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder().title(title).content(content).author("akfkdhwk@gmail.com").build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
[PostsRepositoryTest 테스트 실행 및 결과]
- 실제로 실행된 쿼리 형태 확인을 위해 src/main/resources/application.properties 파일 생성함
- spring.jpa.show_sql=true (옵션 추가)
[쿼리 로그 확인]
- spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect (옵션 추가)
[MySQL 쿼리 로그 확인]
4. 등록/수정/조회 API 생성
- API를 만들기 위해서는 총 3개의 클래스가 필요함
Request 데이터를 받을 Dto |
API 요청을 받을 Controller |
트랜잭션, 도메인 기능 간의 순서를 보장하는 Service |
- Spring 웹 계층 : 비즈니스 로직 처리(Domain)
- Web Layer
=> 흔히 사용하는 컨트롤러(@Controller)와 JSP/Freemarker 등의 뷰 템플릿 영역
=> 이외에도 필터(@Filter), 인터셉터, 컨트롤러 어드바이스 등 외부 요청과 응답에 대한 전반적인 영역을 말함
- Service Layer
=> @Service에 사용되는 서비스 영역이고, 일반적으로 Controller와 Dao의 중간 영역에서 사용됨
=> @Transaction이 사용되어야 하는 영역이기도 함
- Repository Layer
=> Database와 같이 데이터 저장소에 접근하는 영역
- Dtos
=> Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체를 말하며, Dtos는 이들의 영역을 얘기함
=> 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등을 말함
- Domain Model
=> 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고, 공유할 수 있도록 단순화 시킨 것을 도메인 모델
=> 택시 앱이 있다면, 배차, 탑승, 요금 등이 모두 도메인이 됨
=> @Entity가 사용된 영역 역시 도메인 모델
=> 다만, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아님
=> VO처럼 값 객체들도 이 영역에 해당하기 때문
1) 등록
- web/PostsApiController
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.service.PostsService;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
//등록
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
- service/PostsService
=> @RequireArgsConstructor : final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성해줌 (@Autowired 사용안함)
=> 생성자를 직접 안쓰고 롬복 어노테이션을 사용하는 이유는
해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속 수정하는 번거로움을 해결하기 위함
package com.jojoldu.book.springboot.service;
import com.jojoldu.book.springboot.domain.posts.PostsRepository;
import com.jojoldu.book.springboot.web.dto.PostsSaveRequestDto;
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();
}
}
- web/dto/PostsSaveRequestDto
=> Entity 클래스를 Request/Response 클래스로 사용해서는 안됨
=> Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스로, Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경됨
=> 화면 변경은 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스 변경은 무리수임
=> View Layer와 DB Layer의 역할 분리를 해야하며,
실제로 Controller에서 결과값으로 여러 테이블을 조인해서 줘야 할 경우가 빈번하므로
Entity 클래스만으로 표현하기 어려운 경우가 많음
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.springboot.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity(){
return Posts.builder().title(title).content(content).author(author).build();
}
}
- web/PostsApiControllerTest
=> @SpringBootTest와 @TestRestTemplate를 사용하여 JPA기능까지 한번에 테스트함
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 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.web.server.LocalServerPort;
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 postsRepsitory;
@After
public void tearDown() throws Exception{
postsRepsitory.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 = postsRepsitory.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
}
[Posts 등록 API 테스트 결과]
2) 수정
- web/PostsApiController
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.service.PostsService;
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.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);
}
}
- web/dto/PostsResponseDto
=> PostsResponseDto는 Entity의 필드 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣음
=> 굳이 모든 필드를 가진 생성자가 필요하진 않으므로, Dto는 Entity를 받아 처리함
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.title = entity.getTitle();
this.content = entity.getContent();
this.author = entity.getAuthor();
}
}
- web/dto/PostsUpdateRequestDto
package com.jojoldu.book.springboot.web.dto;
import com.jojoldu.book.springboot.domain.posts.Posts;
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;
}
}
- web/dto/Posts
package com.jojoldu.book.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@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.author = author;
this.content = content;
}
//수정
public void update(String title, String content){
this.title = title;
this.content = content;
}
}
- service/PostsService
=> update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없고, 이유는 JPA의 영속성 컨텍스트 때문임
=> 영속성 컨텍스트란 엔티티를 영구 저장하는 환경임
=> JPA의 엔티티 메니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면,
이 데이터는 영속성 컨텍스트가 유지된 상태임
=> 이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영함
(Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없음 = dirty checking)
package com.jojoldu.book.springboot.service;
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 requestDto){
Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id ="+id));
posts.update(requestDto.getTitle(), requestDto.getContent());
return id;
}
public PostsResponseDto findById (Long id){
Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
return new PostsResponseDto(entity);
}
}
- web/PostsApiControllerTest
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.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 postsRepsitory;
@After
public void tearDown() throws Exception{
postsRepsitory.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 = postsRepsitory.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(title);
assertThat(all.get(0).getContent()).isEqualTo(content);
}
//수정
@Test
public void Posts_수정된다() throws Exception{
//given
Posts savedPosts = postsRepsitory.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> requestDtoHttpEntity = new HttpEntity<>(requestDto);
//when
ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestDtoHttpEntity, Long.class);
//then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
List<Posts> all = postsRepsitory.findAll();
assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
}
}
[Posts 수정 API 테스트 결과]
3) 조회
- 조회 기능은 실제 톰켓을 실행해서 확인
- 로컬 환경에선 데이터베이스로 H2를 사용하며, 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야 함
- application.properties 에 'spring.h2.console.enabled=true' 옵션 추가
- Application 클래스의 main 메서드를 실행
- http://localhost:8080/h2-console 접속 후, JDBC URL 확인 후에 Connect 버튼 클릭
=> JDBC URL : jdbc:h2:mem:testdb
[h2-console 화면]
- 현재 프로젝트의 H2를 관리할 수 있는 관리페이지로 이동
=> POSTS 테이블이 정상적으로 노출됨
=> SELECT * FROM posts; 쿼리문 실행하면 조회가 진행됨
[select 쿼리 실행 결과]
=> insert into posts (author, content, title) values ('author', 'content', 'title'); insert 쿼리 실행
[insert 쿼리 실행 결과]
- 등록된 데이터를 확인한 후, API를 요청
=> 'http://localhost:8080/api/v1/posts/1' 입력해 API 조회 기능을 테스트 함
5. JPA Auditing으로 생성시간/수정시간 자동화
- 엔티티에는 해당 데이터의 생성시간과 수정시간을 포함해야 함(언제 만들어지고, 수정되었는지 등은 차후 유지보수에서 중요함)
=> JPA Auditing을 사용함
1) LocalDate 사용
- domain/posts/BaseTimeEntity
=> 모든 Entity의 상위클래스가 되어 Entity들의 createDate, modifiedDate를 자동으로 관리하는 역할을 함
=> @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들도 컬럼으로 인식하도록 함
=> @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에 Auditing 기능을 포함시킴
=> @CreateDate : Entity가 생성되어 저장될 때 시간이 자동 저장됨
=> @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동 저장됨
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.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
- Posts 클래스가 BaseTimeEntity를 상속받도록 변경함
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.*;
@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.author = author;
this.content = content;
}
//수정
public void update(String title, String content){
this.title = title;
this.content = content;
}
}
- Application 클래스에 활성화 어노테이션 하나 추가함
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);
}
}
2) JPA Auditing 테스트 코드 작성
package com.jojoldu.book.springboot.domain.posts;
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.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After
public void cleanup() {
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기(){
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder().title(title).content(content).author("akfkdhwk@gmail.com").build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
@Test
public void BaseTimeEntity_등록(){
//given
LocalDateTime now = LocalDateTime.of(2022,02,27,0,0,0);
postsRepository.save(Posts.builder().title("title").content("content").author("author").build());
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
System.out.println(">>>>>>>>>>>>> createDate="+posts.getCreatedDate()+", modifiedDate="+posts.getModifiedDate());
assertThat(posts.getCreatedDate().isAfter(now));
assertThat(posts.getModifiedDate().isAfter(now));
}
}
- PostsRepositoryTest 클래스에 테스트 메소드 추가
[JPA Auditing 테스트 코드 결과]
'Web > SpringBoot' 카테고리의 다른 글
6. AWS 서버환경 - AWS EC2 (0) | 2022.03.04 |
---|---|
5. Spring security와 OAuth2.0으로 로그인 기능 구현 (0) | 2022.03.03 |
4. 머스테치로 화면 구성 (0) | 2022.02.28 |
2. SpringBoot에서 테스트 코드 작성 (0) | 2022.02.26 |
1. IntelliJ 활용한 SpringBoot 시작하기 (0) | 2022.02.25 |
댓글