Tài liệu này trình bày các bước tách biệt mô hình domain (domain model) và mô hình hạ tầng (infrastructure model) dành cho MongoDB, sử dụng ví dụ về TestDomainItem. Mỗi bước đều bao gồm các ưu, nhược điểm và những đánh giá đánh đổi (trade-offs) để bạn cân nhắc.
✅ Bước 1: Sử dụng trực tiếp Annotation của MongoDB trong Domain Model
import org.springframework.data.annotation.Id
import org.bson.types.ObjectId

data class TestDomainItem(
 @Id val id: ObjectId? = null,
 val itemSequence: String,
 val itemName: String
)
Ưu điểm
  • Phát triển nhanh và đơn giản nhất.
  • Tận dụng hỗ trợ mapping tự động của Spring Data MongoDB.
    Nhược điểm và Lưu ý
  • Mô hình domain bị phụ thuộc trực tiếp vào spring-data-mongodb và bson.
  • Khó coi đây là một "mô hình thuần túy" (pure model) trong kiểm thử hoặc logic nghiệp vụ.
  • Gây kết nối chặt chẽ (strong coupling) khi muốn di chuyển sang các loại DB khác (R2DBC, JPA, v.v.) trong tương lai.
✅ Bước 2: Loại bỏ @Id, chỉ khai báo trường _id
data class TestDomainItem(
 val _id: ObjectId? = null,
 val itemSequence: String,
 val itemName: String
)
Ưu điểm
  • Tăng tính thuần túy một chút bằng cách loại bỏ annotation.
  • Tự động khớp với trường _id của MongoDB.
    Nhược điểm và Lưu ý
  • _id vẫn là một tên trường mang tính đặc thù của cơ sở dữ liệu.
  • Mô hình domain vẫn phụ thuộc gián tiếp vào cấu trúc của MongoDB.
  • Khi chuyển sang loại DB khác, trường này sẽ trở nên dư thừa và gây "bẩn" code.
✅Bước 3: Tách biệt hoàn toàn Domain Model và Mongo Model
Domain Model
data class TestDomainItem(
 val itemSequence: String,
 val itemName: String
)
Infra Model (Mô hình hạ tầng)
@Document(collection = "test_items")
data class TestDomainItemDocument(
 @Id val id: ObjectId? = null,
 val itemSequence: String,
 val itemName: String
) {
 companion object {
     const val COLLECTION = "test_items"
 }
}
Ưu điểm
  • Đảm bảo tính thuần túy hoàn toàn cho Domain Model.
  • Sự phụ thuộc vào MongoDB chỉ tồn tại ở tầng Infrastructure.
  • Trách nhiệm rõ ràng thông qua lớp chuyển đổi (toDomain, fromDomain).
    Nhược điểm và Lưu ý
  • Phải viết code chuyển đổi lặp đi lặp lại nếu có nhiều trường thông tin.
  • Cần viết các bài kiểm thử ánh xạ (mapping test) giữa Domain và Infra.
✅ Bước 4: Đơn giản hóa Infra Model bằng cấu trúc lồng nhau (Nested)
@Document(collection = "test_items")
data class TestDomainItemDocument(
 @Id val id: ObjectId? = null,
 val item: TestDomainItem
)
💡 LƯU Ý: Các DB NoSQL như MongoDB hỗ trợ lưu trữ và truy vấn cấu trúc lồng nhau một cách tự nhiên. Tuy nhiên, trong RDB, bạn không thể biểu diễn đối tượng lồng nhau dưới dạng một cột mà phải làm phẳng (flatten) tất cả thuộc tính thành các cột riêng biệt. Do đó, cấu trúc lồng nhau này đặc thù cho NoSQL và cần được chuyển đổi nếu di chuyển sang RDB.
Ưu điểm
  • Code chuyển đổi giữa Infra ↔ Domain trở nên cực kỳ đơn giản.
  • Loại bỏ sự trùng lặp bằng cách chứa trực tiếp đối tượng domain.
    Nhược điểm và Lưu ý
  • Cấu trúc lưu trữ sẽ ở dạng tài liệu lồng nhau (Ví dụ: { item: {...}, _id: ... }).
  • Khi viết query Mongo, đường dẫn sẽ sâu hơn (Ví dụ: "item.itemSequence").
  • Cần lưu ý đường dẫn bổ sung khi tạo Index.
✅ Chiến lược khuyến nghị cuối cùng
  • Giữ Domain Model thuần túy.
  • Tách riêng mô hình lưu trữ Mongo, chỉ đặt các annotation @Document, @Id ở phía Infra.
  • Thực hiện chuyển đổi đơn giản bằng các hàm mở rộng (extension function) hoặc mapper.
  • Đảm bảo tính chính xác của việc ánh xạ Infra → Domain thông qua các bài test đọc/ghi.
✅ Ví dụ về Bulk Upsert (Cập nhật hàng loạt)
Các hàm chuyển đổi (Extensions)
// Domain sang Document
fun TestDomainItem.toDocument(): Document =
 Document()
 .append("itemSequence", itemSequence)
 .append("itemName", itemName)

// Infra sang Document
fun TestDomainItemDocument.toDocument(): Document {
 val doc = Document("item", this.item.toDocument())
 if (this.id != null) {
     doc["_id"] = this.id
 }
 return doc
}
Domain Repository (Interface)
interface TestDomainItemWriteRepository {
 suspend fun upsertAll(items: Collection<TestDomainItem>): Boolean
}

interface TestDomainItemReadOnlyRepository {
 suspend fun findAll(paginationRequest: PaginationRequest): PaginatedElements<TestDomainItem>
}
Logic xử lý Bulk Upsert
suspend fun upsertAll(items: Collection<TestDomainItem>): Boolean {
 if (items.isEmpty()) return false

 val collection = reactiveMongoTemplate.getCollection(TestDomainItemDocument.COLLECTION).awaitSingle()
 val models = items.map { item ->
     val mongoItem = item.toMongo()
     val doc = mongoItem.toDocument()
     val filter = Filters.eq("item.itemSequence", item.itemSequence)

     // Sử dụng ReplaceOneModel để thay thế toàn bộ document
     ReplaceOneModel(filter, doc, ReplaceOptions().upsert(true))
 }

 val result = collection.bulkWrite(models).awaitSingle()
 return result.modifiedCount > 0 || result.insertedCount > 0 || result.upserts.isNotEmpty()
}
Ghi chú: Vì mục đích là thay thế toàn bộ tài liệu (document), chúng tôi sử dụng ReplaceOneModel. Nếu bạn chỉ muốn cập nhật một vài thuộc tính cụ thể, hãy sử dụng UpdateOneModel với toán tử $set.
Nguồn bài viết ryukato.github.io