실제 활동중인 커뮤니티를둘러보니 게시글 목록, 게시글 상세보기 등의 View단에 작성일자 표기같은 부분들이 아쉬웠다.
그래서 수정할 부분은
- User, Post Entity JPA Auditing 추가 (createDate, modifiedDate)
- 작성일 추가
바로 바꿔보자
Jpa Audit
생성일자, 수정일자 등을 엔티티가 만들어질때 수동으로 넣어줘도 되지만,
JPA를 사용해 도메인을 데이터베이스에 매핑할때 생성일, 수정일 같이 공통적으로 가지는 칼럼,필드들을 따로 분리해서 관리할 수 있는데 JPA Audit을 이용하면 Spring Data JPA에서 이 칼럼,필드들을 자동으로 채워준다.
Main Entry
@SpringBootApplication
@EnableJpaAuditing
public class EmotionCommunityApplication {
public static void main(String[] args) {
SpringApplication.run(EmotionCommunityApplication.class, args);
}
}
Spring Data Jpa에서 Audit를 사용하려면 Spring Boot 설정클래스에 @EnableJpaAuditing을 달아줘야 Jpa Auditing이 작동한다. 보통 스프링부트를 실행시키는 클래스 위에 달아준다.
Time Entity(Auditing Abstract Class)
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeEntity {
@CreatedDate
private LocalDate createdDate;
@LastModifiedDate
private LocalDate modifiedDate;
}
@EntityListeners
해당 엔티티가 영속성 컨텍스트에서 DB에 적용하기 전, 후에 커스텀 콜백을 요청할 수 있게한다.
파라미터로 커스텀 콜백을 요청할 클래스를 지정해준다.
AuditingEntityListener Class를 넘기면 생성, 업데이트시 @PrePersist, @PreUpdate가 걸려있어 생성, 수정 될 때 커스텀 콜백이 걸린다.
@Configurable
public class AuditingEntityListener {
private @Nullable ObjectFactory<AuditingHandler> handler;
/**
* Configures the {@link AuditingHandler} to be used to set the current auditor on the domain types touched.
*
* @param auditingHandler must not be {@literal null}.
*/
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
this.handler = auditingHandler;
}
/**
* Sets modification and creation date and auditor on the target object in case it implements {@link Auditable} on
* persist events.
*
* @param target
*/
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
/**
* Sets modification and creation date and auditor on the target object in case it implements {@link Auditable} on
* update events.
*
* @param target
*/
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markModified(target);
}
}
}
}
@MappedSuperClass
해당 엔티티는 부모 클래스이고 자식 클래스가 속성만 상속받아 사용하고 싶을 때 쓴다.
@ CreateDate, LastModifiedDate
Spring Data에서 추상화한 어노테이션으로 해당 엔티티가 생성, 수정될때 어노테이션을 설정한 필드, 메소드, 어노테이션 자동으로 채워준다.
Post Entity
@Getter
@NoArgsConstructor
@Entity
public class Post extends TimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
@Builder
public Post(String title, String content, User user) {
this.title = title;
this.content = content;
this.user = user;
}
public void edit(PostEdit postEdit) {
this.title = postEdit.getTitle();
this.content = postEdit.getContent();
}
public void addComment(Comment comment) {
this.comments.add(comment);
comment.setPost(this);
}
}
게시글 목록을 보여줄 때 작성일자를 보여주고 싶기 때문에 PostDto에 추가된 Audit Column을 추가해준다.
PostResponseDto
@Getter
public class PostResponse {
private Long id;
private String title;
private String content;
private String nickname;
private String username;
private LocalDate createDate;
private List<CommentResponse> commentResponses;
@Builder
public PostResponse(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.content = post.getContent();
this.nickname = post.getUser().getNickname();
this.username = post.getUser().getUsername();
this.createDate = post.getCreatedDate();
this.commentResponses = post.getComments().stream().map(CommentResponse::new).collect(Collectors.toList());
}
}
User Entity
package com.jackcomunity.emotionCommunity.entity;
import com.jackcomunity.emotionCommunity.util.Roles;
import com.jackcomunity.emotionCommunity.util.TimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@NoArgsConstructor
public class User extends TimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String nickname;
@Column(nullable = false)
private String email;
@Column(nullable = false, length = 100)
private String password;
@Enumerated(EnumType.STRING)
private Roles role;
@OneToMany(mappedBy = "user")
private List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "user")
private List<Comment> comments = new ArrayList<>();
@Builder
public User(String username, String nickname, String email, String password, Roles role) {
this.username = username;
this.nickname = nickname;
this.email = email;
this.password = password;
this.role = role;
}
public void addComment(Comment comment) {
comment.setUser(this);
comments.add(comment);
}
}
Thymeleaf View
<table class="table table-striped">
<thead>
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">작성자</th>
<th scope="col">작성자</th>
</tr>
</thead>
<tbody>
<tr th:each="post : ${posts.contents}">
<th th:text="${post.id}">1</th>
<td><a th:text="${post.title}" th:href="@{|/board/posts/${post.id}|}">Mark</a></td>
<td th:text="${post.nickname}">작성자 들어갈 곳</td>
<td th:text="${post.createDate}">작성시간 들어갈 곳</td>
</tr>
</tbody>
</table>
결과
개선
Auditig 시 날짜뿐 아니라 누가 만들었는지도 Spring Security를 이용하면 적용할 수 있다고 하는데 연관관계 맵핑보다 더 나은 방법인지 공부하고 적용해 볼만한 것 같다.