ResultMap, Constructor Mapping, Association, Collection, and TypeHandler
MyBatis Result Mapping
ResultMap, constructor mapping, association, collection, nested query, custom TypeHandler, JSONB/array mapping, null handling, and column aliasing for production-grade MyBatis usage.
MyBatis Result Mapping
1. Core Thesis
MyBatis result mapping adalah tempat SQL shape bertemu dengan Java object shape.
Di MyBatis, SQL ditulis eksplisit. Itu keunggulan besar. Tetapi hasil SQL tetap harus diterjemahkan menjadi object Java. Di sinilah banyak bug persistence layer muncul:
- kolom hasil query tidak cocok dengan field Java,
- join menghasilkan nama kolom ambigu,
resultTypedipakai untuk query kompleks,ResultMaptidak mendefinisikan identity dengan benar,- nested select diam-diam memicu N+1,
- nested result menghasilkan object graph duplikat,
- enum/string/status mapping tidak konsisten,
- JSONB diperlakukan sebagai string mentah tanpa ownership,
- null dari database tidak dipahami sebagai state domain,
- custom TypeHandler mengubah semantics data tanpa test.
Prinsip utama:
MyBatis mapping harus membuat shape data eksplisit. SQL boleh fleksibel, tetapi return object harus predictable, typed, dan bisa diuji.
Result mapping yang baik menjawab:
- query ini mengembalikan projection atau aggregate?
- kolom mana yang wajib ada?
- kolom mana yang nullable?
- apakah mapping ini read-only?
- apakah mapping ini digunakan untuk command path atau read model?
- apakah nested object dimuat lewat join atau nested select?
- apakah ada risiko N+1?
- apakah PostgreSQL-specific type ditangani secara eksplisit?
Di enterprise system, mapping bukan detail kecil. Mapping menentukan apakah service layer membaca fakta yang benar dari database.
2. Mental Model: SQL Row Shape vs Java Object Shape
Setiap query MyBatis memiliki dua shape:
-
SQL row shape
Bentuk data yang dikembalikan PostgreSQL: column names, aliases, types, nullability, duplicate rows, ordering. -
Java object shape
Bentuk object yang dikembalikan ke application layer: DTO, domain object, projection, command result, nested graph.
Contoh query sederhana:
SELECT
q.id,
q.quote_number,
q.status,
q.created_at
FROM quote q
WHERE q.id = #{quoteId}
Mapping-nya relatif mudah karena satu row menjadi satu object.
Contoh query kompleks:
SELECT
q.id AS quote_id,
q.quote_number AS quote_number,
q.status AS quote_status,
qi.id AS item_id,
qi.product_code AS item_product_code,
qi.quantity AS item_quantity
FROM quote q
LEFT JOIN quote_item qi ON qi.quote_id = q.id
WHERE q.id = #{quoteId}
ORDER BY qi.line_number
Di sini satu quote bisa muncul di banyak row karena join ke item. MyBatis harus tahu bahwa:
quote_idadalah identity parent,item_idadalah identity child,- beberapa row harus digabung menjadi satu object graph,
- child collection harus diisi tanpa menduplikasi parent.
Jika identity mapping salah, bug-nya bisa halus:
- parent object terduplikasi,
- child hilang,
- collection berisi duplikasi,
- object graph terlihat benar untuk data kecil tetapi rusak untuk data nyata.
3. resultType vs ResultMap
MyBatis menyediakan dua pendekatan utama:
resultType: mapping otomatis berdasarkan nama kolom ke property Java,ResultMap: mapping eksplisit antara column dan property.
3.1 Kapan resultType cukup
resultType cukup untuk query sederhana:
- satu table atau projection sederhana,
- column alias sudah cocok dengan property Java,
- tidak ada nested object,
- tidak ada custom conversion,
- tidak ada ambiguity akibat join,
- return shape stabil.
Contoh:
<select id="findQuoteSummaryById" resultType="com.example.quote.QuoteSummaryDto">
SELECT
id,
quote_number AS quoteNumber,
status,
created_at AS createdAt
FROM quote
WHERE id = #{quoteId}
</select>
Ini masih eksplisit karena alias SQL mengarah ke field DTO.
3.2 Kapan harus memakai ResultMap
Gunakan ResultMap jika:
- query memakai join,
- ada nested object,
- ada collection,
- ada custom type conversion,
- column name tidak cocok dengan property,
- ada constructor mapping,
- ada enum/JSONB/array mapping,
- perlu mendefinisikan
idmapping, - return object dipakai di path penting.
Contoh:
<resultMap id="QuoteWithItemsResultMap" type="com.example.quote.QuoteWithItems">
<id property="id" column="quote_id" />
<result property="quoteNumber" column="quote_number" />
<result property="status" column="quote_status" />
<collection property="items" ofType="com.example.quote.QuoteItemDto">
<id property="id" column="item_id" />
<result property="productCode" column="item_product_code" />
<result property="quantity" column="item_quantity" />
</collection>
</resultMap>
Rule praktis:
Jika query lebih dari satu table atau return object lebih dari satu level, default ke
ResultMap.
4. ResultMap as Explicit Contract
ResultMap harus dianggap sebagai contract, bukan boilerplate.
Contract ini menyatakan:
- column mana yang membentuk identity object,
- column mana yang mengisi property biasa,
- nested association apa yang dimuat,
- collection apa yang digabung dari multiple rows,
- TypeHandler apa yang mengubah database type ke Java type,
- null database diterjemahkan menjadi apa.
Contoh naming yang baik:
<resultMap id="QuoteDetailReadModelResultMap" type="com.example.quote.readmodel.QuoteDetailReadModel">
...
</resultMap>
Nama ini lebih baik daripada:
<resultMap id="BaseResultMap" type="Quote">
...
</resultMap>
Masalah BaseResultMap:
- terlalu generik,
- mudah dipakai ulang sembarangan,
- tidak menjelaskan use case,
- sering menjadi tempat semua kolom dimasukkan,
- membuat mapping membesar tanpa ownership.
Guideline naming:
| Mapping Type | Naming Example |
|---|---|
| Simple row | QuoteRowResultMap |
| Detail page read model | QuoteDetailReadModelResultMap |
| Aggregate loading | QuoteAggregateResultMap |
| Reporting projection | QuoteAgingReportResultMap |
| Command result | QuoteSubmitResultMap |
5. Identity Mapping with <id>
<id> bukan hanya dokumentasi. Untuk nested result, <id> membantu MyBatis menggabungkan row yang sama menjadi object yang sama.
Contoh parent-child:
<resultMap id="QuoteWithItemsResultMap" type="QuoteWithItems">
<id property="id" column="quote_id" />
<result property="quoteNumber" column="quote_number" />
<collection property="items" ofType="QuoteItemDto">
<id property="id" column="item_id" />
<result property="productCode" column="item_product_code" />
</collection>
</resultMap>
Jika <id> child tidak ada, MyBatis bisa gagal membedakan child rows dengan benar pada mapping kompleks.
Failure mode:
- collection duplikat,
- item hilang,
- parent object tidak tergabung,
- object graph berubah tergantung urutan row.
Checklist:
- Parent punya
<id>jelas. - Child collection punya
<id>jelas jika berasal dari table/entity berbeda. - Alias column identity tidak ambiguous.
- Composite key dimodelkan eksplisit jika ada.
- Query ordering tidak dijadikan pengganti identity mapping.
6. Column Aliasing Discipline
Join query wajib memakai alias kolom yang eksplisit.
Anti-pattern:
SELECT
q.id,
q.status,
c.id,
c.status
FROM quote q
JOIN customer c ON c.id = q.customer_id
Masalah:
- dua kolom bernama
id, - dua kolom bernama
status, - driver/result set bisa mengekspos nama kolom ambigu,
- mapping bisa salah diam-diam.
Better:
SELECT
q.id AS quote_id,
q.status AS quote_status,
c.id AS customer_id,
c.status AS customer_status
FROM quote q
JOIN customer c ON c.id = q.customer_id
Rule:
Untuk query join, jangan pernah mengandalkan nama kolom mentah. Alias semua kolom yang berpotensi ambigu.
Alias harus menunjukkan ownership:
quote_id,quote_status,quote_created_at,customer_id,customer_name,item_id,item_product_code,price_id,price_amount.
Di sistem CPQ/order/telco, aliasing sangat penting karena domain biasanya memiliki banyak table dengan field generik:
id,status,type,code,name,created_at,updated_at,version,tenant_id,effective_date.
7. Constructor Mapping
Constructor mapping berguna untuk immutable DTO/read model.
Contoh Java:
public record QuoteSummary(
UUID id,
String quoteNumber,
String status,
Instant createdAt
) {}
Contoh MyBatis:
<resultMap id="QuoteSummaryResultMap" type="com.example.quote.QuoteSummary">
<constructor>
<arg column="id" javaType="java.util.UUID" />
<arg column="quote_number" javaType="java.lang.String" />
<arg column="status" javaType="java.lang.String" />
<arg column="created_at" javaType="java.time.Instant" />
</constructor>
</resultMap>
Keuntungan:
- object immutable,
- tidak perlu setter,
- missing field lebih cepat terlihat,
- cocok untuk read model/projection.
Risiko:
- urutan arg harus benar jika tidak memakai name-based mapping,
- constructor berubah bisa memecahkan mapping,
- nullable column ke primitive type bisa error,
- refactor record/class harus sinkron dengan mapper XML.
Practical guidance:
- Gunakan constructor mapping untuk read model stabil.
- Hindari constructor mapping untuk object yang terlalu besar.
- Test mapping harus memverifikasi semua field penting.
- Jangan pakai constructor mapping untuk menyembunyikan query yang terlalu kompleks.
8. Association Mapping
association digunakan untuk nested single object.
Contoh:
<resultMap id="QuoteWithCustomerResultMap" type="QuoteWithCustomer">
<id property="id" column="quote_id" />
<result property="quoteNumber" column="quote_number" />
<association property="customer" javaType="CustomerSummary">
<id property="id" column="customer_id" />
<result property="name" column="customer_name" />
<result property="segment" column="customer_segment" />
</association>
</resultMap>
Association cocok untuk:
- many-to-one lookup,
- embedded summary object,
- object kecil yang memang bagian dari read model,
- data yang selalu diperlukan use case.
Association tidak selalu cocok untuk domain aggregate write model. Jika object association bisa dimodifikasi, hati-hati agar service layer tidak mengira MyBatis akan mengelola lifecycle seperti JPA.
MyBatis tidak melakukan dirty checking. Object hasil association hanyalah hasil mapping.
9. Collection Mapping
collection digunakan untuk nested list/set.
Ada dua pendekatan besar:
- Nested result: satu query join menghasilkan parent dan children.
- Nested select: query parent memanggil query lain untuk children.
9.1 Nested result
<resultMap id="QuoteWithItemsResultMap" type="QuoteWithItems">
<id property="id" column="quote_id" />
<result property="quoteNumber" column="quote_number" />
<collection property="items" ofType="QuoteItemDto">
<id property="id" column="item_id" />
<result property="productCode" column="item_product_code" />
<result property="quantity" column="item_quantity" />
</collection>
</resultMap>
Keuntungan:
- satu round trip database,
- SQL visible,
- tidak ada N+1 query,
- cocok untuk detail read model.
Risiko:
- row explosion jika join terlalu banyak collection,
- parent data berulang di result set,
- mapping identity harus benar,
- pagination parent menjadi sulit jika join ke collection.
9.2 Nested select
<resultMap id="QuoteWithItemsNestedSelectResultMap" type="QuoteWithItems">
<id property="id" column="quote_id" />
<result property="quoteNumber" column="quote_number" />
<collection property="items" column="quote_id" select="findItemsByQuoteId" />
</resultMap>
Risiko utama:
Nested select sering menjadi N+1 problem.
Jika query parent mengembalikan 100 quote, lalu setiap quote memanggil findItemsByQuoteId, total query menjadi 101.
Nested select hanya aman jika:
- parent cardinality sangat kecil,
- query memang hanya untuk satu aggregate by id,
- lazy loading tidak menyebabkan query di layer tak terduga,
- query count ditest,
- performance acceptable secara evidence.
10. Nested Select vs Nested Result Decision
| Scenario | Prefer |
|---|---|
| Load one aggregate by ID with small children | Nested result atau nested select terkontrol |
| List page dengan banyak parent | Avoid nested select |
| Reporting/read model | Explicit join + projection |
| Parent pagination diperlukan | Two-step query: page parent IDs, then fetch children by IDs |
| Child cardinality besar | Separate query with explicit endpoint/use case |
| Need predictable query count | Nested result atau manual batch query |
Two-step pattern:
- Query parent IDs dengan pagination.
- Query parent detail by IDs.
- Query children by parent IDs.
- Assemble di service/read model assembler.
Pattern ini sering lebih jelas daripada nested mapping yang terlalu pintar.
11. Discriminator Mapping
discriminator digunakan jika row bisa dipetakan ke subtype berbeda berdasarkan column value.
Contoh konseptual:
<resultMap id="ChargeResultMap" type="Charge">
<id property="id" column="charge_id" />
<discriminator javaType="string" column="charge_type">
<case value="ONE_TIME" resultMap="OneTimeChargeResultMap" />
<case value="RECURRING" resultMap="RecurringChargeResultMap" />
</discriminator>
</resultMap>
Gunakan dengan hati-hati.
Discriminator bisa berguna untuk:
- read model polymorphic,
- legacy schema dengan type column,
- charge/price/discount model yang memiliki subtype.
Tetapi discriminator bisa menjadi smell jika:
- subtype logic terlalu kompleks,
- banyak field nullable karena satu table menampung banyak type,
- mapping menjadi sulit direview,
- business rule tersebar di XML.
Untuk CPQ/pricing/catalog domain, subtype sering muncul. Pastikan mapping tidak menggantikan domain model yang seharusnya lebih eksplisit.
12. Enum Mapping
Enum mapping harus konsisten antara database, Java, API, dan migration.
Pilihan umum:
- Store as text/varchar.
- Store as PostgreSQL enum.
- Store as code table/reference table.
- Store as integer code.
Untuk MyBatis, mapping bisa langsung jika value database cocok dengan nama enum.
public enum QuoteStatus {
DRAFT,
SUBMITTED,
APPROVED,
REJECTED
}
SELECT status FROM quote
Jika database value berbeda dari enum name, gunakan TypeHandler.
Risiko enum mapping:
- enum Java rename memecahkan data lama,
- database value baru belum dikenal Java,
- lowercase/uppercase tidak konsisten,
- status domain punya lifecycle tetapi dimodelkan sebagai enum pasif,
- migration menambah status tanpa update code.
Checklist:
- Apakah status value dikontrol migration?
- Apakah unknown value ditangani?
- Apakah enum dipakai di API contract?
- Apakah status transition divalidasi domain/service?
- Apakah MyBatis TypeHandler punya test?
13. JSONB Mapping
PostgreSQL JSONB sering digunakan untuk flexible attributes, product metadata, request snapshot, pricing payload, atau integration payload.
MyBatis bisa memetakan JSONB sebagai:
String,JsonNode,- domain-specific value object,
Map<String, Object>,- custom class via TypeHandler.
13.1 Mapping JSONB as String
Keuntungan:
- sederhana,
- tidak perlu dependency object mapper di persistence layer,
- cocok untuk opaque payload.
Risiko:
- tidak typed,
- validation sulit,
- field access tersebar,
- raw JSON bisa bocor ke API/log.
13.2 Mapping JSONB as JsonNode
Keuntungan:
- fleksibel,
- bisa inspect field tertentu,
- cocok untuk semi-structured payload.
Risiko:
- domain invariants tidak kuat,
- manipulation bisa tersebar,
- testing harus lebih disiplin.
13.3 Mapping JSONB as Value Object
Keuntungan:
- typed,
- invariant bisa dipusatkan,
- lebih aman untuk domain penting.
Risiko:
- schema JSON harus stabil,
- backward compatibility JSON perlu dikelola,
- TypeHandler harus robust.
Contoh conceptual TypeHandler:
public final class JsonbTypeHandler<T> extends BaseTypeHandler<T> {
private final ObjectMapper objectMapper;
private final Class<T> type;
@Override
public void setNonNullParameter(
PreparedStatement ps,
int i,
T parameter,
JdbcType jdbcType
) throws SQLException {
try {
PGobject jsonObject = new PGobject();
jsonObject.setType("jsonb");
jsonObject.setValue(objectMapper.writeValueAsString(parameter));
ps.setObject(i, jsonObject);
} catch (JsonProcessingException e) {
throw new SQLException("Failed to serialize JSONB parameter", e);
}
}
}
Production concerns:
- serialization failure harus jelas,
- unknown field handling harus diputuskan,
- migration JSON shape harus dipikirkan,
- JSON path index harus sesuai query,
- PII di JSONB harus tetap direview.
14. PostgreSQL Array Mapping
PostgreSQL array bisa berguna tetapi harus digunakan dengan disiplin.
Contoh use case:
- list of tags,
- product features,
- allowed channels,
- small immutable set,
- technical metadata.
Risiko:
- relational query menjadi lebih sulit,
- constraint lebih lemah daripada normalized table,
- indexing harus spesifik,
- mapping ke Java list/array perlu TypeHandler,
- update partial bisa tidak jelas.
Guideline:
- Gunakan array untuk small bounded values.
- Hindari array untuk child entity yang punya lifecycle sendiri.
- Hindari array jika perlu foreign key per element.
- Pastikan query operator dan index strategy dipahami.
15. Custom TypeHandler
TypeHandler adalah extension point penting di MyBatis.
Digunakan untuk:
- JSONB,
- PostgreSQL enum,
- array,
- money/currency value object,
- encrypted value,
- domain-specific identifier,
- custom timestamp/timezone handling.
TypeHandler adalah code persistence yang high-risk karena ia mengubah representasi database menjadi object Java.
Checklist TypeHandler:
- Apakah null handling eksplisit?
- Apakah serialization/deserialization error jelas?
- Apakah timezone ditangani benar?
- Apakah unknown enum value ditangani?
- Apakah test memakai PostgreSQL nyata?
- Apakah log tidak membocorkan PII?
- Apakah handler reusable atau terlalu domain-specific?
Anti-pattern:
catch (Exception e) {
return null;
}
Ini buruk karena data corrupt terlihat seperti null valid.
Better:
- throw exception dengan context aman,
- map unknown value ke explicit
UNKNOWNjika domain mengizinkan, - fail fast untuk data yang tidak valid.
16. Null Handling
Null di persistence layer bukan detail kecil.
Null bisa berarti:
- data belum tersedia,
- field optional,
- data tidak berlaku untuk subtype tertentu,
- data corrupt,
- migration belum backfilled,
- left join tidak menemukan child,
- soft-deleted row tidak ikut join,
- tenant filter menghilangkan row.
Mapping null harus sesuai semantics.
Risiko umum:
- nullable DB column dipetakan ke primitive Java,
- left join child null menghasilkan empty object palsu,
- null status dianggap default,
- null timestamp dianggap epoch,
- null JSON dianggap
{}tanpa alasan, - missing row tidak dibedakan dari row dengan null fields.
Guideline:
- Gunakan wrapper type, bukan primitive, untuk nullable field.
- Jangan default null di TypeHandler tanpa alasan domain.
- Bedakan
not foundvsfound with null fields. - Untuk left join nested object, pastikan identity child null tidak membuat object kosong.
- Test mapping untuk null cases.
17. Mapping Domain Object vs DTO Projection
MyBatis sering lebih cocok untuk DTO projection/read model daripada domain aggregate rich object.
DTO projection cocok untuk:
- list page,
- search result,
- report,
- dashboard,
- export,
- read-only endpoint,
- integration response shape.
Domain object mapping cocok jika:
- object dipakai untuk business rule,
- lifecycle dikelola application service,
- invariants dipastikan setelah load,
- write path eksplisit via mapper method.
Risiko mapping langsung ke domain object:
- domain object menerima partially loaded state,
- business method mengira object lengkap,
- nullable persistence state masuk ke invariant domain,
- read model shape mencemari domain model.
Rule praktis:
Jika query hanya untuk membaca tampilan/API/report, gunakan read model/projection. Jangan memaksa domain object.
18. Mapping and Transaction Boundary
Result mapping biasanya terjadi saat query dieksekusi dalam transaction atau connection scope.
Perhatikan:
- nested select bisa membuka query tambahan dalam scope yang sama,
- lazy loading MyBatis jika digunakan bisa memicu query setelah service method,
- cursor/streaming result membutuhkan connection tetap terbuka,
- mapping besar bisa memperpanjang transaction duration,
- TypeHandler lambat bisa memperpanjang waktu connection dipinjam.
Di JAX-RS request lifecycle, jangan biarkan object hasil MyBatis memicu query saat serialization response. Jika lazy loading aktif dan query terjadi saat JSON serialization, boundary sudah bocor.
Guideline:
- Load semua data yang dibutuhkan di service/application layer.
- Return DTO yang sudah lengkap ke resource/controller.
- Jangan biarkan serialization memanggil database.
- Ukur query count di integration test.
19. PostgreSQL-Specific Mapping Concerns
PostgreSQL memiliki type dan behavior yang perlu dipahami:
uuid,jsonb,text[]atau array type lain,numeric,timestamp with time zone,timestamp without time zone,- enum type,
- composite query result,
RETURNING,- generated columns,
- trigger-modified columns.
Mapping concern:
numericuntuk money jangan asal kedouble,- timestamp harus jelas timezone semantics-nya,
- trigger-updated column mungkin berbeda dari object input,
RETURNINGdapat dipakai untuk mengambil generated/audited value,- JSONB operator query harus tetap punya mapping typed.
Contoh RETURNING:
<select id="insertQuoteReturningId" resultType="java.util.UUID">
INSERT INTO quote (quote_number, status, created_at)
VALUES (#{quoteNumber}, #{status}, now())
RETURNING id
</select>
Untuk PostgreSQL, RETURNING sering lebih jelas daripada melakukan insert lalu select terpisah.
20. Failure Modes
20.1 Column alias collision
Symptom:
- field terisi nilai table lain,
- object terlihat valid tetapi salah,
- bug muncul hanya saat join ditambah.
Detection:
- review SQL aliases,
- assert field values di mapper test,
- log result shape saat debugging lokal.
20.2 Nested select N+1
Symptom:
- endpoint lambat saat data bertambah,
- query count naik linear terhadap jumlah parent,
- database round trip tinggi.
Detection:
- SQL log,
- query count assertion,
- APM trace,
- PostgreSQL query stats.
20.3 Wrong identity mapping
Symptom:
- duplicate child,
- missing child,
- parent duplicate,
- mapping berubah saat order query berubah.
Detection:
- test dengan parent multiple children,
- test dengan duplicate-looking values,
- verify
<id>mapping.
20.4 TypeHandler hides bad data
Symptom:
- invalid enum menjadi null,
- invalid JSON menjadi empty object,
- data corrupt tidak terlihat.
Detection:
- negative tests,
- fail-fast behavior,
- logging error tanpa PII.
20.5 Null semantics bug
Symptom:
- optional field dianggap default,
- left join missing child menjadi object kosong,
- API mengembalikan misleading data.
Detection:
- null test cases,
- left join tests,
- domain invariant checks.
21. Debugging Playbook
Saat mapping MyBatis bermasalah:
- Ambil SQL final yang dieksekusi.
- Jalankan SQL langsung di PostgreSQL console.
- Lihat nama kolom aktual yang keluar.
- Bandingkan dengan
ResultMap/DTO property. - Cek duplicate column names.
- Cek
<id>mapping untuk nested result. - Cek nested select query count.
- Cek TypeHandler untuk field custom.
- Cek null cases.
- Tambahkan mapper integration test dengan dataset minimal yang mereproduksi bug.
Jangan langsung memperbaiki object Java sebelum memastikan row shape SQL benar.
22. Testing Strategy for Result Mapping
Mapper test harus membuktikan mapping, bukan hanya query tidak error.
Test minimal:
- SQL syntax valid di PostgreSQL nyata,
- semua field penting terisi benar,
- null field dipetakan benar,
- enum/JSONB/array dipetakan benar,
- join alias tidak ambigu,
- nested collection tidak duplikat,
- nested select query count acceptable,
- unknown value behavior jelas,
- migration schema compatible dengan mapper.
Example assertion style:
QuoteWithItems quote = mapper.findQuoteWithItems(quoteId);
assertThat(quote.id()).isEqualTo(quoteId);
assertThat(quote.items()).hasSize(2);
assertThat(quote.items())
.extracting(QuoteItemDto::productCode)
.containsExactly("PLAN_A", "ADDON_B");
Untuk mapping kompleks, test data harus sengaja mencakup:
- multiple children,
- child null karena left join,
- duplicate-like values,
- enum value berbeda,
- JSONB dengan optional field,
- array kosong dan null.
23. PR Review Checklist
Gunakan checklist ini saat mereview mapper result mapping:
- Apakah query sederhana cukup dengan
resultType, atau seharusnyaResultMap? - Apakah semua kolom join punya alias eksplisit?
- Apakah
ResultMapmemiliki nama yang mencerminkan use case? - Apakah parent dan child identity didefinisikan dengan
<id>? - Apakah nested select bisa menimbulkan N+1?
- Apakah nested result bisa menimbulkan row explosion?
- Apakah DTO projection lebih tepat daripada domain object?
- Apakah nullable column dipetakan ke nullable Java type?
- Apakah enum/JSONB/array memakai TypeHandler yang diuji?
- Apakah mapping membaca field tenant/soft delete/effective date dengan benar?
- Apakah mapping test mencakup realistic dataset?
- Apakah query result shape berubah akibat migration?
- Apakah SQL logging cukup untuk debugging?
24. Internal Verification Checklist
Untuk konteks internal CSG/team, jangan mengasumsikan struktur repository. Verifikasi langsung:
- Di mana mapper interface disimpan?
- Di mana mapper XML disimpan?
- Apakah ada naming convention untuk
ResultMap? - Apakah
BaseResultMapumum dipakai? - Apakah query join wajib alias kolom?
- Apakah ada custom TypeHandler untuk JSONB, enum, array, UUID, money, atau encrypted value?
- Apakah mapper mengembalikan entity/domain object atau DTO projection?
- Apakah nested select diperbolehkan?
- Apakah ada query count test?
- Apakah ada standard untuk handling null?
- Apakah mapper test memakai PostgreSQL nyata/Testcontainers?
- Apakah migration PR wajib mengecek mapper impact?
- Apakah slow query incident pernah disebabkan nested mapping?
- Apakah ada dashboard/log untuk query count dan duration?
25. Senior Engineer Heuristics
Beberapa heuristik praktis:
-
Untuk list/search/report, utamakan projection.
Jangan load object graph besar jika hanya butuh beberapa field. -
Untuk join query, alias semua kolom.
Ambiguity mapping adalah bug yang murah dicegah tetapi mahal dideteksi. -
Untuk nested collection, pikirkan cardinality dulu.
Mapping yang benar secara fungsi belum tentu benar secara performance. -
Untuk JSONB/enum/array, TypeHandler harus punya test.
Custom conversion tanpa test adalah correctness risk. -
Untuk mapping kompleks, jangan takut assemble manual.
Kadang dua query eksplisit + assembler lebih mudah direview daripadaResultMapterlalu pintar. -
Mapping adalah bagian dari contract use case.
Jangan reuse mapping besar hanya karena sudah ada.
26. Summary
MyBatis result mapping adalah area tempat bug halus sering lahir.
Key takeaways:
resultTypecocok untuk query sederhana.ResultMapwajib untuk join, nested object, custom type, dan mapping kompleks.<id>mapping penting untuk nested result.- Column aliasing adalah disiplin wajib untuk join query.
- Nested select bisa memicu N+1.
- Nested result bisa memicu row explosion.
- JSONB, enum, array, dan custom value object harus dimapping dengan TypeHandler yang diuji.
- Null handling harus mencerminkan semantics domain.
- DTO projection sering lebih sehat daripada memaksa domain object.
- Mapping harus direview sebagai bagian dari correctness, performance, security, dan production readiness.
Result mapping yang baik membuat SQL-first MyBatis tetap aman untuk enterprise production system.
You just completed lesson 07 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.