GettingStarted_KO - kwon37xi/freemarker-dynamic-ql-builder GitHub Wiki

Freemarker Dynamic QL Builder ์‹œ์ž‘ํ•˜๊ธฐ

freemarker-dynamic-ql-builder ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Java์—์„œ ๋™์ ์œผ๋กœ SQL, JPQL, HQL ๋“ฑ์„ ์ƒ์„ฑํ•˜๊ณ  JDBC ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค. ์ด์ œ๋ถ€ํ„ฐ ๋‚˜์˜ค๋Š” QL์€ SQL, JPQL, HQL ๋“ฑ์„ ์˜๋ฏธํ•œ๋‹ค. ์ด๋Š” ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ชฉ๋ก ์ƒ์„ฑ์˜ ์—ญํ• ๋งŒ ํ•˜๊ณ  ์ฟผ๋ฆฌ์˜ ์‹คํ–‰์€ ๋‹ค๋ฅธ ๊ฒƒ์—๊ฒŒ ๋งก๊ธด๋‹ค.

์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ•œ ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด๊ณผ ๋ฐ”์ธ๋”ฉ ํŒŒ๋ผ๋ฏธํ„ฐ List(ํ˜น์€ ๋ฐฐ์—ด)์„ Plain PreparedStatement๋‚˜ SpringFramework JdbcTemplate, JDBI, JPA, Hibernate ๋“ฑ์—์„œ Native ํ˜น์€ JPQL/HQL์„ ์‹คํ–‰ํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์š”๊ตฌ์‚ฌํ•ญ

  • Java 6+
  • ์˜์กด์„ฑ์ด freemarker 2.3.23+์™€ slf4j ๋‹จ ๋‘๊ฐœ๋ฟ์ธ ๊ฐ€๋ฒผ์šด ๋™์  ์ฟผ๋ฆฌ ์ƒ์„ฑ ํˆด์ด๋‹ค.

์ตœ์‹  ๋ฒ„์ „

kr.pe.kwonnam.jspMaven Central

์˜์กด์„ฑ ์„ค์ •

Gradle

compile 'kr.pe.kwonnam.freemarkerdynamicqlbuilder:freemarker-dynamic-ql-builder:{version}'

Maven

<dependency>
    <groupId>kr.pe.kwonnam.freemarkerdynamicqlbuilder</groupId>
    <artifactId>freemarker-dynamic-ql-builder</artifactId>
    <version>{version}</version>
</dependency>

FreemarkerDynamicQlBuilder ๊ฐ์ฒด ์„ค์ •

๋จผ์ € ํ”„๋ฆฌ๋งˆ์ปค Configuration ๊ฐ์ฒด๋ฅผ ์ž…๋ง›๋Œ€๋กœ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ FreemarkerDynamicQlBuilder ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

์ตœ์ข… ์ƒ์„ฑ๋œ FreemarkerDynamicQlBuilder ๊ฐ์ฒด๋ฅผ SpringFramework Bean์œผ๋กœ ๋“ฑ๋กํ•ด์„œ Spring๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค.

FreemarkerDynamicQlBuilder๋Š” Thread Safe ํ•˜๋‹ค. ๋‹จ Configuration ๊ฐ์ฒด๋ฅผ ํ•œ ๋ฒˆ ์„ค์ •ํ•œ ์ดํ›„ ๋ณ€๊ฒฝํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค.

import freemarker.template.Configuration;
import kr.pe.kwonnam.freemarkerdynamicqlbuilder.FreemarkerDynamicQlBuilder;
import kr.pe.kwonnam.freemarkerdynamicqlbuilder.FreemarkerDynamicQlBuilderFactory;

// ...

// ์ž์‹ ์ด ์›ํ•˜๋Š” ๋Œ€๋กœ Freemarker ์„ค์ •์„ ํ•ด์ค€๋‹ค.
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
cfg.setClassForTemplateLoading(this.getClass(), "/META-INF/dynamicqls");
cfg.setDefaultEncoding("UTF-8");

