Querydsl에 대해 찾아보고, 적용해 본 후?
내가 직접 간단한 설계를 해보고, Querydsl까지 적용해보고 싶다는 생각을 했다.
물론 너무 복잡한 건 아직 무리이다.
그리고 이번에 이 연습을 하면서 느낀 건, Query에 대해서도 더 공부가 필요하다는 것이다.
할 게 많군요..!
👉 DB 설계
DB 설계를 위해 이용한 툴은 ERD CLOUD이다.
아티스트가 메인인데, 엔터테인먼트를 메인으로 해버렸...
이건 뭐 필요에 따라 바꾸면 될 것 같다.
여러 가지 생각을 하다가, 구 덕질을 해본 입장으로?
이게 가장 생각하기 좋은 것 같아서, 이 주제로 해봤다.
내가 생각한 관계는 이러하다. (이해한 게 맞는지,,, 모르겠지만, 틀린 게 있다면 알려주시면 감사하겠습니다.)
- 엔터테인먼트-아티스트: 1:N
- 하나의 엔터테인먼트에는 여러 아티스트가 있다고 가정한다.
- 아티스트는 한 소속사만 가질 수 있다. (물론 요즘 여러 소속사에 속하기도 하지만... 하나에만 속한다고 가정한다.)
- 아티스트-앨범: 1:N
- 아티스트는 여러 개의 앨범을 가질 수 있다.
- 앨범-구입처: N:N
- 하나의 앨범은 여러 구입처에서 구입할 수 있다.
- 구입처에서는 여러 앨범을 가지고 있을 수 있다.
- 구입처-댓글: 1:N
- 한 구입처의 앨범에는 여러 개의 댓글이 달릴 수 있다.
이렇게 관계를 생각해 봤다.
댓글을 달 때는 가입한 사람만이 달 수 있게 하는 게 맞겠지만?
지금 상황에서는 꽤나 복잡해질 것 같고, 지금은 Querydsl을 연습해 보는 게 먼저라서 복잡한 건 배제했다.
0. Querydsl 관련 설정
💾 pom.xml
<!-- QueryDSL -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
QueryDSL 관련 의존성은 위처럼 설정을 했다.
보통 구글링을 해보면, 의존성 추가할 때 플러그인 설정도 같이 하는 것 같은데!
최신 버전에서는 플러그인 추가는 안 해도 된다.
그것 때문에 한참을 헤맸다...
💾 config/QuerytDslConfiguration.java
package com.example.querydsltest.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QueryDslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Querydsl 사용을 위한 설정 클래스도 추가한다.
1. entity
DB 설계한 것을 바탕으로 entity를 먼저 작성했다.
관계를 생각하며, join컬럼도 사용했다.
💾 Entertainment.java
package com.example.querydsltest.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Entertainment")
@Getter
@Setter
public class Entertainment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long enterId;
private String enterName;
}
💾 Artist.java
package com.example.querydsltest.entity;
import com.example.querydsltest.entity.Entertainment;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Artist")
@Getter
@Setter
public class Artist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long artistId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "enter_id")
private Entertainment enterId;
private String artistName;
}
💾 Album.java
package com.example.querydsltest.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Album")
@Getter
@Setter
public class Album {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long albumId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "artist_id")
private Artist artistId;
private String albumName;
private int albumPrice;
}
💾 Place.java
package com.example.querydsltest.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Place")
@Getter
@Setter
public class Place {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long placeId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "album_id")
private Album albumId;
private String placeName;
}
💾 Comment.java
package com.example.querydsltest.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Comment")
@Getter
@Setter
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long commentId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "place_id")
private Place placeId;
private String commentUser;
private String comment;
}
2. 임시 데이터
entity 생성한 이후에 한 번 실행을 해서, db 생성을 한 후에!
임시 데이터를 밀어 넣었다.
혹시라도 따라 할 사람이 있을지 모르겠지만..?!
참고하실 분들을 위해 임시 데이터도 공유한다.
-- Entertainment
insert into entertainment (enter_id, enter_name) values(1,"SM");
insert into entertainment (enter_id, enter_name) values(2,"YG");
insert into entertainment (enter_id, enter_name) values(3,"JYP");
insert into entertainment (enter_id, enter_name) values(4,"하이브");
-- Artist
insert into artist (enter_id, artist_name) values(1,"동방신기");
insert into artist (enter_id, artist_name) values(1,"샤이니");
insert into artist (enter_id, artist_name) values(1,"레드벨벳");
insert into artist (enter_id, artist_name) values(1,"에스파");
insert into artist (enter_id, artist_name) values(2,"빅뱅");
insert into artist (enter_id, artist_name) values(2,"블랙핑크");
insert into artist (enter_id, artist_name) values(2,"위너");
insert into artist (enter_id, artist_name) values(2,"악동뮤지션");
insert into artist (enter_id, artist_name) values(3,"2PM");
insert into artist (enter_id, artist_name) values(3,"데이식스");
insert into artist (enter_id, artist_name) values(3,"트와이스");
insert into artist (enter_id, artist_name) values(3,"스트레이키즈");
insert into artist (enter_id, artist_name) values(4,"방탄소년단");
insert into artist (enter_id, artist_name) values(4,"뉴진스");
insert into artist (enter_id, artist_name) values(4,"투모로우바이투게더");
insert into artist (enter_id, artist_name) values(4,"르세라핌");
-- Album
insert into album (artist_id, album_name,album_price) values(1,"Tri-Angle",12000);
insert into album (artist_id, album_name,album_price) values(1,"MIROTIC",15000);
insert into album (artist_id, album_name,album_price) values(5,"Bigbang Vol.1",13000);
insert into album (artist_id, album_name,album_price) values(5,"Stand Up",14000);
insert into album (artist_id, album_name,album_price) values(10,"Every DAY6 February",15000);
insert into album (artist_id, album_name,album_price) values(10,"The Book of Us : Gravity",10000);
insert into album (artist_id, album_name,album_price) values(13,"화양연화 Young Forever",13000);
insert into album (artist_id, album_name,album_price) values(13,"WINGS",12000);
-- Place
insert into place (album_id,place_name) values(1,"예스24");
insert into place (album_id,place_name) values(2,"알라딘");
insert into place (album_id,place_name) values(3,"교보문고");
insert into place (album_id,place_name) values(4,"예스24");
insert into place (album_id,place_name) values(5,"알라딘");
insert into place (album_id,place_name) values(6,"교보문고");
insert into place (album_id,place_name) values(7,"예스24");
insert into place (album_id,place_name) values(8,"알라딘");
insert into place (album_id,place_name) values(1,"교보문고");
-- Comment
insert into comment (place_id, comment_user, comment) values(1,"하늘","최고!!!");
insert into comment (place_id, comment_user, comment) values(2,"슬기","멋있어요!!!");
insert into comment (place_id, comment_user, comment) values(3,"홍","짱!!!");
insert into comment (place_id, comment_user, comment) values(4,"민","와우!!!");
insert into comment (place_id, comment_user, comment) values(5,"종이","최고네요!!!");
insert into comment (place_id, comment_user, comment) values(6,"제인","닥치고 구매!!!");
insert into comment (place_id, comment_user, comment) values(7,"미나","귀호강!!!");
insert into comment (place_id, comment_user, comment) values(8,"큘라","재구매 각!!!");
insert into comment (place_id, comment_user, comment) values(1,"미리","재구매!!!");
insert into comment (place_id, comment_user, comment) values(6,"데이","미쳤다!!!");
조회
select
e.enter_id , e.enter_name,
a.artist_name,
a1.album_name,
a1.album_price,
p.place_name ,
c.comment_user, c.comment
from entertainment e
inner join artist a on a.enter_id = e.enter_id
inner join album a1 on a1.artist_id = a.artist_id
inner join place p ON p.album_id = a1.album_id
inner join comment c on c.place_id = p.place_id ;
임시 데이터까지 모두 넣은 후에!
전체 db를 join해서 어떻게 나오는지 확인을 해본다.
최종적으로 나는 댓글이 있는 데이터들만 출력하고 싶다.
이제 이걸 QueryDSL을 이용해 작업하면 된다.
3. DTO
💾 ResponseDto.java
package com.example.querydsltest.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseDto {
private Long enterId;
private String enterName;
private String artistName;
private String albumName;
private int albumPrice;
private String placeName;
private String commentUser;
private String comment;
}
그전에 dto를 먼저 작성한다.
호출했을 때 위 필드들을 출력할 거다.
4. Repository
package com.example.querydsltest.repository;
import com.example.querydsltest.dto.EntertainmentResponseDto;
import com.example.querydsltest.entity.*;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
public class EntertainmentRepositoryImpl {
private final JPAQueryFactory queryFactory;
public EntertainmentRepositoryImpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
QEntertainment qEntertainment = QEntertainment.entertainment;
QArtist qArtist = QArtist.artist;
QAlbum qAlbum = QAlbum.album;
QPlace qPlace = QPlace.place;
QComment qComment = QComment.comment1;
public List<EntertainmentResponseDto> searchList() {
return queryFactory
.select(Projections.constructor(EntertainmentResponseDto.class,
qEntertainment.enterId,
qEntertainment.enterName,
qArtist.artistName,
qAlbum.albumName,
qAlbum.albumPrice,
qPlace.placeName,
qComment.commentUser,
qComment.comment))
.from(qEntertainment)
.innerJoin(qArtist).on(qArtist.enterId.enterId.eq(qEntertainment.enterId))
.innerJoin(qAlbum).on(qAlbum.artistId.artistId.eq(qArtist.artistId))
.innerJoin(qPlace).on(qPlace.albumId.albumId.eq(qAlbum.albumId))
.innerJoin(qComment).on(qComment.placeId.placeId.eq(qPlace.placeId))
.fetch();
}
}
그리고 위에서 쿼리 조회 시 작성했던 쿼리를 참고하여!
Querydsl 문법을 이용해, 다시 작성한다.
5. Service
💾 EntertainmentService.java
package com.example.querydsltest.service;
import com.example.querydsltest.dto.EntertainmentResponseDto;
import com.example.querydsltest.repository.EntertainmentRepositoryImpl;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@RequiredArgsConstructor
@Service
public class EntertainmentService {
private final EntertainmentRepositoryImpl entertainmentRepositoryImpl;
@Transactional
public List<EntertainmentResponseDto> searchList() {
return entertainmentRepositoryImpl.searchList();
}
}
전체 조회를 할 것이기 때문에, service에서는 전체 조회를 return 한다.
6. Controller
💾 EntertainmentController.java
package com.example.querydsltest.controller;
import com.example.querydsltest.dto.EntertainmentResponseDto;
import com.example.querydsltest.service.EntertainmentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping("/enter")
@RequiredArgsConstructor
public class EntertainmentController {
private final EntertainmentService entertainmentService;
@GetMapping("/list")
public ResponseEntity<List<EntertainmentResponseDto>> list() {
List<EntertainmentResponseDto> entertainment = entertainmentService.searchList();
if (!entertainment.isEmpty()) {
return ResponseEntity.ok(entertainment);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
}
그리고 컨트롤까지 작성을 한다.
http:/localhost:8080/enter/list
get으로 호출했을 때!
원하는 결과를 출력할 수 있다.
7. Postman으로 조회
포스트맨에서 호출을 해본다.
파라미터는 없기 때문에, 그냥 'send' 버튼을 클릭하면 된다.
오!
잘 나온다.
쿼리로 확인 시, 총 10개의 행이 출력되었는데!
[
{
"enterId": 1,
"enterName": "SM",
"artistName": "동방신기",
"albumName": "Tri-Angle",
"albumPrice": 12000,
"placeName": "예스24",
"commentUser": "하늘",
"comment": "최고!!!"
},
{
"enterId": 1,
"enterName": "SM",
"artistName": "동방신기",
"albumName": "Tri-Angle",
"albumPrice": 12000,
"placeName": "예스24",
"commentUser": "미리",
"comment": "재구매!!!"
},
{
"enterId": 1,
"enterName": "SM",
"artistName": "동방신기",
"albumName": "MIROTIC",
"albumPrice": 15000,
"placeName": "알라딘",
"commentUser": "슬기",
"comment": "멋있어요!!!"
},
{
"enterId": 2,
"enterName": "YG",
"artistName": "빅뱅",
"albumName": "Stand Up",
"albumPrice": 14000,
"placeName": "교보문고",
"commentUser": "홍",
"comment": "짱!!!"
},
{
"enterId": 2,
"enterName": "YG",
"artistName": "빅뱅",
"albumName": "Bigbang Vol.1",
"albumPrice": 13000,
"placeName": "예스24",
"commentUser": "민",
"comment": "와우!!!"
},
{
"enterId": 3,
"enterName": "JYP",
"artistName": "데이식스",
"albumName": "Every DAY6 February",
"albumPrice": 15000,
"placeName": "알라딘",
"commentUser": "종이",
"comment": "최고네요!!!"
},
{
"enterId": 3,
"enterName": "JYP",
"artistName": "데이식스",
"albumName": "The Book of Us : Gravity",
"albumPrice": 10000,
"placeName": "교보문고",
"commentUser": "제인",
"comment": "닥치고 구매!!!"
},
{
"enterId": 3,
"enterName": "JYP",
"artistName": "데이식스",
"albumName": "The Book of Us : Gravity",
"albumPrice": 10000,
"placeName": "교보문고",
"commentUser": "데이",
"comment": "미쳤다!!!"
},
{
"enterId": 4,
"enterName": "하이브",
"artistName": "방탄소년단",
"albumName": "화양연화 Young Forever",
"albumPrice": 13000,
"placeName": "예스24",
"commentUser": "미나",
"comment": "귀호강!!!"
},
{
"enterId": 4,
"enterName": "하이브",
"artistName": "방탄소년단",
"albumName": "WINGS",
"albumPrice": 12000,
"placeName": "알라딘",
"commentUser": "큘라",
"comment": "재구매 각!!!"
}
]
실제로도 10개가 출력된 것을 볼 수 있었다.
이 이후에는!
enterId, enterName, artistName, albumName, albumPrice, placeName 까지 동일한 내용이면 한 번만 출력이 되고!
commentUser, comment만 list에 담아서 출력될 수 있게 수정을 해보려고 한다.