Query DSL
Ex) 여러 개의 검색 필드(모델 ID, 모델명, 등)를 입력 받아서 해당 조건을 만족하는 엔티티를 불러오려고 한다.
- 그런데 검색의 특성상 콤보 박스에서 선택(null) 상태로 두고 검색 버튼을 누르는 경우가 있다.
- 그런 경우에는 해당 조건에 상관없이 모든 데이터를 보여주려고 한다.
- 예를 들어, 검색 필드 중에서 "모델 유형" 필드가 있을 때, 여기를 비운 상태로 검색을 누르면 모든 모델 유형에 대한 데이터를 가져오려고한다.
<hide/>
@Generated("com.querydsl.codegen.EntitySerializer")
public class QModel extends EntityPathBase<Model> {
private static final long serialVersionUID = 1612220557L;
public static final QModel model = new QModel("model");
public final StringPath modelId = createString("modelId");
public final StringPath modelNm = createString("modelNm");
...
public QModel(String variable) {
super(Model.class, forVariable(variable));
}
..
}
참고)
https://github.com/rbsks/WorkDuo_dev/blob/master/src/main/java/com/workduo/group/gropcontent/repository/query/impl/GroupContentQueryRepositoryImpl.java
https://sas-study.tistory.com/393
- ModelQuery 클래스를 만들어서 쿼리 역할을 하는 메서드를 작성한다.
- 다음과 같이 BooleanBuilder를 이용했다.
- BooleanBuilder: 쿼리의 WHERE 절 조건을 만들어주는 클래스이다. 그런데 가독성이 떨어지기 때문에 BooleanExpression으로 바꿀 계획이다.
- and()를 걸어주면서 WHERE 조건에 맞춰서 내용을 추가한다.
- predicate() => WHERE 절 역할
- projection() => SELECT 역할
- StringUtil 클래스는 회사에서 직접 만든 클래스이다.
- isNotEmpty(): isEmpty() 메서드와 반대이다.
- isEmpty()는 매개변수가 null이거나 "" 인 경우 true를 반환하는 메서드이다.
- offset, limit는 페이징을 위해 필요한 변수이다. offset은 시작 인덱스값, limit은 보여줄 데이터 개수를 의미한다. 따라서 limit는 1이상의 값이 들어가야 데이터를 보여줄 수 있다.
<hide/>
@Component
@RequiredArgsConstructor
public class ModelQuery {
private final JPAQueryFactory queryFactory;
private final QModel model = QModel.model;
// WHERE
public BooleanBuilder predicate(Model modelVo) {
BooleanBuilder search = new BooleanBuilder();
if (StringUtil.isNotEmpty(modelVo.getModelId())) {
search.and(model.modelId.eq(modelVo.getModelId()));
}
..
return search;
}
// SELECT
private QBean<Model> projection() {
return Projections.bean(Model.class,
model.modelId,
model.modelNm,
...;
}
public List<Model> getListByPaging(Model modelVO) {
int offset, limit;
List<Model> resultList;
offset = (modelVO.getCurPage() - 1) * modelVO.getPageListSize();
limit = modelVO.getPageListSize();
resultList = queryFactory.select((projection()))
.from(modelVO)
.where(predicate(modelVO))
.orderBy(modelVO.modelId.asc())
.offset(offset)
.limit(limit)
.fetch();
return resultList;
}
}
- 오류
- Parameter 0 of constructor in kr.co.platform.feature.model.query.ModelQuery required a bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' that could not be found.
Action:Consider defining a bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' in your configuration.
- Parameter 0 of constructor in kr.co.platform.feature.model.query.ModelQuery required a bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' that could not be found.
- 원인: ModelQuery 라는 빈을 만들기 위해? JPAqueryFactory 유형의 빈이 필요하다는 뜻이다.
- 해결: JPAqueryFactory 유형의 빈을 구성 파일에 정의해준다.
- JPA config 설정해준다.
<hide/>
@Configuration
public class JPAConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
JPA 메서드 명명 규칙
- findByIdAndName(id, name): id, name 모두 일치하는 데이터만 가져온다.
- findByIdOrName(id, name): id나 name이 일치하는 데이터만 가져온다.
- 여러 조건 중 몇 개가 null 인 경우도 포함인지 null은 아니어야만 하는지 모르겠다.
- findByNameIsNull(): Name 필드가 null 인 데이터를 가져온다.
- 검색 조건을 위해서 다음과 같은 쿼리를 짰지만
- 아래 쿼리로 JPA 메서드 관련된 오류가 발생해서 QueryDSL로 방향을 바꿨다.
<hide/>
@Query(
" SELECT m FROM Model m " +
" WHERE (:modelId is null OR m.modelId = :modelId) "
+ " AND (:modelNm is null OR m.modelNm = :modelNm) "
+ " AND (:modelCd is null OR m.modelCd = :modelCd) "
+ " AND (:modelExeCd is null OR m.modelExeCd = :modelExeCd) "
+ " AND (:useYn is null OR m.useYn = :useYn) "
)
참고) https://zara49.tistory.com/130
HttpStatus code
- 200 - 정상
- 200: OK
- 201: CREATED(서버에 어떤 자원이 생성됐을 때)
- 204: NO_CONTENT(DELETE 요청 성공)
- 300: redirect URL 오류
- 예를 들어, OAuth 로그인처럼 어떤 URL에 연결하는 경우 연결된 주소에 이상이 있는 경우 발생한다.
- 400 - 클라이언트 오류
- 400: BAD_REQUEST
- 404: NOT_FOUND
- 409: CONFLICT (생성 실패 또는 수정 실패)
- 500 - 서버 오류
Java Serializable 인터페이스
- 객체를 직렬화할 수 있다. (객체를 바이트 스트림(byte stream)으로 변환)
- 객체를 파일이나 네트워크를 통해 전송하거나 저장 가능
- Serializable 인터페이스를 implements한 클래스는 ObjectOutputStream 클래스를 사용해서 객체를 직렬화할 수 있다.
- 직렬화된 객체는 ObjectOutputStream를 사용해서 파일이나 네트워크에 전송하거나 ObjectOutputStream 클래스를 사용해서 다시 역직렬화해서 객체로 복원 가능하다.
Paging(페이징)
- 페이지 별로 목록 조회하기
- Model를 매개변수로 넣어서
- Map<String, Object> 형태로 ("data", ModelList 정보), ("page", page 정보)를 넣어준다.
- 아래와 같이 commons 디펜던시를 추가한다.
- API의 기능 확장을 목표로 하는 라이브러리이다.
- 개발자들이 자주 사용하는 기능(문자열 처리, 배열 및 숫자 조작, 동시성 등 여러 순서가 지정된 데이터 구조 구현 등)을 제공
- 아래에서 ToStringBuilder 클래스를 사용하기 위해 추가했다.
<hide/>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
- BaseUtil
- ToStringstyle 클래스를 이용해서 문자열을 형식화할 수 있다. 필드의 출력 형식, 문자열의 시작과 끝에 사용되는 특수 문자, 필드값이 null일 때, 출력되는 문자열 등을 지정 가능하다. 다양한 출력 스타일을 제공한다.
- ToStringBuilder: 객체를 문자열로 표현할 때 사용된다.
- reflectionToString()은 반복적으로 append()로 여러 개를 붙일 필요 없이 한 번에 필드들을 이어 붙여주기 때문에 편리하다.
<hide/>
@Getter
@Setter
@Value
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public class BaseUtil implements Serializable {
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}
- PageUtil
- 주의) 해당 클래스는 프론트에서 url에 변수와 값을 param으로 넣어서 보내주기 때문에 변수명이 일치해야한다.
- BaseUtil 클래스를 상속한다.
- curPage: 현재 페이지 번호 (프론트에서 해당 정보를 보내준다. )
- paging: 목록에 대상 객체가 하나 이상 존재하면 true로 설정한다.
- pageListSize: 한 페이지당 보여줄 엔티티의 개수
- totalListSize: DB에 저장된 총 엔티티의 개수
- boolean paging은 totalListSize가 0인지 아닌지 여부로 판단한다.
- totalPage: 총 페이지 개수 (총 엔티티 개수를 10으로 나눠서 올림한 것과 같다.)
<hide/>
@Getter
@Setter
public class PageUtil extends BaseUtil {
private int currPage = 1;
private int pageListSize = 10; // 한 페이지에 나타낼 model 개수
private int startIndex;
private long totalListSize; // 모든 model 개수
private long totalPage; // 모든 페이지 수
private boolean paging = false;
public void setTotalListSize(long totalListSize) {
if (totalListSize > 0) {
this.totalListSize = totalListSize;
this.startIndex = (this.currPage - 1) * this.pageListSize;
this.totalPage = this.totalListSize % this.pageListSize == 0 ? this.totalListSize / this.pageListSize : this.totalListSize / pageListSize + 1; // 45개인경우는 5개 페이지 필요하고 , 40개의 경우 4개가 필요하다.
}
}
}
- BeanUtils
- 스프링에서 제공하는 클래스
- BeanUtils.copyProperties(source, target): source(원본 객체), target(복사 대상 객체)
- 필드의 개수가 많을 때 어떤 객체를 다른 객체에 복사할 때 사용된다.
- 예를 들어, Dto를 Entity로 바꾸거나 그 반대의 경우에 쓰면 편리하다.
- source 안에 getter(), target 안에는 setter()가 존재해야한다.
- SearchUtil 클래스는 PageUtil 클래스를 상속한다.
- searchLimit 안에는 데이터를 보여주기 위한 시작, 마지막 인덱스가 들어간다.
<hide/>
@Data
public class SearchUtil extends PageUtil {
private String searchRegSt;
private String searchRegEd;
private String searchAmdSt;
private String searchAmdEd;
@Value("[]")
private int[] searchLimit;
}
- Query 클래스
- 다음과 같이 페이징 정보를 넣어서 메서드에 쿼리 정보를 넣어준다.
<hide/>
<hide/>
@Component
@RequiredArgsConstructor
public class ModelQuery {
private final JPAQueryFactory queryFactory;
private final QModel model = QModel.model;
...
public List<Model> getListByPaging(Model modelVO) {
int offset, limit;
List<Model> resultList;
offset = (modelVO.getCurPage() - 1) * modelVO.getPageListSize();
limit = modelVO.getPageListSize();
resultList = queryFactory.select((projection()))
.from(modelVO)
.where(predicate(modelVO))
.orderBy(modelVO.modelId.asc())
.offset(offset)
.limit(limit)
.fetch();
return resultList;
}
}
LocalDate <=> String으로 바꾸기
- String => LocalDate
- DateTimeFormatter를 이용한다.
- modelVO는 프론트에서 넘어온 데이터로 model에 관한 정보를 String 형태로 담고 있다.
<hide/>
String startDtOfInput = modelVo.getSearchRegSt();
String endDtOfInput = modelVo.getSearchRegEd();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate startDt = LocalDate.parse(startDtOfInput, formatter);
LocalDate endDt = LocalDate.parse(endDtOfInput, formatter);
- LocalDate => String
- LocalDate 형태의 변수에 toString()을 붙이면 변환 완료!
에러 해결
404 Not Found
- 오류: 404 NOT FOUND
- 원인: GET 요청을 보낼 때 Header에 content-length가 체크되어 있음.
- 해결: content-length 체크 해제한다. content-length는 POST 요청을 보낼 때 필요하다.
JpaObjectRetrievalFailureException , NoSuchElementException - JPA 관련
"org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find kr.co.Model with id 아이디; nested exception is javax.persistence.EntityNotFoundException:
"trace": "java.lang.RuntimeException: modelId에 대해 존재하는 ModelParam 가 없습니다.\r\n\tat kr.co.ModelParamService.getModelParam(ModelService.java:34)\r\n\tat kr.co.
- 오류: 저장해둔 값에 대해
- findById() JPA 메서드가 안 먹힌다.
- 원인
- model <=> modelParam 이 일대다 매핑이 되어있다.
- modelParam 테이블에는 (FK) modelId= 3인 여러 행을 추가했으나 model 테이블에는 (PK) modelId = 3인 행이 존재하지 않아서 문제가 생김
- 즉, 연관관계 매핑을 해뒀으나 FK에 연결된 PK 값이 주 테이블에 존재하지 않아서 문제가 발생
- 해결
- modelParam 에 FK로 넣은 model_id 내용을 model 테이블에도 PK 값을 넣어서 서로 연결되어 있도록 해준다.
- findById() 안에 다른 테이블의 PK 값이 들어가는 경우에 다른 테이블에도 해당 데이터가 들어가 있어야한다. 가장 조회 메서드에서 가장 먼저 modelRepository.findById()를 하는데 이 테이블에 modelId가 없으면 당연히 null이기 때문이다.
찾아 보기
- QueryDSL 세팅 방법
'개발 일지 > 주간 개발 일지' 카테고리의 다른 글
[04월 3주차] Vue.js 3 과 Spring (0) | 2023.04.22 |
---|---|
[04월 2주차] JPA, Vue.js 3 기본 명령어 (0) | 2023.04.16 |
[03월 5주차] Thread API 전체적인 흐름 파악하기, 고정 소수점과 부동 소수점 (2) | 2023.04.01 |
[03월 4주차] OkHttp 예제, Thread와 ExecutorService (1) | 2023.03.26 |
[03월 3주차] JPA 연관 관계 매핑, 복합 키, 제네릭 클래스에 의존성 주입 (0) | 2023.03.18 |