커뮤니티 프로젝트

JpaRepository, Querydsl, 게시물 CRUD

쭈녁 2022. 11. 24. 18:57

게시물 CRUD 에 필요한 Repository 개발 및 게시물Controller작업에 대한 포스팅

 

기본CRUD는 JpaRepository를 활용하여 아래와 같이 작성하였다.

 

Querydsl 설정

dependencies {
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl-apt:$
{dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

위와같이 의존관계를 주입하고 Gradle을 통해 빌드하여 Q타입 객체가 생성되었는지 확인해야 한다.

 

SpringDataJpaPostRepository

public interface SpringDataJpaPostRepository extends JpaRepository<Post, Long> {

}

save, findById, delete와 같은 기본 쿼리를 JpaRepository를 상속받는 것 만으로 사용이 가능하다.

 

PostSearchCond

@Data
public class PostSearchCond {
    private Long id;

    private String title;
    private String contents;
    private String categoryType;


    public PostSearchCond(/**CategoryType categoryName,**/ String title, String contents, String categoryType) {
        this.title = title;
        this.contents = contents;
        this.categoryType = categoryType;
    }
    public PostSearchCond() {

    }


}

게시물을 찾아올 때 검색의 기준이 되는 정보를 담은 SearchCond객체 생성.

 

PostQueryPostRepository

@Repository
@Transactional
public class PostQueryPostRepository {


    private final EntityManager em;
    private final JPAQueryFactory query;
    public PostQueryPostRepository(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }

    public List<Post> findPost(PostSearchCond cond) {
        String title = cond.getTitle();
        String categoryType = cond.getCategoryType();
        String contents = cond.getContents();
        List<Post> posts = query
                .select(post)
                .from(post)
                .where(categorySame(categoryType),titleLike(title), contentLike(contents))
                .fetch();
        return posts;
    }


    private BooleanExpression categorySame(String categoryType) {
        if (StringUtils.hasText(categoryType)) {
            return post.categoryType.like(categoryType);
        }
        return null;
    }
    private BooleanExpression titleLike(String title) {
        if (StringUtils.hasText(title)) {
            return post.title.like("%" + title + "%");
        }
        return null;
    }
    private BooleanExpression contentLike(String contents) {
        if (StringUtils.hasText(contents)) {
            return post.title.like("%" + contents + "%");
        }
        return null;
    }

게시물의 리스트를 뽑아올 때의 동적 쿼리를 적용하기 위한 별도의 Repository를 작성한다.

Querydsl을 사용하려면 JPAQuertFactory가 필요하다. 그리고 JPAQuertFactory는 JAP쿼리를 작성하기 위해 EntityManager가 필요하다. 이는 JdbcTemplete에 쿼리 작성을 위한 DataSource를 주입하는 것과 유사하다.

 

Querydsl은 자바코드로 동적 코드를 편리하게 넣을 수 있는 장점이 있다.

위 코드는 제목, 내용의 검색 조건이 게시물에 포함되어 있는지 그리고 카테고리를 선택하여 게시물을 확인할 수 있도록 동적 쿼리를 작성하였다. where()에 여러 조건을 넣어 AND조건으로 쿼리를 날릴 수 있다.

 

PostService

@Service
@Repository
@RequiredArgsConstructor
@Transactional
public class PostService {
    private final PostQueryPostRepository postQueryPostRepository;
    private final SpringDataJpaPostRepository springDataJpaPostRepository;


    public Post save(Post post, Member member) {
        post.setView(0);
        post.setLikeCount(0);
        post.setMember(member);
        return springDataJpaPostRepository.save(post);
    }

    public void update(Long postId, PostUpdateDto updateParam) {
        Post findPost = springDataJpaPostRepository.findById(postId).orElseThrow();
        findPost.setTitle(updateParam.getTitle());
        findPost.setContents(updateParam.getContents());
        findPost.setCategoryType(updateParam.getCategoryType());
    }

    public Optional<Post> findById(Long id) {
        return springDataJpaPostRepository.findById(id);
    }

    public void delete(Long postId) {
        springDataJpaPostRepository.delete(
                springDataJpaPostRepository.findById(postId).orElseThrow(()-> new EntityNotFoundException("게시물이 존재하지 않습니다."))
        );
    }

    public List<Post> findPost(PostSearchCond postSearchCond) {
        return postQueryPostRepository.findPost(postSearchCond);
    }
}

save에선 API에서 받아올 정보 이외의 정보들을 추가적으로 set 해주었다.

Member 의 경우 Controller에서 시큐리티의 PrincipalDetail에서 Member에 대한 정보를 받아왔다. (아래 Controller 참고)

 

update에선 API에서 PostId를 파라미터 받아와 해당 게시물을 findById로 찾고 UpdateDto의 수정된 내용으로 다시 set하였다.

 

delete는 PostId로 게시물을 찾아오고 해당 게시물을 삭제하였다. 람다식으로 만일 null일 경우 EntityNotFoundException을 줬다.

 

findPost의 경우 위에 작성된 Querydsl의 동적 쿼리로 게시물 리스트를 뽑아오도록 작성했다.

 

PostUpdatDto

@Data
public class PostUpdateDto {
    private String title;
    private String contents;
    private LocalDateTime updated;

    private String categoryType;

    public PostUpdateDto(String title, String contents, String categoryType) {

        this.title = title;
        this.contents = contents;
        this.categoryType = categoryType;
        updated = LocalDateTime.now();
    }
}

 

PostController

@Controller
@RequiredArgsConstructor
@Slf4j
public class PostController {
    private final PostServiceInterface postService;

    @ModelAttribute("categoryTypes")
    public List<CategoryType> categoryTypes() {
        List<CategoryType> categoryTypes = new ArrayList<>();
        categoryTypes.add(new CategoryType("자유게시판", "자유게시판"));
        categoryTypes.add(new CategoryType("공연게시판", "공연게시판"));
        categoryTypes.add(new CategoryType("중고거래", "중고거래"));
        return categoryTypes;
    }

    @GetMapping("/posts")
    public String post(@ModelAttribute("postSearch") PostSearchCond postSearch, Model model) {
        List<Post> posts = postService.findPost(postSearch);
        model.addAttribute("posts", posts);
        return "/post/posts";
    }
    @GetMapping("/post/{postId}")
    public String post(@PathVariable long postId, Model model) {
        Post post = postService.findById(postId).get();
        model.addAttribute("post", post);
        return "/post/post";
    }
    @GetMapping("/posts/add")
    public String addForm(Model model) {
        model.addAttribute("post", new Post());
        return "/post/addForm";
    }

    @PostMapping("/posts/add")
    public String addPost(@ModelAttribute Post post, RedirectAttributes redirectAttributes, @AuthenticationPrincipal PrincipalDetail principalDetail) {
        Post savedPost = postService.save(post,principalDetail.getMember());
        redirectAttributes.addAttribute("postId", savedPost.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/post/{postId}";
    }

    @GetMapping("/post/{postId}/edit")
    public String editForm(@PathVariable Long postId, Model model, @AuthenticationPrincipal PrincipalDetail principalDetail) {
        Post post = postService.findById(postId).get();
        if (post.getMember().equals(principalDetail.getMember())) {
        model.addAttribute("post", post);
            return "/post/editForm";
        }
        return "/post/editFail";
    }

    @Transactional
    @PostMapping("/post/{postId}/edit")
    public String edit(@PathVariable Long postId, @ModelAttribute PostUpdateDto updateParam) {
        postService.update(postId, updateParam);
        return "redirect:/post/{postId}";
    }

    @Transactional
    @GetMapping("/post/delete/{postId}")
    public String delete(@PathVariable Long postId) {
        postService.delete(postId);
        return "post/delete";
    }

add의 경우 이전 Thymeleaf 게시글에서 작성했던 내용과 동일하다. 주된 변경 내용은 edit과 delet 부분이다.

 

edit

postService.findById(postId).get() : 파라미터로 받은 postId로 Post객체를 찾는다.

 

if (post.getMember().equals(principalDetail.getMember())) : 게시물의 member 객체와 시큐리티를 통해 가져온 member객에가 동일하면 수정폼으로 이동하도록, 아니라면 수정 실패를 안내하는 URL로 가도록 작성하였다.

 

delete

삭제의 경우 수정폼 내에서 삭제 가능하도록 작성하여 게시글의 작성자가 아니면 삭제하지 못하도록 하였다.

'커뮤니티 프로젝트' 카테고리의 다른 글

댓글 기능 개발 (댓글 저장 부분)  (0) 2022.12.02
회원정보 수정  (0) 2022.11.30
Spring Security 회원가입 및 로그인  (0) 2022.11.23
Spring Validator, 추가 Validator  (0) 2022.11.16
Thymeleaf  (0) 2022.11.15