Server : Spring Boot, Spring Data Jpa, Querydsl
Database : MySQL
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.6.6"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
kotlin("plugin.jpa") version "1.6.10"
kotlin("kapt") version "1.6.10" // kapt 등록
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
// Q파일 생성 경로
sourceSets["main"].withConvention(org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet::class) {
kotlin.srcDir("$buildDir/generated/source/kapt/main")
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
runtimeOnly("mysql:mysql-connector-java")
// querydsl
api("com.querydsl:querydsl-jpa:")
kapt(group = "com.querydsl", name = "querydsl-apt", classifier = "jpa")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
spring:
datasource:
url: jdbc:mysql://localhost:3306/[dbname]?serverTimezone=UTC&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: [userid]
password: [userpassword]
jpa:
hibernate:
# ddl-auto: create
properties:
hibernate:
dialect: com.example.practice_qdsl.Dialect.CustomDialect # 커스텀한 Dialect 경로
# show_sql: true
format_sql: true
logging:
level:
org:
hibernate:
SQL: debug
type:
descriptor:
sql: trace
package com.example.practice_qdsl.Configuration
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.persistence.EntityManager
import javax.persistence.PersistenceContext
@Configuration
class QuerydslConfig(
@PersistenceContext
private val entityManager: EntityManager
) {
@Bean
fun jpaQueryFactory(): JPAQueryFactory {
return JPAQueryFactory(this.entityManager)
}
}
Dialect?
- JPA는 보통 직접 SQL을 작성하고 실행한다. 그런데 DBMS 종류마다 사용하는 SQL이 다르다. JPA는 해당 DBMS에 맞춰 SQL을 생성해야 하는데 DBMS의 정보를 모르면 문제가 발생할 수 있다.
- 그래서 JPA에 어떤 DBMS를 사용하는 알려주는 방법이 Dialect를 설정하는 방법이 된다. JPA에 Dialect를 설정할 수 있는 추상화 Dialect Class를 제공하고 설정된 방언으로 각 DBMS에 맞는 구현체를 제공한다.
package com.example.practice_qdsl.Dialect
import org.hibernate.dialect.MySQL57Dialect
import org.hibernate.dialect.function.SQLFunctionTemplate
import org.hibernate.type.StandardBasicTypes
class CustomDialect: MySQL57Dialect() {
init{
registerFunction("match", SQLFunctionTemplate(StandardBasicTypes.INTEGER, "match(?1) against (?2 in boolean mode)"))
}
}
MySQL57Dialect를 상속 받은 뒤, Fulltext Search를 사용하기 위해 생성자에서 regsiterFunction로 match - against 함수를 등록한다.
검색 모드는 NATURAL LANGUAGE MODE와 BOOLEAN MODE 2가지가 있다. 자연어 검색 모드에서는 전체 테이블의 50% 이상의 레코드가 검색된 키워드를 가지고 있다면, 그 키워드는 검색어로서 의미가 없다고 판단하고 검색 결과에서 배제 시킨다. 주소 데이터를 배제 시키면 당연히 안되므로 각 키워드의 포함과 불포함을 판단하는 BOOLEAN MODE로 선택한다.
type이 String이 아니라 INTEGER로 받은 이유는 Expressions.stringTemplate
은 Select 절에서만 사용이 가능. (이유는 잘 모르겠다.. 누군가 알면 댓글로 남겨주시면 감사하겠습니다 ㅎ)
package com.example.practice_qdsl.Entity
import javax.persistence.*
@Entity(name = "ADDRESS_INFO")
data class AddressInfo(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
var id: Long = 0,
@Column(name = "address")
val address: String,
)
package com.example.practice_qdsl.Repository
import com.example.practice_qdsl.Entity.AddressInfo
interface AddressInfoRepositoryCustom {
fun getAddressList(keyword : String) :List<AddressInfo>
fun getLists():List<AddressInfo>
}
package com.example.practice_qdsl.Repository.Impl
import com.example.practice_qdsl.Entity.AddressInfo
import com.example.practice_qdsl.Entity.QAddressInfo
import com.example.practice_qdsl.Repository.AddressInfoRepositoryCustom
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.core.types.dsl.NumberTemplate
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.stereotype.Repository
@Repository
class AddressInfoRepositoryCustomImpl(
val jpaQueryFactory: JPAQueryFactory,
) : AddressInfoRepositoryCustom {
override fun getAddressList(keyword: String): List<AddressInfo> {
val booleanTemplate: NumberTemplate<*> = Expressions.numberTemplate(Integer::class.java,
"function('match',{0},{1})",
QAddressInfo.addressInfo.address, keyword)
return jpaQueryFactory.select(QAddressInfo.addressInfo).from(QAddressInfo.addressInfo).where(booleanTemplate.gt(0)).fetch()
}
override fun getLists(): List<AddressInfo> {
return jpaQueryFactory.selectFrom(QAddressInfo.addressInfo).fetch()
}
}
약 5배 차이가 난다. 단 검색어가 "서울특별시" 같이 짦은 것들은 %Like%가 더 빨랐다.
즉, 검색어가 길어질 수록 Perfomance는 match-against >>>>>>>> %Like%이 된다.