spring, db에 의존적인 sql 및 함수 다루기 (mysql ngram과 함께)
ngram을 사용할 일이 있었으며, 마침 mysql이 지원하였다
ngram?
- ngram은 인덱스 중 하나로 자동완성에 자주 사용하는 기능이다.
- 단어를 정해진 갯수로 자른 모든 조합을 인덱스로 배치한 후 그 단어를 맵핍하는 방법이다.
- 쿼리 : eating
- 사이즈 : 3
- 결과 : eat, ati, tin, ing
- ngram에 대하여 쿼리를 할 경우 DB는 다음과 같으 방식으로 응답한다.
- 쿼리 : eat
- 결과 : ‘eat’, ‘eat’ting, w’eat’her, sw’eat’
- ngram은 보통 자동완성에 사용한다.
- 쿼리 : 노트북
- 응답 : 삼성’노트북’, ‘노트북’케이스 등
mysql의 지원
- 한편, mysql은 ngram을 지원하며 매우 간단하게 사용 가능하다. 성능이나 복잡한 기능이 필요하지 않을 경우 mysql로 테스트 해보기를 추천한다!
-- my_table의 word를 ngram으로 만든다
ALTER TABLE my_table ADD FULLTEXT INDEX my_table_ngram_word (word) WITH PARSER ngram;
- 쿼리는 일반 match라는 mysql의 특화 함수를 사용한다.
- where 문으로 해당 인덱스에 대한 조회를 수행하며
- order by를 통해 일치 수준에 따라 정렬한다.
select *
from
my_table tb
where
match(tb.word) against (?1 in natural language mode)>0
order by
match(tb.word) against (?1 in natural language mode) desc,
match(tb.word) against (?1 in boolean mode) desc
-- ?1 = ord
- db에 존재했던 값과, 쿼리의 응답 값 및 정렬 순서는 다음과 같다.
- db : [run, swordsman, sword, wording, word]
- response : [word, wording, sword, swordsman]
JDBCTemplate을 사용하여 ngram index 사용하기
- 현재 spring-data-jpa와 함께 ddl-auto=create을 사용 중이다.
- DB에 의존적인 인덱스를 jpa가 생성하지 못한다. 별도로 ngram 인덱스를 생성할 ddl을 작성 및 요청해야 한다.
- JDBCTemplate을 사용하여 작성하였다. 스프링 컨텍스트가 완료 된 후 동작하도록 CommandLineRunner를 사용하였다.
import com.zaxxer.hikari.HikariConfig;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Component
@RequiredArgsConstructor
public class Run implements CommandLineRunner {
private final HibernateProperties hibernateProperties;
private final HikariConfig hikariConfig;
private final DataSource dataSource;
@Override
public void run(String... args) {
log.info("hibernate ddl-auto : {}", hibernateProperties.getDdlAuto());
log.info("db driver : {}", hikariConfig.getDriverClassName());
if(hikariConfig.getDriverClassName().toLowerCase().contains("mysql")
&& hibernateProperties.getDdlAuto().toLowerCase().contains("create")){
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
log.info("query : ALTER TABLE my_table ADD FULLTEXT INDEX my_table_ngram_word (word) WITH PARSER ngram;");
jdbcTemplate.execute("ALTER TABLE my_table ADD FULLTEXT INDEX my_table_ngram_word (word) WITH PARSER ngram;");
}
}
}
MySQL8Dialect을 사용하여 mysql 방언 사용히기
-
jpa에서 MySQL에 특화된 쿼리를 사용하기 위해서는 MySQL8Dialect을 사용한다.
- 작성 및 사용은 다음과 같다. MySQL8Dialect 구현체와 함께 spring.jpa.properties.hibernate.dialect 에 해당 클래스의 경로를 작성한다.
- ?1과 ?2는 String.format처럼 인자의 순서에 따라 동적으로 입력된다.
import org.hibernate.dialect.MySQL8Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.type.StandardBasicTypes;
public class MySQL8DialectCustom extends MySQL8Dialect {
public MySQL8DialectCustom(){
super();
registerFunction("match_bool", new SQLFunctionTemplate(StandardBasicTypes.DOUBLE, "match(?1) against (?2 in boolean mode)"));
registerFunction("match_nat", new SQLFunctionTemplate(StandardBasicTypes.DOUBLE, "match(?1) against (?2 in natural language mode)"));
}
}
spring:
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
dialect: com.example.MySQL8DialectCustom
- queryDSL을 사용 중이었으며 다음과 같이 코드를 작성하였다.
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberTemplate;
NumberTemplate<Double> matchBool = Expressions.numberTemplate(Double.class,
"function('match_bool',{0},{1})", qMyTable.word, query);
NumberTemplate<Double> matchNat = Expressions.numberTemplate(Double.class,
"function('match_nat',{0},{1})", qMyTable.word, query);
List<MyTable> fetch = queryFactory
.select(qMyTable)
.from(qMyTable)
.where(matchNat.gt(0))
.orderBy(matchNat.desc(), matchBool.desc())
.fetch();
mysql에 크게 의존적인 코드
- DB를 변경할 일이 없고 자동완성 기능을 간편하게 구현할 목적이라면, 위와 같이 코드를 작성할 수 있다.
- mariadb의 경우, 해당 쿼리 문법은 존재하지만 라이브러리는 내장되어 있지 않다. 별도의 라이브러리를 설치해야 하는 것 같다.