스프링 어플리케이션을 개발하면서 보통은 jpa로 개발을 진행하고 있다.
그러나 어떤 경우에는 native 쿼리를 활용하여 퍼포먼스를 끌어 올리거나 조금 더 DB와 근접한 쿼리를 사용하고 싶을 때가 있었다.
간단한 쿼리의 경우 NamedJdbcTemplate을 써도 괜찮을 거 같다. 하지만 쿼리가 복잡해지고 관리해야 하는 쿼리의 양이 많아지는 경우에는 마이바티스가 괜찮은 선택인것 같아서 도입하기로 했다
config 파일
@Configuration
@RequiredArgsConstructor
@MapperScan(basePackages = {"com.example.infra.db.*"}, annotationClass = Mapper.class)
public class KpopDbMybatisConfiguration {
private final KpopDbPropertyBinder kpopDbPropertyBinder;
@Bean
public DataSource kpopMybatisDataSource() {
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
dataSourceBuilder.url(kpopDbPropertyBinder.getJdbcUrl());
dataSourceBuilder.username(kpopDbPropertyBinder.getUsername());
dataSourceBuilder.password(kpopDbPropertyBinder.getPassword());
return dataSourceBuilder.build();
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
sqlSessionFactory.setDataSource(kpopMybatisDataSource());
sqlSessionFactory.setTypeAliasesPackage("com.example.infra.db.*");
sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/**/*.xml"));
org.apache.ibatis.session.Configuration configuration =new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@MapperScan을 사용하여 mapper xml파일의 쿼리와 매핑되는 java interface가 위치하는 위치를 지정해준다.
annotationClass는 반드시 지정해주도록 한다. 보통은 mybatis에서 제공하는 @Mapper class를 지정해주면 될것이다.
첫번째로는 DataSource 의존성이 필요하다.
코드에서 보다시피 DataSource Bean을 먼저 등록했다.
그리고 SqlSessionFactory Bean이 필요하다.
dataSource를 설정하고, typeAlaisesPackage를 문자열로 등록하게끔 되어 있는데 클래스 방식으로 등록할 수 없는것이 아쉽다.
그리고 mapperLocations를 등록해줘야 하는데 이 부분은 classpath에 mybatis mapper파일이 위치한 곳을 명시해주면 된다.
mapperUnderscoreToCamelCase를 true로 해주었는데 예상하기로는 보통 DB에서는 underscore를 많이 쓰는데 이 방식을 camelCase의 dto형식으로 변환이 가능하게 해주는 것으로 보인다. (ex: DB: created_date -> dto: createdDate)
SqlSessionTemplate은 정확히 어떤 역할인지 모르겠지만 설정된 SqlSessionFactory를 참조하여 생성한다.
@Mapper
public interface IpStatisticsMybatisRepository {
...
List<IpUseRowDto> findIpUseCountAndRank(String startDate, String endDate, Integer shopNo, Long artistId);
...
}
보통 나는 jpa는 UserJpaRepository, queryDsl은 UserQueryDslRepository, jdbcTemplate은 UserJdbcRepository와 같은 형식으로 repository의 이름을 정하는데 mybatis는 UserMybatisRepository 형식으로 짓는 편이다.
이 부분은 통계에 관련한 쿼리이기 때문에 위와 같이 이름을 지정하였다.
위치는 위에 config에서 지정한 "com.example.infra.db.*" 패키지 내부 어딘가에 적당한 곳에 지정해주었다.
쿼리에서의 응답형과 (보통 select에서는 List 형식이 될거라고 예상한다) 인자로 받을 파라미터를 지정한다.
mapper xml 파일 작성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.infra.db.kpop.domain.statistics.ip.repository.IpStatisticsMybatisRepository">
...
<select id="findIpUseCountAndRank" resultType="com.example.infra.db.kpop.domain.statistics.ip.dto.IpUseRowDto">
select
results.ip_name,
case
when results.ip_low_quality_preview_url != null
then results.ip_low_quality_preview_url
else results.ip_preview_url
end as image_url,
results.ip_use_count as ip_use_count
from (
select
ip.name as ip_name,
resource.resource_type as ip_resource_type,
resource.preview_url as ip_preview_url,
resource.low_quality_preview_url as ip_low_quality_preview_url,
count(ip.id) as ip_use_count
from dtb_ip ip
inner join dtb_design_ip_relation designIpRelation
on designIpRelation.ip_id = ip.id
inner join dtb_design design
on design.id = designIpRelation.design_id
inner join dtb_artist artist
on artist.id = ip.artist_id
inner join dtb_member member
on design.member_id = member.id
inner join dtb_resource resource
on ip.resource_id = resource.id
where
design.created_at between DATE_FORMAT(#{startDate},'%Y-%m-%d %H:%i:%s') and DATE_FORMAT(#{endDate},'%Y-%m-%d %H:%i:%s')
and artist.id = #{artistId}
<if test="shopNo != null">
and member.shop_no = #{shopNo}
</if>
group by ip.id
) results
order by results.ip_use_count desc
</select>
...
</mapper>
먼저 mapper에 namespace를 연결하고자 하는 inerface (repository)를 입력해준다.
id는 위에 inerface의 메소드 명과 동일하게 맞추어서 작성해주면 되며 (findIpUseCountAndRank), 내부에 쿼리를 작성해주면 된다.
중간 중간 보이는 #{startDate}이런 방식은 메소드를 통해 전달되는 파라미터를 매핑해주는 mybatis의 표현 방식이다.
resultType의 경우 해당 쿼리의 응답형을 작성해둔 dto에 매핑시키는 정보로 활용된다.
public class IpUseRowDto {
private String ipName;
private String imageUrl;
private Long ipUseCount;
private Integer rank;
}
@Slf4j
@Service
@RequiredArgsConstructor
public class IpStatisticsService {
private final IpStatisticsMybatisRepository ipStatisticsMybatisRepository;
...
public IpUseExcelFileDto extractStatistics(LocalDateTime startDate, LocalDateTime endDate, Integer shopNo, Long artistId) {
...
List<IpUseRowDto> ipUseRowDto = ipStatisticsMybatisRepository.findIpUseCountAndRank(startDateStr, endDateStr, shopNo, artistId);
...
}
...
mybatisRepository를 주입받아온 이후 repository에 작성한 메소드의 파라미터를 함께 전달해서 실행하면, 쿼리에 문제가 없고 파라미터가 정확하게 전달이 되었다면 DB에서 검색한 내용을 파싱해서 지정한 dto 형식으로 돌려주는 것을 확인할 수 있을 것이다.
나의 경우에는 기존에 jpa로 버겁게 통계쿼리를 개발해서 excel 파일을 추출하는데에 약 5분 이상 걸리던 작업을 40초 정도로 줄인바 있다.