@Injectable()
export class SetQuery {
protected query: SelectQueryBuilder<Keyword>;
@InjectRepository(Keyword)
protected keywordRepository?: Repository<Keyword>;
@InjectRepository(KeywordUser)
protected keywordUserRepository?: Repository<KeywordUser>;
// 키워드가 유저에게 존재하는지 확인
protected setIsExistKeyword() {
this.query.leftJoin(`KeywordUser`, `KU01`, `K01.id = KU01.keywordId`);
this.query.addSelect(
`CASE WHEN KU01.keywordId IS NULL THEN false ELSE true END AS isExistKeyword`,
);
}
// 키워드 검색
protected setSearchByKeyword(keyword: string) {
this.query = this.query.where(`K01.keyword LIKE :keyword`, {
keyword: `%${keyword}%`,
});
}
// 유저 아이디로 키워드 검색
protected setWhereByUserId(userId: number) {
this.query = this.query
.innerJoin(`KeywordUser`, `KU01`, `K01.id = KU01.keywordId`)
.innerJoin(`user`, `U01`, `KU01.userId = U01.id`)
.where(`U01.id = :userId`, { userId });
}
// limit 설정
protected setLimit(page?: number, limit?: number) {
page = page ?? 1;
limit = limit ?? 10;
const offset = (page - 1) * limit;
this.query = this.query.limit(limit).offset(offset);
}
}
export class FindAllKeywordQueryBuilder extends SetQuery {
constructor() {
super();
}
// 모든 키워드를 찾기
public async findAll(findAllKeywordDto: FindAllKeywordDto) {
this.query = this.keywordRepository
.createQueryBuilder(`K01`)
.select([
`K01.id AS keywordId`,
`K01.keyword AS keyword`,
`K01.createdAt AS createdAt`,
`K01.count AS count`,
])
.groupBy(`K01.id`);
this.setSearchByKeyword(findAllKeywordDto.keyword);
this.setLimit(findAllKeywordDto.page, findAllKeywordDto.limit);
this.setIsExistKeyword();
return await this.query.getRawMany();
}
}
export class FindAllByUserIdQueryBuilder extends SetQuery {
constructor() {
super();
}
// 해당 유저가 가진 키워드만 조회
public async findAllByUserId(findKeywordByUserIdDto: FindKeywordByUserIdDto) {
this.query = this.keywordRepository
.createQueryBuilder(`K01`)
.select([`K01.*`]);
this.setWhereByUserId(findKeywordByUserIdDto.userId);
this.setLimit(findKeywordByUserIdDto.page, findKeywordByUserIdDto.limit);
this.setIsExistKeyword();
return await this.query.getRawMany();
}
}
export class FindAllRecomendedKeywordQueryBuilder extends SetQuery {
constructor() {
super();
}
// 추천 키워드 찾기
public async findAllRecomendedKeyword(
findRecommentKeywordDto: FindRecommentKeywordDto,
) {
this.query = this.keywordRepository
.createQueryBuilder(`K01`)
.select([
`K01.id AS keywordId`,
`K01.keyword AS keyword`,
`K01.createdAt AS createdAt`,
`K01.count AS count`,
])
.groupBy(`K01.id`);
switch (findRecommentKeywordDto.recomendType) {
case 'recent':
this.query.orderBy(`K01.id`, `DESC`);
this.setLimit(1, findRecommentKeywordDto.limit);
break;
case 'popular':
this.query.orderBy(`K01.count`, `DESC`);
this.setLimit(1, findRecommentKeywordDto.limit);
break;
case 'recommend':
this.query.orderBy(`K01.count`, `DESC`);
this.setLimit(1, findRecommentKeywordDto.limit);
break;
}
return await this.query.getRawMany();
}
}
쿼리를 작성하는건 항상 생각하게된다.
아무랩스의 회사에서 배운점은 SQL 쿼리를 작성할 때 정말 필요한 JOIN, WHERE등의 조건을 걸어야 한다는 것이다.
예를 들어 title에서 특정 키워드로 검색을 한다면 매개변수가 ""로 들어왔을 때,
WHERE title LIKE '%%'; 이런 WHERE문을 붙이는게 아니라 아예 WHERE문이 붙지 않도록 만들어야 한다.
안그래도 SQL쿼리는 느린데 필요하지 않은 조건이 붙는다면 데이터를 가져오는 것이 느린데에 큰 지분을 차지하게 될 것이다.
@Injectable()
export class SetQuery {
protected query: SelectQueryBuilder<Keyword>;
@InjectRepository(Keyword)
protected keywordRepository?: Repository<Keyword>;
@InjectRepository(KeywordUser)
protected keywordUserRepository?: Repository<KeywordUser>;
// 키워드가 유저에게 존재하는지 확인
protected setIsExistKeyword() {
this.query.leftJoin(`KeywordUser`, `KU01`, `K01.id = KU01.keywordId`);
this.query.addSelect(
`CASE WHEN KU01.keywordId IS NULL THEN false ELSE true END AS isExistKeyword`,
);
}
// 키워드 검색
protected setSearchByKeyword(keyword: string) {
this.query = this.query.where(`K01.keyword LIKE :keyword`, {
keyword: `%${keyword}%`,
});
}
// 유저 아이디로 키워드 검색
protected setWhereByUserId(userId: number) {
this.query = this.query
.innerJoin(`KeywordUser`, `KU01`, `K01.id = KU01.keywordId`)
.innerJoin(`user`, `U01`, `KU01.userId = U01.id`)
.where(`U01.id = :userId`, { userId });
}
// limit 설정
protected setLimit(page?: number, limit?: number) {
page = page ?? 1;
limit = limit ?? 10;
const offset = (page - 1) * limit;
this.query = this.query.limit(limit).offset(offset);
}
}
나는 이 부모쿼리를 작성해서 다른 클래스들이 상속받도록 만들었다.
여러가지 조건을 이용해 필요한 키워드만 가져올 수 있어야 하기때문에 키워드 검색, 특정 유저가 가진 키워드 검색 등
JOIN이나 WHERE를 중점적으로 부모에게 작성했다.
자식 클래스들은 이 부모 클래스를 상속받아 필요한 WHERE, JOIN을 쿼리에 붙이도록 만든다.
조금 더 생각해보면 setLimit같은 함수는 아마 많은 SELECT쿼리에서 사용하게 될 것이기 때문에 SetQuery의 부모 클래스를 하나 더 만들어 공용의 메소드로 넣어줘도 좋을 것 같다.
분명 이 방식이 최고의 방식은 아닐 것 같다. 하지만 지금 작성한 현재의 나의 시점에서는 나쁘지 않은 방법으로 여러가지 쿼리를 작성할 수 있는 빌더를 만들었다고 생각한다.
어떻게하면 더 좋은 코드를 만들고 더 쉽게 알아볼 수 있는 코드를 만들 수 있을까?
주기적으로 코드를 볼 때마다 고민을 하는 내 모습이 나쁘지는 않은 것 같다.