ข้ามไปยังเนื้อหา
แก่น Kaen
ค้นหา
Ctrl
K
ยกเลิก
เลือกธีม
มืด
สว่าง
อัตโนมัติ
เก็บ Aggregate ลงฐานข้อมูลด้วย EF Core โดยไม่ทำลาย model
เก็บ Aggregate ที่รักษากฎธุรกิจของตัวเองลง SQL โดยไม่ยอมให้ ORM บังคับให้ model กลวง — 8 บทเรียน ตั้งแต่ owned type จนถึง query ที่ไม่ N+1
เริ่มบทที่ 1
ความคืบหน้า
0 / 8 บทเรียน
เส้นทางการเรียน
หัวข้อที่มีชื่อว่า “เส้นทางการเรียน”
●
01
ทำไม persistence ยากสำหรับ model domain ที่ดี
เริ่มตรงนี้
Persistence Ignorance
หลักที่ว่า domain model (Order, OrderLine, Money) ไม่ควรรู้อะไรเลยเกี่ยวกับวิธีที่มันถูกบันทึก — ไม่มี attribute ของ EF Core ไม่สืบทอดจาก base class ใด ๆ ทุกอย่างเรื่อง mapping อยู่นอก domain ทั้งหมด
Object-Relational Impedance Mismatch
ความไม่เข้ากันโดยธรรมชาติระหว่าง model เชิงวัตถุ (Aggregate ที่ซ้อนกัน, Value Object, collection ส่วนตัว) กับ model เชิงสัมพันธ์ (ตาราง/แถว/column แบน) — สาเหตุที่การ map Order ทั้งก้อนลง SQL ไม่ตรงไปตรงมา
○
02
Strongly-typed IDs ด้วย Value Converter
Value Converter
กลไกของ EF Core ที่แปลง Value Object เช่น Money หรือ strongly-typed OrderId ไปเป็น column primitive ตอนเขียน และแปลงกลับตอนอ่าน โดยที่ domain ไม่ต้องรู้ว่าถูกแปลงเลย
Value Comparer
ตัวบอก EF Core ว่าจะ 'เทียบค่าเท่ากัน' และ snapshot ของ type ที่ไม่ใช่ primitive อย่างไร (เช่น record VO หรือ owned type ที่มี collection ภายใน) จำเป็นเมื่อ default reference equality ของ .NET ใช้ไม่ได้ ไม่งั้น change tracking จะไม่รู้ว่าค่าเปลี่ยนจริง
○
03
Owned Types & backing fields
Owned Entity Type
feature ของ EF Core ที่ map Value Object หรือ Entity ที่ไม่มี identity ของตัวเอง (Address, Money) ให้เป็นส่วนหนึ่งของ owner โดยไม่ต้องมีตารางแยก — วิธีมาตรฐานที่สุดในการเก็บ Value Object ลง SQL
Backing Field
field private ที่ EF Core เขียน/อ่านตรง ๆ (ข้าม property) เพื่อให้ Aggregate เปิดเผยแค่ IReadOnlyCollection<OrderLine> ให้ภายนอก แต่ยังยอม EF เติมข้อมูลลง List<OrderLine> ภายในได้ — รักษา encapsulation ของ root ไว้ไม่ให้ใครเพิ่ม/ลบบรรทัดผ่าน collection ตรง ๆ
○
04
Migrations ให้ปลอดภัย
Migration
file C# ที่ EF Core generate จาก diff ระหว่าง model ปัจจุบันกับ Model Snapshot ก่อนหน้า อธิบายการเปลี่ยน schema เป็น Up/Down ที่ควบคุม version ได้ และ apply กับฐานข้อมูลจริงตามลำดับ
Model Snapshot
file ที่ EF Core เก็บสถานะล่าสุดของ model หลัง migration ล่าสุดทุกครั้ง ใช้เป็นฐานเทียบตอนสร้าง migration ถัดไป — ต้อง commit คู่กับ file migration เสมอ ไม่งั้น diff ของ migration ถัดไปจะผิด
○
05
Concurrency
Optimistic Concurrency
กลยุทธ์คุมการเขียนพร้อมกันที่ 'สันนิษฐานว่าไม่ค่อยชน' — ไม่ล็อกแถวไว้ล่วงหน้า แต่ตรวจตอน SaveChanges ว่าแถวยังเป็นค่าที่อ่านมาหรือไม่ ถ้ามีคนอื่นแก้ไปก่อนแล้วจะโยน DbUpdateConcurrencyException แทนที่จะ silently เขียนทับ
Concurrency Token
column (เช่น rowversion หรือ field version) ที่ EF Core ผูกไว้กับ Optimistic Concurrency แล้วใส่ใน WHERE clause ตอน UPDATE/DELETE — ถ้าค่าไม่ตรงกับตอนอ่านมา แปลว่ามีธุรกรรมอื่นแก้ Order ไปแล้ว
○
06
Transactions & Unit of Work
Unit of Work
pattern ที่รวบรวมการเปลี่ยนแปลงหลาย object ไว้เป็นชุดเดียวแล้ว commit พร้อมกันเป็น transaction เดียว — DbContext ของ EF Core เป็น unit of work อยู่ในตัวแล้ว ไม่ต้องเขียน class ห่อซ้ำ
Transaction Boundary
ขอบเขตที่กำหนดว่างานชิ้นไหนต้องสำเร็จหรือล้มเหลวพร้อมกันทั้งหมด — ปกติคือ1 Aggregate ต่อ1 SaveChanges เดียว ข้าม Aggregate ให้ใช้ eventual consistency แทนการยัดหลาย aggregate ไว้ใน transaction เดียวกัน
Transactional Outbox
pattern ที่เขียนแถว event ลงตาราง outbox ใน transaction เดียวกับข้อมูล Aggregate ผ่าน SaveChanges ครั้งเดียว แล้วให้ worker แยกต่างหากอ่าน/ส่ง event ทีหลัง — กัน dual-write problem ระหว่างฐานข้อมูลกับ message broker
○
07
Query performance ที่ไม่ทำร้าย domain
N+1 Problem
bug ด้านประสิทธิภาพที่เกิดเมื่อ query รายการหลัก 1 ครั้ง แล้ววน loop query ข้อมูลที่เกี่ยวข้องของแต่ละแถวแยกอีกทีละครั้ง (N ครั้ง) เช่น โหลด Order มาทีละใบแล้ววน query OrderLine ของแต่ละใบซ้ำ — แก้ด้วย Include/eager loading หรือ projection เดียว
Projection
การ query โดยเลือกเฉพาะ field ที่ต้องใช้ (Select ไปเป็น DTO) แทนที่จะโหลดทั้ง Aggregate ผ่าน change tracking — เร็วกว่าและไม่ผูกกับ entity graph ทั้งก้อน เหมาะกับ use case ที่แค่ 'อ่าน' เช่นหน้ารายการออเดอร์
Read Model
โครงสร้างข้อมูลที่ออกแบบมาเพื่ออ่านโดยเฉพาะ ไม่ใช่ Aggregate เดิมที่ออกแบบมาเพื่อรักษา invariant ตอนเขียน — มักเป็นผลของ Projection หรือ view ที่ flatten หลายตารางเข้าด้วยกันเพื่อหน้าจอเดียว
Change Tracking
กลไกของ DbContext ที่จด snapshot ค่าตอน entity ถูกโหลดเข้ามา แล้วเทียบกับค่าปัจจุบันตอน SaveChanges เพื่อสร้าง UPDATE เฉพาะ column ที่เปลี่ยนจริง — มีต้นทุน จึงควรใช้ AsNoTracking() กับ query ที่ไม่ได้ตั้งใจแก้ไข
○
08
เมื่อ EF Core ไม่ใช่คำตอบ
DbContext
จุดเข้าหลักของ EF Core ที่รวม connection, change tracker และ unit of work ไว้ในตัวเดียว มี DbSet<T> ต่อ Aggregate หนึ่งตัว — อายุสั้น (scoped ต่อ request) ไม่ใช่ singleton ที่ใช้ข้าม request
Repository
ตัวที่ทำให้ domain เข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียด EF Core ไว้ทั้งหมด — implementation จริงมักเป็นแค่ wrapper บาง ๆ รอบ DbSet<Order> เพราะ DbContext เป็น unit of work อยู่แล้วในตัว
Aggregate
กลุ่มของ Entity และ Value Object ที่ถือเป็นหนึ่งหน่วยความสอดคล้อง มี root เดียวเป็นประตูเข้าและเป็นหน่วยของ transaction เช่น Order ที่คุม OrderLine — งานของบทนี้คือ map Aggregate ทั้งก้อนลง SQL โดยไม่ทำลาย invariant ที่ root รักษาไว้