// ์ˆซ์žํ˜•์„ ์‰ผํ‘œ์—†์ด ์ˆซ์ž๋งŒ ์ถœ๋ ฅํ•˜๊ฒŒ ํฌ๋ฏธํŒ…
cfg.setNumberFormat("0.######");
// ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ ๋ณ€๊ฒฝ ์ฒดํฌ ์ฃผ๊ธฐ๋ฅผ 1์‹œ๊ฐ„์œผ๋กœ ์„ค์ •
cfg.setTemplateUpdateDelayMilliseconds(3600000L); // ms ๋‹จ์œ„
// ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์œ„ํ•œ ์บ์‹œ ์„ค์ •
cfg.setCacheStorage(new MruCacheStorage(500, 5000));

// Freemarker ์„ค์ •์„ ๊ฐ€์ง€๊ณ  FreemarkerDynamicQlBuilder ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
FreemarkerDynamicQlBuilder dynamicQlBuilder = new FreemarkerDynamicQlBuilderFactory(cfg)
		// ์ค‘๊ฐ„์— ๊ธฐํƒ€ ์„ค์ •๋“ค..
		.getFreemarkerDynamicQlBuilder();

๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ Dynamic QL Template์˜ ์ƒ์„ฑ ์‚ดํŽด๋ณด๊ธฐ

๋™์ ์œผ๋กœ QL์„ ์ƒ์„ฑํ•˜๋ ค๋ฉด ํ”„๋ฆฌ๋งˆ์ปค Configuration ์—์„œ ์ง€์ •ํ•œ ๊ฒฝ๋กœ์ƒ์— ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ  ํŒŒ์ผ์„ ํ™•์žฅ์ž๊ฐ€ .ql.ftl๋กœ ๋๋‚˜๊ฒŒ ์ƒ์„ฑํ•ด์ค€๋‹ค.

์œ„์˜ ์„ค์ •์—์„œ๋Š” CLASSPATH ๋‚ด์˜ /META-INF/dynamicqls ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ํ…œํ”Œ๋ฆฟ Home์œผ๋กœ ์ง€์ •ํ•˜์˜€์œผ๋ฏ€๋กœ /META-INF/dynamicqls/users/select.ql.ftl ์„ ์ƒ์„ฑํ–ˆ๋‹ค๊ณ  ๊ฐ„์ฃผํ•˜๋ฉด,

