MyBatis Dynamic SQL
iFlex 는 SQL 문을 손으로 쓰지 않는 것을 지향합니다. 대신 MyBatis Dynamic SQL 로 자바 코드에서 SQL 을 조립합니다.
이 문서는 그 라이브러리를 이 프로젝트에서 어떻게 쓰는지 다룹니다. 엔터티를 처음 만드는 순서는 엔터티 만들기 를 먼저 보세요.
핵심 한 문장
컬럼명을 문자열로 쓰지 마세요. {X}Dsl 로 참조하면 컬럼이 바뀔 때 컴파일 에러로 잡힙니다.
이 문서의 예제 테이블
설명에는 nv_article(게시글) 테이블을 씁니다. art_title, art_content, art_read, art_bd_code, art_deleted 같은 컬럼이 있어 검색·집계 예제를 보이기 좋습니다.
문서에 실린 SQL 은 전부 실제로 렌더해서 확인한 결과입니다. 읽기 쉽도록 바인딩 자리만 ? 로 줄였습니다(실제로는 #{parameters.p1,jdbcType=...} 형태).
왜 문자열 SQL 을 쓰지 않나
// ❌ XML 에 이렇게 쓰면 — 컬럼명이 바뀌어도 아무도 모릅니다. 런타임에 터집니다.
"SELECT * FROM nv_article WHERE art_title LIKE #{title} ORDER BY ${sortBy}"
// ✅ Dsl — art_title 을 리네임하면 이 줄이 컴파일 에러가 납니다
select(a.allColumns())
.from(a, a.getAlias())
.where(a.title, isLike("%공지%"))
.orderBy(a.desc(a.createdDt))얻는 것이 셋입니다.
| 문자열 SQL | Dynamic SQL | |
|---|---|---|
| 컬럼 리네임 | 배포 후 그 화면을 열어야 발견 | 컴파일 에러 |
| 타입 실수 | art_read > '많음' 이 그대로 나감 | 컴파일 에러 |
| 조건 조립 | if 로 문자열 이어붙이기 | 메서드 체인 |
${} 문자열 치환은 전면 금지입니다
${sortBy} 처럼 값이 SQL 에 그대로 박히는 문법은 인젝션 경로가 됩니다. DollarTokenRatchetTest 가 mapper XML 에 이런 토큰이 들어오는 것을 자동으로 막습니다.
정렬도 문자열이 아니라 컬럼 참조로 넘기세요 — 뒤의 정렬 참고.
{X}Dsl — 라이브러리가 요구하는 테이블 정의
Dynamic SQL 은 a.title 같은 컬럼 객체를 받아 SQL 을 만듭니다. 그래서 컬럼 객체를 선언해 둘 자리가 필요한데, 그것이 {X}Dsl 입니다.
public class ArticleDsl extends NvSqlTable {
public static final String DEFAULT_ALIAS = "a";
//region ===== table columns (nv_article) =====
public final SqlColumn<Long> id = column("art_id", JDBCType.BIGINT);
public final SqlColumn<String> boardCode = column("art_bd_code", JDBCType.VARCHAR);
public final SqlColumn<String> title = column("art_title", JDBCType.VARCHAR);
public final SqlColumn<String> content = column("art_content", JDBCType.LONGVARCHAR);
public final SqlColumn<Integer> read = column("art_read", JDBCType.INTEGER);
public final SqlColumn<Boolean> deleted = column("art_deleted", JDBCType.BOOLEAN);
public final SqlColumn<Instant> createdDt = column("art_created_dt", JDBCType.TIMESTAMP);
//endregion
public static ArticleDsl defaultAlias() { return new ArticleDsl(DEFAULT_ALIAS); }
public static ArticleDsl as(String alias) { return new ArticleDsl(alias); }
}| 요소 | 의미 |
|---|---|
SqlColumn<Integer> read | 컬럼 참조 + 타입 — where(a.read, isEqualTo("많음")) 은 컴파일 에러 |
DEFAULT_ALIAS | SQL 별칭. 검색 스키마의 경로("a.title")와 일치해야 합니다 |
as("별칭") | 같은 테이블을 두 번 조인할 때 |
ArticleDsl a = ArticleDsl.defaultAlias(); // 별칭 "a"
ArticleDsl a2 = ArticleDsl.as("a2"); // self join 용
a.title // 컬럼 참조
a.allColumns() // 전체 컬럼
a.getAlias() // "a"
a.asc(a.title) // 정렬 (타입 안전)
a.desc(a.createdDt)컬럼을 추가·변경할 때는 3개 파일을 함께 고칩니다
{X}.java— 엔터티 필드{X}Dsl.java—SqlColumn선언 (컬럼맵은NvSqlTable이 자동 수집)mybatis/sql/.../{X}Repository.xml— resultMap
resultMap 을 빠뜨리면 저장은 되는데 조회 결과에만 안 담깁니다. 자세한 이유는 엔터티 만들기 참고.
조건 작성
기본 조건
import static org.mybatis.dynamic.sql.SqlBuilder.*;
select(a.allColumns())
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.and(a.title, isLike("%공지%"))
.and(a.createdDt, isGreaterThan(from))
.orderBy(a.desc(a.createdDt))
.build().render(RenderingStrategies.MYBATIS3);자주 쓰는 조건:
| 조건 | SQL |
|---|---|
isEqualTo(v) / isNotEqualTo(v) | = v / <> v |
isGreaterThan(v) / isLessThan(v) | > v / < v |
isGreaterThanOrEqualTo(v) / isLessThanOrEqualTo(v) | >= v / <= v |
isIn(list) / isNotIn(list) | IN / NOT IN |
isLike("%v%") / isNotLike(...) | LIKE / NOT LIKE |
isNull() / isNotNull() | IS NULL / IS NOT NULL |
isTrue() / isFalse() | 불리언 |
isBetween(a).and(b) | BETWEEN |
isIn 에 빈 컬렉션을 넘기면 SQL 이 깨집니다
IN () 은 문법 오류입니다. repository 안에서 early-return 으로 막으세요.
default List<Article> listByCodes(Collection<String> codes) {
if (CollectionUtils.isEmpty(codes)) {
return List.of(); // ← 빈 컬렉션 방어
}
...
}중첩 괄호 — AND (A OR B)
가장 자주 필요하면서 가장 헷갈리는 부분입니다. "삭제되지 않았고, 제목 또는 내용에 키워드가 있는" 검색을 만들어 봅시다.
WHERE art_deleted = false
AND (art_title LIKE '%The%' OR art_content LIKE '%The%')괄호가 없으면 완전히 다른 쿼리가 됩니다. SQL 은 AND 가 OR 보다 강하므로, 괄호를 빼면 (deleted=false AND title LIKE ...) OR (content LIKE ...) 로 해석되어 삭제된 글까지 걸려 나옵니다.
방법이 둘 있고, 렌더 결과는 완전히 같습니다.
방법 A — and(...) 의 세 번째 인자로 or(...) 넘기기
//@formatter:off
select(a.id, a.title)
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.and(a.title, isLike(kw), or(a.content, isLike(kw)))
.build().render(RenderingStrategies.MYBATIS3);
//@formatter:on방법 B — group(...) 으로 명시적으로 묶기
//@formatter:off
select(a.id, a.title)
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.and(group(a.title, isLike(kw), or(a.content, isLike(kw))))
.build().render(RenderingStrategies.MYBATIS3);
//@formatter:on두 방법 모두 다음을 만듭니다.
select a.art_id, a.art_title from iflexdb.nv_article a
where a.art_deleted = ?
and (a.art_title like ? or a.art_content like ?)어느 쪽을 쓸까
where/and/or 는 마지막에 가변 인자로 추가 조건을 받고, 그것들을 한 괄호로 묶어 렌더합니다. 즉 방법 A 의 괄호는 "덤" 이 아니라 규칙입니다.
- 조건이 2~3개면 방법 A — 짧습니다
- 중첩이 생기거나 조건을 변수로 뽑으면 방법 B —
group(...)이라는 이름이 의도를 드러냅니다
읽는 사람이 괄호를 의심하지 않게 하는 쪽을 고르세요.
3중 이상 중첩
or(...) 안에 다시 group(...) 을 넣으면 됩니다.
//@formatter:off
select(a.id)
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.and(group(a.enabled, isEqualTo(true),
or(group(a.title, isLike(kw), or(a.content, isLike(kw))))))
.build().render(RenderingStrategies.MYBATIS3);
//@formatter:onwhere a.art_deleted = ?
and (a.art_enabled = ? or (a.art_title like ? or a.art_content like ?))조건 개수가 실행 중에 정해질 때
검색 대상 필드를 사용자가 고르는 화면이라면 조건 개수가 고정이 아닙니다. List<AndOrCriteriaGroup> 을 만들어 넘기면 됩니다.
List<AndOrCriteriaGroup> ors = new ArrayList<>();
if (searchContent) ors.add(or(a.content, isLike(kw)));
if (searchCategory) ors.add(or(a.boardCode, isEqualTo(category)));
//@formatter:off
select(a.id)
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.and(group(a.title, isLike(kw), ors)) // ← List 를 그대로 전달
.build().render(RenderingStrategies.MYBATIS3);
//@formatter:onwhere a.art_deleted = ?
and (a.art_title like ? or a.art_content like ? or a.art_bd_code = ?)화면 검색창은 대부분 직접 만들 필요가 없습니다
위는 리포지토리에서 직접 쿼리를 짤 때 이야기입니다.
CMS 목록 화면의 검색 조건은 프론트가 조건 트리({ao, conds})로 보내고, NvQueryBinder 가 알아서 괄호를 씌웁니다. 중첩 조건도 트리에 그대로 담기므로 서버 코드를 고칠 필요가 없습니다.
특히 결합자가 섞이면(AND 와 OR 혼재) 바인더가 왼쪽부터 접으며 괄호를 강제합니다 — 평평하게 두면 DB 의 우선순위 때문에 필터가 넓어지기 때문입니다.
[a, b(or), c(and)] → ((a OR b) AND c)자세한 내용은 검색 조건 참고.
NOT 과 EXISTS
.where(not(a.enabled, isEqualTo(true))) // NOT (...)
.where(exists(select(a2.id).from(a2, a2.getAlias())
.where(a2.boardCode, isEqualTo(a.boardCode))))where exists (select a2.art_id from iflexdb.nv_article a2
where a2.art_bd_code = a.art_bd_code)집계 — count · group by · having
건수만 세기
countFrom(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.build().render(RenderingStrategies.MYBATIS3);select count(*) from iflexdb.nv_article a where a.art_deleted = ?countDistinctColumn(a.boardCode).from(a, a.getAlias()) // 중복 제거 건수select count(distinct a.art_bd_code) from iflexdb.nv_article a실행은 selectOneLong 으로 받습니다 — resultMap 이 필요 없습니다.
default long countArticle() {
ArticleDsl a = ArticleDsl.defaultAlias();
return selectOneLong(countFrom(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.build().render(RenderingStrategies.MYBATIS3));
}그룹별 집계
됩니다. groupBy / having 과 집계 함수를 모두 지원합니다.
//@formatter:off
select(a.boardCode,
count().as("cnt"),
max(a.read).as("max_read"),
avg(a.read).as("avg_read"))
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.groupBy(a.boardCode)
.having(count(), isGreaterThan(10L))
.orderBy(sortColumn("cnt").descending())
.build().render(RenderingStrategies.MYBATIS3);
//@formatter:onselect a.art_bd_code, count(*) as cnt, max(a.art_read) as max_read, avg(a.art_read) as avg_read
from iflexdb.nv_article a
where a.art_deleted = ?
group by a.art_bd_code
having count(*) > ?
order by cnt DESC사용 가능한 집계·함수:
| 함수 | SQL |
|---|---|
count() / count(col) | count(*) / count(col) |
countDistinctColumn(col) | count(distinct col) |
sum(col) / avg(col) | sum / avg |
min(col) / max(col) | min / max |
concat(col, ...) / concatenate(...) | 문자열 결합 |
substring(col, start, len) | 부분 문자열 |
add(col, ...) / multiply(col, ...) | 산술 |
집계 결과는 엔터티에 안 담깁니다
count(*) as cnt 같은 컬럼은 Article 엔터티에 자리가 없습니다. @ResultMap("resultMap") 으로는 받을 수 없다는 뜻입니다.
받는 방법은 바로 다음 절 — resultMap 없이 받기 입니다.
resultMap 없이 결과 받기
집계나 임시 통계처럼 엔터티에도 응답 DTO 에도 자리가 없는 결과를 위해, XML 선언 없이 결과를 받는 방법이 있습니다.
NvRepositoryDynamic 은 CommonSelectMapper 를 상속하므로, 모든 리포지토리가 이 메서드들을 이미 가지고 있습니다.
단일 값
long total = selectOneLong(provider); // count 등
String name = selectOneString(provider);
Integer n = selectOneInteger(provider);
List<Long> ids = selectManyLongs(provider); // ID 목록| 메서드 | 반환 |
|---|---|
selectOneLong / selectOptionalLong / selectManyLongs | Long |
selectOneInteger / selectOptionalInteger / selectManyIntegers | Integer |
selectOneString / selectOptionalString / selectManyStrings | String |
selectOneDouble / selectOneBigDecimal (+ Optional/Many) | 실수 |
콜백으로 원하는 객체에 담기
행 하나를 Map<String, Object> 으로 받아, 콜백 안에서 원하는 객체로 바꾸는 방식입니다. resultMap 을 선언하지 않아도 됩니다.
/** 게시판별 글 수 통계 — 전용 resultMap 없이 record 로 받는다 */
default List<ArticleStat> statByBoard() {
ArticleDsl a = ArticleDsl.defaultAlias();
//@formatter:off
SelectStatementProvider provider =
select(a.boardCode.as("board_code"),
SqlBuilder.count().as("cnt"), // ← 한정 호출에 주의 (아래 설명)
max(a.createdDt).as("last_dt"))
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.groupBy(a.boardCode)
.build().render(RenderingStrategies.MYBATIS3);
//@formatter:on
return selectMany(provider, row -> new ArticleStat(
(String) row.get("board_code"),
((Number) row.get("cnt")).longValue(),
(Instant) row.get("last_dt")
));
}
record ArticleStat(String boardCode, long count, Instant lastDt) {}렌더되는 SQL:
select a.art_bd_code as board_code, count(*) as cnt, max(a.art_created_dt) as last_dt
from iflexdb.nv_article a
where a.art_deleted = ?
group by a.art_bd_code리포지토리 안에서는 count() 를 그냥 못 씁니다
NvRepositoryDynamic 이 이미 count(QueryBuilder) / count(NvQuery) 를 가지고 있어서, 리포지토리 인터페이스 안에서는 상속된 count 가 import static SqlBuilder.count 를 가립니다.
count().as("cnt") // ❌ error: no suitable method found for count(no arguments)
SqlBuilder.count().as("cnt") // ✅static import 를 해 두었는데도 나는 에러라 원인을 찾기 어렵습니다. 리포지토리 default 메서드 안에서 집계를 쓸 땐 SqlBuilder. 를 붙이세요. (테스트나 서비스 클래스처럼 NvRepositoryDynamic 을 상속하지 않는 곳에서는 그냥 count() 로 됩니다.)
| 메서드 | 설명 |
|---|---|
selectMany(provider, row -> ...) | 각 행(Map)을 원하는 타입으로 |
selectOne(provider, row -> ...) | 단건 |
selectManyMappedRows(provider) | 변환 없이 List<Map<String,Object>> 그대로 |
selectOneMappedRow(provider) | 단건 Map |
Map 의 키는 반드시 .as("...") 로 고정하세요
키는 DB 드라이버가 돌려준 컬럼 라벨 그대로입니다. 이 프로젝트는 mapUnderscoreToCamelCase 를 켜지 않았으므로 자동 변환도 없습니다.
별칭을 주지 않으면 DBMS 마다 대소문자가 달라집니다 — 특히 Oracle 은 대문자로 돌려줍니다.
select(a.boardCode, count()) // ❌ 키가 art_bd_code? ART_BD_CODE? count(*)?
select(a.boardCode.as("board_code"), count().as("cnt")) // ✅ 키가 확정된다as() 로 이름을 고정하면 4개 DBMS 에서 같은 키로 읽을 수 있습니다.
언제 콜백이고 언제 resultMap 인가
| 상황 | 방법 |
|---|---|
| 목록·상세 등 화면이 반복해서 쓰는 응답 | {X}Dto + resultMap |
| 집계·통계·일회성 조회 | 콜백 |
| 단일 스칼라(건수, ID 목록) | selectOneLong / selectManyLongs |
콜백은 컴파일 타임 타입 검사가 없습니다(row.get("cnt") 은 Object). 화면 계약이 되는 응답에는 쓰지 마세요 — 오타가 런타임 ClassCastException 이 됩니다.
서브쿼리 · UNION
서브쿼리
select(a.id, a.title)
.from(a, a.getAlias())
.where(a.boardCode, isIn(
select(a2.boardCode)
.from(a2, a2.getAlias())
.where(a2.enabled, isEqualTo(true))))select a.art_id, a.art_title from iflexdb.nv_article a
where a.art_bd_code in (select a2.art_bd_code from iflexdb.nv_article a2
where a2.art_enabled = ?)isEqualTo(select ...), isNotIn(select ...) 등도 같은 방식입니다.
UNION
select(a.id, a.title)
.from(a, a.getAlias()).where(a.enabled, isEqualTo(true))
.union()
select(a2.id, a2.title)
.from(a2, a2.getAlias()).where(a2.read, isGreaterThan(100))unionAll() 은 중복을 제거하지 않습니다.
정렬과 limit offset
selectDistinct(a.boardCode)
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.orderBy(a.boardCode)
.limit(20).offset(40)select distinct a.art_bd_code from iflexdb.nv_article a
where a.art_deleted = ? order by art_bd_code limit ? offset ?정렬 키를 문자열로 만들지 마세요
new SortBy("createdDt", ASC) // ❌ 화이트리스트에서 못 찾으면 조용히 드롭
a.asc(a.createdDt) // ✅ 리네임하면 컴파일 에러문자열 sortKey 는 QueryBuilder.resolveSortColumn 이 못 찾을 때 예외도 로그도 없이 폴백 정렬로 넘어갑니다. 컬럼을 리네임해도 빌드는 통과하고 정렬만 조용히 바뀝니다.
집계 별칭으로 정렬할 때만 sortColumn("cnt") 를 씁니다(그 별칭은 테이블 컬럼이 아니므로).
목록 화면의 페이징은 직접 limit/offset 을 쓰지 않습니다 — InPagination 이 처리하므로 목록과 페이징 을 보세요.
수정 · 삭제
// 상태만 바꾸기 — 다른 컬럼은 건드리지 않습니다
default int updateReadCount(Long id, int read) {
ArticleDsl a = ArticleDsl.defaultAlias();
//@formatter:off
return _update(update(a)
.set(a.read).equalTo(read)
.set(a.modifiedDt).equalTo(Instant.now())
.where(a.id, isEqualTo(id))
.build().render(RenderingStrategies.MYBATIS3));
//@formatter:on
}
// soft-delete
default int markAsDelete(Long id) {
ArticleDsl a = ArticleDsl.defaultAlias();
//@formatter:off
return _update(update(a)
.set(a.deleted).equalTo(true)
.where(a.id, isEqualTo(id))
.build().render(RenderingStrategies.MYBATIS3));
//@formatter:on
}전체 행 CRUD 는 직접 쓰지 않아도 됩니다 — NvRepositoryDynamic 이 제공합니다.
| 메서드 | 설명 |
|---|---|
get(id) | 단건 조회 |
list(builder, sortBy...) / page(...) | 목록 |
count(builder) | 건수 |
insert(t) | 등록 (키 자동 생성) |
insertSelective(t) | 등록 — null 컬럼 제외 (DB 기본값 보존) |
update(t) | 전체 수정 |
updateSelective(t) | null 아닌 필드만 수정 |
delete(id) | 물리 삭제 |
insert vs insertSelective
insert 는 모든 컬럼을 씁니다 — null 필드는 DB 에도 null 이 들어갑니다. insertSelective 는 null 필드를 아예 빼므로 DB 의 DEFAULT 값이 적용됩니다.
단, insertSelective 는 IDENTITY 방식에서 생성 키 회수를 보장하지 않습니다. 채번된 id 가 필요하면 insert 를 쓰세요.
키 생성 전략
iFlex 는 4개 DBMS 를 지원하는데 같은 엔터티라도 DDL 이 다릅니다 — 대부분 auto-increment 이지만 Oracle 은 시퀀스를 씁니다.
@TableInfo(dsl = ArticleDsl.class) // AUTO (기본)
@TableInfo(dsl = BannerDsl.class, sequenceName = "seq_banner") // 시퀀스 이름 지정
@TableInfo(dsl = TermDsl.class, keyStrategy = KeyStrategy.ASSIGNED) // PK 직접 지정| 전략 | 동작 |
|---|---|
AUTO (기본) | Oracle → SEQUENCE, 그 외 → IDENTITY 로 자동 해석 |
IDENTITY | auto-increment. getGeneratedKeys() 로 키 회수, id 는 insert 에서 제외 |
SEQUENCE | insert 전에 SELECT <seq>.NEXTVAL 로 id 확보 후 id 포함 insert |
ASSIGNED | 호출자가 id 를 직접 지정 (코드성 PK 등) |
시퀀스 이름을 지정하지 않으면 seq_<테이블명>_id 관례를 따릅니다.
왜 이렇게까지 하나
DBMS 별로 리포지토리를 따로 만들거나 if (isOracle()) 분기를 코드에 뿌리지 않기 위해서입니다. 개발자는 insert(article) 만 호출하고, 어느 DB 에서 도는지는 신경 쓰지 않습니다.
코드 포맷 규칙
SQL 을 코드로 쓰면 읽기 어려워지기 쉽습니다. 프로젝트는 SQL 키워드 우측정렬 규칙을 씁니다.
//@formatter:off
return select(a.allColumns())
.from(a, a.getAlias())
.where(a.deleted, isEqualTo(false))
.and(a.title, isLike(keyword))
.orderBy(a.desc(a.createdDt))
.build().render(RenderingStrategies.MYBATIS3);
//@formatter:on//@formatter:off 를 지우지 마세요
IDE 자동 포맷이 이 정렬을 망가뜨립니다. .editorconfig 에 IntelliJ 마커가 활성화돼 있으니 DSL 블록은 항상 //@formatter:off … //@formatter:on 으로 감싸세요.
그래서 XML 에는 무엇이 남나
| 대상 | 위치 |
|---|---|
| SELECT / INSERT / UPDATE / DELETE 문 | Java Dsl (필요하면 XML 에 직접 쓰기도 함) |
| resultMap (컬럼 → 객체 매핑) | XML |
<resultMap id="articleResponseResult" type="articleResponse">
<id property="id" column="art_id"/>
<result property="title" column="art_title"/>
<result property="config" column="art_config" typeHandler="...VarcharJsonTypeHandler"/>
<association property="createdUser" columnPrefix="ins_user_" resultMap="accountRefResult"/>
</resultMap>왜 결과 매핑만 XML 인가
Dynamic SQL 은 SQL 문을 만드는 라이브러리이고, 결과 매핑 기능이 아예 없습니다. 결과를 객체로 바꾸는 것은 MyBatis 본체의 일이며, 선언 수단은 XML 아니면 @Results 애노테이션입니다.
그중 XML 을 쓰는 이유는 매핑을 공유하기 위해서입니다. 두 방향으로 공유됩니다.
① 실행 경로가 둘인데 매핑은 하나 — SQL 은 Dsl 로 짤 때도 있고 XML 에 직접 쓸 때도 있습니다. 어느 쪽이든 같은 매핑을 가리킵니다.
adminResult ← XML 의 <select id="getAdmin"> 이 사용
resultMap ← 제네릭 CRUD 의 @ResultMap("resultMap") 이 사용
adminDtoResult ← provider + fetcher 가 사용
↓ 셋 다 extends
AccountRepository.simpleAccountResult② 모듈을 넘어 재사용 — <association> 과 extends 로 다른 매퍼의 매핑을 끌어씁니다. 등록자/수정자 매핑(AccountRepository.accountRefResult)은 현재 21곳에서 <association> 으로 참조됩니다. 애노테이션으로는 columnPrefix·extends·<collection> 을 표현할 수 없고 매퍼 간 공유도 안 됩니다.
typeHandler 는 양쪽에 다 씁니다
typeHandler 가 XML 전용이라 XML 을 쓰는 게 아닙니다. 방향이 달라서 둘 다 필요합니다.
| 위치 | 방향 |
|---|---|
{X}Dsl 의 SqlColumn | 쓰기 — insert/update 파라미터 바인딩 |
XML resultMap | 읽기 — 조회 결과 파싱 |
public final SqlColumn<JsonMap> config =
column("bd_config", JDBCType.LONGVARCHAR, "com.unvus.iflex.core.config.mybatis.type.VarcharJsonTypeHandler");제네릭 CRUD 는 insert/update 파라미터를 XML 이 아니라 Dsl 정의에서 렌더링합니다. 그래서 Dsl 에 빠뜨리면 읽기는 되는데 저장에서 Type ...JsonMap not supported type 로 실패합니다 (DslJsonMapTypeHandlerTest 가 전 Dsl 을 전수 검사).
엔터티당 resultMap 은 2종(resultMap = 엔터티, {X}DtoResult = 응답 DTO)만 유지합니다. 집계처럼 매핑할 곳이 없는 결과는 XML 을 만들지 말고 콜백으로 받으세요.
관련 문서
| 문서 | 내용 |
|---|---|
| 엔터티 만들기 | 새 테이블 추가 순서 (Dsl 이 왜 필요한지) |
| 목록과 페이징 | provider 로 목록 SQL 작성하기 |
| 검색 조건 | 조건 트리를 WHERE 로 변환 |
| MyBatis Dynamic SQL (GitHub) | 라이브러리 원본 |
| MyBatis Dynamic SQL 공식 문서 | 전체 레퍼런스 |