SELECT *
FROM somewhere
<@ql.where>
    <#if user.name?has_content>
    name = ${param(user.name)}
    </#if>
    <#if user.birthyear gt 0>
    AND birthyear = ${param(user.birthyear)}
    </#if>
    <#if user.employeeType??>
    AND employeeType = ${param(user.employeeType, 'enumToName')}
    </#if>
    <#list userIds!>
    AND userId IN (<#items as userId>${param(userId)}<#sep>,</#sep></#items>)
    </#list>
</@ql.where>

ORDER BY userId
LIMIT 10

์ด์ œ Java ์ฝ”๋“œ์ƒ์—์„œ๋Š”

User user = new User();
user.setName(""); // empty on purpose
user.setBirthyear(2015);
user.setEmployeeType(EmployeeType.FULLTIME);

Map<String,Object> dataModel = new HashMap<String,Object>();
dataModel.put("user", user);
dataModel.put("userIds", new int[]{100, 200, 300});

DynamicQuery dynamicQuery = dynamicQlBuilder.buildQuery("users/select", dataModel);
// dynamicQuery ์— ์ƒ์„ฑ๋œ QL๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ชฉ๋ก์ด ๋“ค์–ด ์žˆ๋‹ค.

์ด ๋ช…๋ น์„ ์‹คํ–‰ํ•˜๊ณ ๋‚˜์„œ dynamicQuery ๊ฐ์ฒด๋ฅผ ์‚ดํŽด๋ณด๋ฉด ์ฟผ๋ฆฌ ์‹คํ–‰์— ํ•„์š”ํ•œ SQL ๋ฌธ์ž์—ด๊ณผ, Positional Parameter(๋ฌผ์Œํ‘œ, ?)์— ๋ฐ”์ธ๋”ฉ ๋  ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด๋“ค์˜ ๋ฆฌ์ŠคํŠธ ํ˜น์€ ๋ฐฐ์—ด์„ ์–ป์„ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

dynamicQuery.getQueryString() 
==> String
"SELECT *
FROM somewhere
WHERE birthyear = ?
    AND employeeType = ?
    AND userId IN (?,?,?)
ORDER BY userId
LIMIT 10"

dynamicQuery.getParameters()
==> List<Object> : [2015, FULLTIME, 100, 200, 300] 

dynamicQuery.getQueryParameterArray()
==> Object[] : [2015, FULLTIME, 100, 200, 300] 

์ด์ œ ์ด๊ฒƒ์„ ๊ฐ€์ง€๊ณ  PreparedStatement์— ๋ฐฐ์—ด์„ for๋ฌธ ๋Œ๋ฉด์„œ ๋ฐ”์ธ๋”ฉํ•˜๊ฑฐ๋‚˜ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฐ„ํŽธํ•˜๊ฒŒ ๋ฐ”์ธ๋”ฉ ํ•  ์ˆ˜ ๋„ ์žˆ๋‹ค.

PreparedStatement psmt = connection.prepareStatement(dynamicQuery.getQueryString());
dynamicQuery.bindParameters(psmt);

ResultSet rs = psmt.executeQuery();
// ...

Plain JDBC๊ฐ€ ์•„๋‹Œ SpringJdbcTemplate์ด๋‚˜ jDBI, Hibernate, JPA ๋“ฑ์— ์ฟผ๋ฆฌ์™€ ๋ฐ”์ธ๋”ฉํ•  ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋„˜๊ฒจ์„œ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

Parameter Binding

๋™์  ์ฟผ๋ฆฌ ์ƒ์„ฑ์˜ ํ•ต์‹ฌ์€ ๋™์ ์œผ๋กœ ๋ฌธ์ž์—ด์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ๋™์ ์œผ๋กœ ์ƒ์„ฑ๋œ ๋ฌธ์ž์—ด์— JDBC ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๋งค์นญ์‹œํ‚ค๋А๋ƒ์— ์žˆ๋‹ค.

${param(๋ณ€์ˆ˜)}๋ฅผ ํ†ตํ•œ ? ์ถœ๋ ฅ๊ณผ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ

MyBatis์—์„œ #{๋ณ€์ˆ˜} ์—ญํ• ์„ ํ•˜๋Š” ๊ฒƒ์ด ์—ฌ๊ธฐ์„œ ${param(๋ณ€์ˆ˜)}์ด๋‹ค.

${param(๋ณ€์ˆ˜)}

์œ„ ๊ตฌ๋ฌธ์€ ์‹ค์ œ๋กœ๋Š” Prepared Statement Positional Parameter์ธ ๋ฌผ์Œํ‘œ(?) ํ•˜๋‚˜๋งŒ ์ถœ๋ ฅํ•œ๋‹ค.

?

ํ•˜์ง€๋งŒ, ์ด์™€ ๋™์‹œ์— ๋ณ€์ˆ˜์˜ ๊ฐ’์ด ์ˆœ์„œ๋Œ€๋กœ ์ €์žฅ๋˜์–ด ๋™์  QL ์ƒ์„ฑ์ด ์ข…๋ฃŒ๋˜๋ฉด DynamicQuery.getParameters() ๋กœ๋Š” java.util.List<Object> ๊ฐ์ฒด ํ˜•ํƒœ๋ฅผ, DynamicQuery.getParameterArray()๋กœ๋Š” ๋™์ผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ฒด ๋ฐฐ์—ด(Object[]) ํ˜•ํƒœ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. ์ด๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฆฌ์ŠคํŠธ/๋ฐฐ์—ด์„ ๊ฐ€์ง€๊ณ  PreparedStatement ์— ์ˆœ์„œ๋Œ€๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ง€์ •ํ•˜๋ฉด ๋œ๋‹ค.

Plain JDBC PreparedStatement๋Š” DynaimcQuery.bindParameters(PreparedStatement) ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ๋ฐ”์ธ๋”ฉ์„ ์ž๋™์œผ๋กœ ํ•ด์ค„ ์ˆ˜๋„ ์žˆ๋‹ค.

JPQL/HQL Positional Parameter

JPA์˜ JPQL๊ณผ Hibernate์˜ HQL์€ ?์ธ๋ฑ์Šค ํ˜•ํƒœ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์ด ํ‘œ์ค€์ด๋‹ค. HQL์€ ?๋„ ์ง€์›ํ•˜๊ธด ํ•˜์ง€๋งŒ ์•ž์œผ๋กœ ํ•ด๋‹น ํŒŒ๋ผ๋ฏธํ„ฐ ํ˜•ํƒœ๋Š” ์‚ญ์ œํ•  ์˜ˆ์ •์ด๋ผ๊ณ  ํ•œ๋‹ค.

FreemarkerDynamicQlBuilder#buildQuery()์˜ ๋งˆ์ง€๋ง‰ ์ธ์ž(withPositionalIndex)๋ฅผ true๋กœ ์ง€์ •ํ•˜๋ฉด ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ ๋’ค์— ์ˆซ์ž๊ฐ€ ๋ถ™๊ฒŒ ๋œ๋‹ค.

DynamicQuery dynamicQuery = 
    dynamicQlBuilder.buildQuery(queryTemplateName, dataModel, true);

์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ…œํ”Œ๋ฆฟ์„ ์ž‘์„ฑํ•˜์˜€๋‹ค๋ฉด

${param(๋ณ€์ˆ˜ํ•˜๋‚˜)}, ${param(๋ณ€์ˆ˜๋‘˜)}, ${param(๋ณ€์ˆ˜์…‹)}

์ถœ๋ ฅ๋œ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๊ฐ€ ๋œ๋‹ค.

?1, ?2, ?3

๋ฌธ์ž์—ด ์ง์ ‘์ถœ๋ ฅ

ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ํ•˜์ง€ ์•Š๊ณ  ๋ณ€์ˆ˜์˜ ๋ฌธ์ž์—ด ํ˜•ํƒœ๋ฅผ ์ง์ ‘ ์ถœ๋ ฅํ•˜๋Š” ๊ฒƒ์€ ํ”„๋ฆฌ๋งˆ์ปค์˜ ๊ธฐ๋ณธ ๋ณ€์ˆ˜ ์ถœ๋ ฅ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•œ๋‹ค.

${๋ณ€์ˆ˜}

๋ณ€์ˆ˜์˜ ๋‚ด์šฉ์ด ๋ฌธ์ž์—ด๋กœ ์žˆ๋Š” ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅ๋œ๋‹ค. ๋ฌผ๋ก  ์ด ๊ฒฝ์šฐ, SQL Injection ๋“ฑ์˜ ์œ„ํ—˜์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ฒ ์ €ํ•˜๊ฒŒ ๊ฒ€์ฆ๋œ ๋ฐ์ดํ„ฐ์— ํ•œํ•ด์„œ๋งŒ ์ด ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. ๋ณดํ†ต์˜ ๊ฒฝ์šฐ์—๋Š” ์‚ฌ์šฉํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค.

ํ”„๋ฆฌ๋งˆ์ปค Configuration ๊ฐ์ฒด ์„ค์ •์‹œ์— cfg.setNumberFormat("0.######")์„ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด ์ˆซ์ž๊ฐ€ ์‰ผํ‘œ๊ฐ€ ์ฐํžŒ ํ˜•ํƒœ(123,456,789)๋กœ ์ถœ๋ ฅ๋˜์–ด SQL ๋ฌธ๋ฒ•์— ์–ด๊ธ‹๋‚˜๊ฒŒ ๋˜๋ฏ€๋กœ ๋งค์šฐ ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค.

Freemarker Template ์ง€์‹œ์ž(Directive)

๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹น์—ฐํžˆ ๋ชจ๋“  ํ”„๋ฆฌ๋งˆ์ปค ์˜ ํ…œํ”Œ๋ฆฟ ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด์— ๊ด€ํ•ด์„œ๋Š” Freemarker Manual ์„ ์ฐธ์กฐํ•œ๋‹ค.

ํ”„๋ฆฌ๋งˆ์ปค์˜ ๊ธฐ๋ณธ ์ง€์‹œ์ž ์™ธ์—๋„, MyBatis์˜ ์ง€์‹œ์ž๋“ค์„ ํ‰๋‚ด๋‚ด์–ด ์ฟผ๋ฆฌ ์ž‘์„ฑ์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ๋„์™€์ฃผ๋„๋ก ํ•˜์˜€๋‹ค.

<@ql.where>

MyBatis์˜ <where>์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•œ๋‹ค. SQL์˜ ๋™์  WHERE ์กฐ๊ฑด ์ƒ์„ฑ์‹œ ์‚ฌ์šฉํ•œ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ ๊ตฌ๋ฌธ์ด ์žˆ์„ ๋•Œ ๋ชจ๋“  ์•ž ๋’ค ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ง€์‹œ์ž ์•ˆ์˜ ๋‚ด์šฉ์ด

  • ๊ณต๋ฐฑ์ด ์•„๋‹Œ ๋ฌธ์ž์—ด์„ ํฌํ•จํ•œ๋‹ค๋ฉด WHERE ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์ฒ˜์Œ ๋‚˜์˜ค๋Š” AND , and , OR , or ๋ฅผ ์‚ญ์ œํ•œ๋‹ค.
  • ๋ชจ๋‘ ๊ณต๋ฐฑ(๋นˆ์นธ, ์ƒˆ์ค„๊ธฐํ˜ธ ๋“ฑ)์ด๋ผ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์ถœ๋ ฅํ•˜์ง€ ์•Š๋Š”๋‹ค.
<@ql.where>
    <#if user.name?has_content>
    name = ${param(user.name)}
    </#if>
    <#if user.birthyear gt 0>
    AND birthyear = ${param(user.birthyear)}
    </#if>
    <#if user.employeeType??>
    AND employeeType = ${param(user.employeeType, 'enumToName')}
    </#if>
</@ql.where>

==>
WHERE birthyear = ?
    AND employeeType = ?

์ด ๋‚ด์šฉ์€ <@ql.trim>์œผ๋กœ ํ•  ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๋™์ผํ•˜๋‹ค.

<@ql.trim prefix="WHERE " prefixOverrides=["AND ", "and ", "OR ", "or "]>
  ๊ธฐํƒ€ ๋‚ด์šฉ๋“ค
</@ql.trim>

<@ql.set>

MyBatis์˜ <set>์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•œ๋‹ค. SQL์˜ ๋™์  UPDATE๋ฌธ ์ƒ์„ฑ์‹œ ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ ๊ตฌ๋ฌธ์ด ์žˆ์„ ๋•Œ ๋ชจ๋“  ์•ž ๋’ค ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ง€์‹œ์ž ์•ˆ์˜ ๋‚ด์šฉ์ด

  • ๊ณต๋ฐฑ์ด ์•„๋‹Œ ๋ฌธ์ž์—ด์„ ํฌํ•จํ•œ๋‹ค๋ฉด SET ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ณต๋ฐฑ์„ ์ œ์™ธํ•˜๊ณ  ๋งจ ๋งˆ์ง€๋ง‰์— ๋‚˜์˜ค๋Š” ์‰ผํ‘œ(,)๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.
  • ๋ชจ๋‘ ๊ณต๋ฐฑ(๋นˆ์นธ, ์ƒˆ์ค„๊ธฐํ˜ธ ๋“ฑ)์ด๋ผ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์ถœ๋ ฅํ•˜์ง€ ์•Š๋Š”๋‹ค.
UPDATE sometable
<@ql.set>
    <#if user.name??>name = ${param(user.name)},</#if>
    <#if user.birthyear gt 0>birthyear = ${param(user.birthyear)},</#if>
    <#if user.employeeType??>employeeType = ${para(user.employyType)}</#if>
</@ql.set>

==>
UPDATE sometable
SET name = ?,
    birthyear = ?

์ด ๋‚ด์šฉ์€ <@ql.trim>์œผ๋กœ ํ•  ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๋™์ผํ•˜๋‹ค.

UPDATE sometable
<@ql.trim prefix="SET " suffixOverrides=[","]>
  ๊ธฐํƒ€ ๋‚ด์šฉ๋“ค
</@ql.trim>

<@ql.trim>

MyBatis์˜ <trim>๊ณผ ๊ฐ™์€ ์—ญํ• ์„ ํ•œ๋‹ค. ๋ฒ”์šฉ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” ํ•ด๋‹น ์ง€์‹œ์ž ์•Š์— ์ƒ์„ฑ๋œ ๋ฌธ์ž์—ด์˜ ์•ž๋’ค ๊ณต๋ฐฑ์„ ๋ชจ๋‘ ์ œ๊ฑฐํ•œ๋‹ค.
  • prefix="๋ฌธ์ž์—ด" : trim ๊ฒฐ๊ณผ๊ฐ€ ๊ณต๋ฐฑ์ด ์•„๋‹Œ ๋ฌธ์ž์—ด์„ ํฌํ•จํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ๋ฌธ์ž์—ด ๋งจ ์•ž์— ํ•ด๋‹น ๋ฌธ์ž์—ด์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
  • suffix="๋ฌธ์ž์—ด" : trim ๊ฒฐ๊ณผ๊ฐ€ ๊ณต๋ฐฑ์ด ์•„๋‹Œ ๋ฌธ์ž์—ด์„ ํฌํ•จํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ๋ฌธ์ž์—ด ๋งจ ๋’ค์— ํ•ด๋‹น ๋ฌธ์ž์—ด์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
  • prefixOverrides=["๋ฌธ์ž์—ด1", "๋ฌธ์ž์—ด2", ...] : trim ๊ฒฐ๊ณผ๊ฐ€ ๊ณต๋ฐฑ์ด ์•„๋‹Œ ๋ฌธ์ž์—ด์„ ํฌํ•จํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ๋ฌธ์ž์—ด์˜ ๋งจ ์•ž์— ๋ฐฐ์—ด๋กœ ์ง€์ •๋œ ๋ฌธ์ž์—ด๋“ค ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋‚˜์˜ค๋ฉด ์ด๋ฅผ ์‚ญ์ œํ•œ๋‹ค. ์ฒซ๋ฒˆ์งธ ๋งค์นญ๋งŒ ์‚ญ์ œํ•œ๋‹ค.
  • suffixOverrides=["๋ฌธ์ž์—ด1", "๋ฌธ์ž์—ด2", ...] : trim ๊ฒฐ๊ณผ๊ฐ€ ๊ณต๋ฐฑ์ด ์•„๋‹Œ ๋ฌธ์ž์—ด์„ ํฌํ•จํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ๋ฌธ์ž์—ด์˜ ๋งจ ๋’ค์— ๋ฐฐ์—ด๋กœ ์ง€์ •๋œ ๋ฌธ์ž์—ด๋“ค ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋‚˜์˜ค๋ฉด ์ด๋ฅผ ์‚ญ์ œํ•œ๋‹ค. ์ฒซ๋ฒˆ์งธ ๋งค์นญ๋งŒ ์‚ญ์ œํ•œ๋‹ค.

์˜ˆ์ œ๋Š” <@ql.where>์™€ <@ql.set> ์ฐธ์กฐ.

IN ์ ˆ์˜ ์ฒ˜๋ฆฌ

MyBatis์—๋Š” foreach ๊ตฌ๋ฌธ์ด ์žˆ๋Š”๋ฐ, Freemarker๋Š” ์ด๋ฏธ ์ด์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ํ›จ์”ฌ ๋” ์œ ์—ฐํ•œ ์ง€์‹œ์ž๊ฐ€ ์กด์žฌํ•œ๋‹ค. ๊ทธ๋ž˜์„œ foreach๋Š” ๋”ฐ๋กœ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.

userIds = new int[] {1,2,3,4,5} ๋ผ๊ณ  ํ•  ๋•Œ,

WHERE
<#list userIds!>
user_id in (<#items as userId>${param(userId)}<#sep>,</#sep></#items>)
<#else>
user_id IS NOT NULL
</#list>

userIds์— ๊ฐ’์ด ์กด์žฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋œ๋‹ค.

WHERE
user_id in (?, ?, ?, ?, ?)

userIds๊ฐ€ null์ด๊ฑฐ๋‚˜ ๋นˆ List ํ˜น์€ ๋ฐฐ์—ด์ด๋ฉด <#else> ๊ตฌ๋ฌธ๋’ค๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

WHERE
user_id IS NOT NULL

๋ฌผ๋ก  <#else>๋Š” ์ƒ๋žต ๊ฐ€๋Šฅํ•˜๋‹ค. ์ž์„ธํ•œ ๊ฒƒ์€ ํ”„๋ฆฌ๋งˆ์ปค ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•œ๋‹ค.

ParameterConverter

ParameterConverter๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด์˜ ๊ฐ’์„ ๋‹ค๋ฅธ ํƒ€์ž… ํ˜น์€ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ธฐ๋Šฅ์„ ํ•œ๋‹ค.

๊ฐ์ฒด๋ฅผ ์ฟผ๋ฆฌ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธธ ๋•Œ ๋•Œ๋กœ๋Š” Java ์˜ ๊ฐ์ฒด Type๊ณผ DB์˜ Column Type์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค.

์˜ˆ๋ฅผ๋“ค๋ฉด Java Date ๊ฐ์ฒด์ด์ง€๋งŒ DB Column์€ 2015/12/25 ๊ฐ™์€ ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด์ผ ์ˆ˜๋„ ์žˆ๊ณ , Java Enum์„ ๋ฌธ์ž ํ˜น์€ ์ˆซ์ž๋กœ ์ €์žฅํ•˜๊ณ  ์‹ถ๊ฑฐ๋‚˜, Boolean์„ Y|N ํ˜น์€ T|F ๋กœ ์ €์žฅํ•  ๋•Œ๋„ ์žˆ๋‹ค.

ํ˜น์€ ๋™์ผ ๊ฐ์ฒด๋ผ ํ•˜๋”๋ผ๋„ ๋ฐ์ดํ„ฐ์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ธธ ์›ํ•  ๋•Œ๋„ ์žˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค๋ฉด Date๊ฐ์ฒด์ด์ง€๋งŒ ์–ด๋–จ ๋•Œ๋Š” ๋‚ ์งœ ๋ถ€๋ถ„๋งŒ, ์–ด๋–จ ๋•Œ๋Š” ์‹œ๊ฐ„ ๋ถ€๋ถ„๋งŒ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ฒจ์•ผ ํ•  ๋•Œ๋„ ์žˆ๋‹ค.

์ด๋Ÿฌํ•œ ์—ญํ• ์„ ํ•˜๋Š” ๊ฐ์ฒด ๋ณ€ํ™˜๊ธฐ๋ฅผ kr.pe.kwonnam.freemarkerdynamicqlbuilder.paramconverter.ParameterConverter ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๋งŒ๋“ค๊ณ  FreemarkerDynamicQlBuilder ์ƒ์„ฑ ์„ค์ •์‹œ์— ๋“ฑ๋กํ•ด์ฃผ๋ฉด,

FreemarkerDynamicQlBuilder dynamicQlBuilder = 
    new FreemarkerDynamicQlBuilderFactory(freemarkerConfiguration)
        .addParameterConverter("booleanToYN", new BooleanToStringParameterConverter("Y", "N"))
        .addParameterConverter("dateToString", new DateToStringParameterConverter("yyyy-MM-dd HH:mm:ss"))
		// ....
        .getFreemarkerDynamicQlBuilder();

์ด์ œ ํ…œํ”Œ๋ฆฟ์—์„œ ๋‹ค์Œ์ฒ˜๋Ÿผ ${param(๋ณ€์ˆ˜,"parameterConverterName")} ์œผ๋กœ ํ˜ธ์ถœํ•ด์ฃผ๋ฉด ๋ณ€์ˆ˜๊ฐ€ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๊ฑฐ์ณ ๋ณ€ํ™˜๋œ ์ƒํƒœ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ชฉ๋ก์— ์ €์žฅ๋˜๊ฒŒ ๋œ๋‹ค.

${param(today, "dateToString")}
${param(areYouReady, "booleanToYN")}
==>
["2015-12-25 13:21:31", "Y"]

๊ธฐ๋ณธ์ ์œผ๋กœ ์ด๋ฏธ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ParameterConverter๋“ค์„ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด ๋‘์—ˆ๋‹ค. ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋“ฑ๋ก๋งŒ ํ•ด์ฃผ๋ฉด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • BooleanToNumberParameterConverter : boolean์„ ์ˆซ์ž๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.
  • BooleanToStringParameterConverter : boolean์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.
  • DateToSqlDateParameterConverter : java.util.Date๋ฅผ java.sql.Date ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. ๋‚ ์งœ ๋ถ€๋ถ„๋งŒ ์ €์žฅํ•œ๋‹ค.
  • DateToSqlTimeParameterConverter : java.util.Date๋ฅผ java.sql.Time ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. ์‹œ๊ฐ„ ๋ถ€๋ถ„๋งŒ ์ €์žฅํ•œ๋‹ค.
  • DateToStringParameterConverter : java.util.Date๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. ๋ฌธ์ž์—ด ํฌ๋งท์„ ์ง€์ •ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
  • EnumToNameParameterConverter : enum์„ ํ•ด๋‹น name() ๋ฌธ์ž์—ด๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.
  • EnumToOrdinalParameterConverter : enum์„ ํ•ด๋‹น ordinal() ์ˆซ์ž๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. enum์˜ oridnal์„ ์ €์žฅํ•˜๋Š” ๊ฒƒ์€ ์ถ”ํ›„ ํ•ด๋‹น enum์˜ ์ˆœ์„œ ๋ณ€๊ฒฝ์ด ์ƒ๊ธฐ๋ฉด ๋ฒ„๊ทธ๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ๋ณธ์ ์œผ๋กœ๋Š” ํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค. ์ •๋ง ํ•„์š”ํ•œ์ง€ ์ƒ๊ฐํ•ด๋ณด๊ณ  ๋ถ€๋“์ดํ•œ ๊ฒฝ์šฐ์—๋งŒ DB์— ordinal์„ ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค.

FreemarkerDynamicQlBuilderFactory

FreemarkerDynamicQlBuilderFactory๋ฅผ ํ†ตํ•ด FreemarkerDynamicQlBuilder ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์ค„ ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜๋“ค์„ ์‚ดํŽด๋ณธ๋‹ค.

  • qlDirectivePrefix("๋ฌธ์ž์—ด") : <@ql.trim> ๋“ฑ์˜ ์‚ฌ์šฉ์ž ์ •์˜ ์ง€์‹œ์ž์˜ prefix๋ฅผ ์ง€์ •ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ql์ด๋‹ค. ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ dataModel Map์— ์—ฌ๊ธฐ ์ง€์ •๋œ ๊ฐ’์„ Key๋กœ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ๋„ฃ์„ ์ˆ˜ ์—†๋‹ค.
  • paramMethodName("๋ฌธ์ž์—ด") : ${param()}์˜ param ์ด๋ฆ„์„ ์›ํ•˜๋Š” ๋Œ€๋กœ ์ง€์ •ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ param์ด๋‹ค. ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ dataModel Map์— ์—ฌ๊ธฐ ์ง€์ •๋œ ๊ฐ’์„ Key๋กœ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ๋„ฃ์„ ์ˆ˜ ์—†๋‹ค.
  • queryTemplateNamePostfix("๋ฌธ์ž์—ด") : ํ”„๋ฆฌ๋งˆ์ปค ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ์„ ์ฐพ์„ ๋•Œ ๋’ค์— ๋ถ™์„ ํ™•์žฅ์ž๋ฅผ ์ง€์ •ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ .ql.ftl์ด๋‹ค. ํ…œํ”Œ๋ฆฟ ์ด๋ฆ„ ์ง€์ •์‹œ ์ด ๋ถ€๋ถ„์€ ๋นผ๊ณ  ์ง€์ •ํ•œ๋‹ค.
  • templateModelObjectUnwrapper(TemplateModelObjectUnwrapper๊ฐ์ฒด) : ${param()}์„ ํ˜ธ์ถœํ•˜๋ฉด Freemarker๋Š” ์‹ค์ œ ๊ฐ’ ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ Freemarker ์ „์šฉ TemplateModel์ด๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฐ์ฒด๋กœ ๊ฐ์‹ธ์„œ ๊ฐ’์„ ๋„˜๊ฒจ ์ค€๋‹ค. ์—ฌ๊ธฐ์„œ ์‹ค์ œ ๊ฐ’์„ ๊ตฌํ•  ์ˆ˜ ์žˆ๋Š” Unwrapper๋ฅผ ์ง์ ‘ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ TemplateModelObjectUnwrapperDefaultImpl์ด๋‹ค. ๊ธฐ๋ณธ๊ฐ’์„ ๊ทธ๋ƒฅ ๋‘๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.
  • addParameterConverter("์ปจ๋ฒ„ํ„ฐ์ด๋ฆ„", ParameterConverter๊ฐ์ฒด) : ParameterConverter๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.
  • addAllParameterConverters(Map<String, ParameterConverter>๊ฐ์ฒด) : ํ•œ๋ฒˆ์— ๋ชจ๋“  ParameterConverter๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.
  • clearParameterConverters() : ๋“ฑ๋ก๋œ ๋ชจ๋“  ParameterConverter๋ฅผ ์‚ญ์ œํ•œ๋‹ค.
  • getFreemarkerDynamicQlBuilder() : ์„ค์ •์— ๋”ฐ๋ผ FreemarkerDynamicQlBuilder ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
โš ๏ธ **GitHub.com Fallback** โš ๏ธ