Aggregate ควรใหญ่แค่ไหน — ให้ concurrency เป็นคนตอบ ไม่ใช่ความรู้สึก
สอง DbContext เปิดออเดอร์ใบเดียวกัน คนหนึ่งแก้บรรทัดแรก อีกคนแก้บรรทัดที่สอง ไม่มีบรรทัดไหนซ้ำกันสักบรรทัด
เรียก SaveChanges() ไป 2 ครั้ง ผ่าน 1 ครั้ง และได้ DbUpdateConcurrencyException 1 ครั้ง
ex.Entries ของ exception ก้อนนั้นมี 1 รายการ และ entity ที่อยู่ในนั้นคือ Order ไม่ใช่ OrderLine ที่ใครสักคนไปแก้
ตัวเลขชุดนี้ไม่ได้มาจากการนึกภาพ มันมาจาก EF Core 10.0.10 บน file SQLite 3.49.1 จริงบนดิสก์ รันด้วย .NET 10.0.10 และรันซ้ำได้ด้วยคำสั่งเดียวใน repo ของไซต์นี้
ขนาดของ aggregate ไม่ใช่รสนิยมการออกแบบ แต่เป็น การตัดสินใจเรื่อง contention ที่นับได้
concurrency token อยู่บนแถวของ aggregate root ดังนั้นทุกการแก้ที่ต้องเขียนแถวนั้นใหม่ จะแย่งกันเองไม่ว่าจะเกิดจากคนละ use case หรือคนละบรรทัดก็ตาม
คำถามที่หน้านี้ตอบจึงไม่ใช่ “aggregate ควรเล็กหรือใหญ่” แต่คือ:
“ขอบที่คุณวาดไว้ตอนนี้ อยู่ตรงไหนเมื่อเทียบกับการแก้ที่ระบบคุณทำจริง และถ้าย้ายมัน invariant ข้อไหนตายก่อน”
ทั้งสองครึ่งของคำถามตอบด้วยจำนวน ไม่ใช่ด้วยกฎ
ใบสัญญาสองใบที่ไซต์นี้ค้างไว้
หัวข้อที่มีชื่อว่า “ใบสัญญาสองใบที่ไซต์นี้ค้างไว้”ไซต์นี้สอนเรื่องขนาดของ aggregate ไปแล้วหลายที่ และสอนถูก แต่ทุกที่จบลงด้วยคำสั่งให้ไปวัด แล้วไม่มีที่ไหนวัด
ใบแรกอยู่ในคอร์ส Tactical Patterns บทที่ว่าด้วย Aggregate ซึ่งเตือนเอาไว้เองว่า “คุณยังต้องวัด round-trip และ database performance ของจริง”
ใบที่สองอยู่ใน DDD Distilled บท Aggregate ที่เขียนว่าการจะเปลี่ยนความคิดของทีม “มักต้องใช้เวลาพิสูจน์ให้เห็นว่า transaction จะล้มเหลวอย่างไร”
หน้านี้คือการทำตามสัญญาสองข้อนั้น มันไม่ได้มาเสนอข้อโต้แย้งใหม่ มันมาเป็น เครื่องมือ ที่ผลิตจำนวนให้เอาไปตัดสินใจ
หน้านี้ตอบอะไร ที่บทเรียนตอบไม่ได้
หัวข้อที่มีชื่อว่า “หน้านี้ตอบอะไร ที่บทเรียนตอบไม่ได้”สามหน้า สามคำถามคนละข้อ และหน้านี้จงใจไม่ทับกับอีกสองหน้า
- บทเรียน จัดการ concurrency ว่าด้วย กลไกที่รายงาน conflict — token ถูกเทียบตอนไหน 0 แถวแปลว่าอะไร แล้วจะกู้อย่างไร
- บทเรียน Aggregate กับการรักษา invariant ว่าด้วย กฎที่บอกให้ทำให้เล็ก และเหตุผลเชิงการออกแบบของกฎนั้น
- หน้านี้ว่าด้วย ตำแหน่งที่ขอบของคุณอยู่ตอนนี้ เทียบกับการแก้ที่ระบบคุณทำจริง
ถ้าคุณเรียนบทเรียนสองบทนั้นมาแล้ว สองฉากแรกของหน้านี้จะไม่ทำให้แปลกใจ ส่วนฉากที่สามควรทำ เพราะมันบอกเงื่อนไขที่บทเรียนเหล่านั้นไม่ได้บอก
สิ่งที่คุณยังไม่เคยเห็นคือ จำนวน และจำนวนของขอบอีกแบบวางเทียบข้าง ๆ กัน
นับอะไรอยู่
หัวข้อที่มีชื่อว่า “นับอะไรอยู่”หน้าที่รายงานตัวเลขโดยไม่บอกว่านับอะไร คือหน้าที่เอาไปอ้างต่อไม่ได้
| ชื่อในหน้านี้ | นับอะไรจริง ๆ |
|---|---|
| conflict | จำนวนครั้งที่ SaveChanges() โยน DbUpdateConcurrencyException |
| entries | ex.Entries.Count ของ exception ก้อนนั้น |
| entity ที่โหลด | จำนวน entity ที่ถูก materialise เข้า ChangeTracker |
| command ที่ส่ง | จำนวน DbCommand ที่ถูกส่งออกไปจริงระหว่างโหลด นับด้วย DbCommandInterceptor โดย connection ถูกเปิดไว้ก่อนเริ่มนับแล้ว |
| แถวที่ invariant พัง | จำนวนแถว Order ที่ Total ไม่ตรงกับผลรวมของ OrderLine ในแถวนั้น |
ตาราง 1 — หน่วยนับทั้งห้าของหน้านี้ ทุกตัวเป็นจำนวนของสิ่งที่นับได้ ไม่มีตัวไหนเป็นตัวเลขเวลา · วัดบน SQLite 3.49.1 · .NET 10.0.10 · EF Core 10.0.10
ฉากแรก — ขอบอ้วน สอง context แก้คนละบรรทัด
หัวข้อที่มีชื่อว่า “ฉากแรก — ขอบอ้วน สอง context แก้คนละบรรทัด”ตั้ง
Orderเป็น aggregate root เดียว วาง concurrency token ไว้บนราก แล้วให้สองฝั่งแก้คนละบรรทัด
ออเดอร์ที่ใช้ในฉากนี้มี 12 บรรทัด OrderLine เป็นลูกที่ไม่มี token ของตัวเอง ตามรูปทรงที่ตำราสอนกันมาตรงตัว
ทั้งสองฝั่งโหลดออเดอร์ใบเดียวกันพร้อมบรรทัดทั้งหมด แก้กันคนละ OrderLineId แล้วต่างคนต่าง save
sequenceDiagram
participant A as context A
participant DB as file SQLite
participant B as context B
A->>DB: โหลด Order ใบเดียวกัน ได้ Version = 1
B->>DB: โหลด Order ใบเดียวกัน ได้ Version = 1
A->>DB: แก้บรรทัดแรก แล้ว SaveChanges
DB-->>A: UPDATE Orders WHERE Version = 1 โดน 1 แถว save ผ่าน
B->>DB: แก้บรรทัดที่สอง แล้ว SaveChanges
DB-->>B: UPDATE Orders WHERE Version = 1 โดน 0 แถว จึงโยน DbUpdateConcurrencyException
ภาพ 1 — ของที่ถูกเทียบไม่ใช่บรรทัดที่ใครแก้ แต่เป็นแถวของราก · ฝั่งที่ save ทีหลังจึงถูกปฏิเสธ ทั้งที่ไม่มีใครแตะของชิ้นเดียวกันเลย · วัดบน SQLite 3.49.1 · .NET 10.0.10 · EF Core 10.0.10
| ตัวนับ | ค่าที่ได้ |
|---|---|
SaveChanges() ที่เรียก | 2 |
| save ที่ผ่าน | 1 |
| conflict | 1 |
รายการใน ex.Entries | 1 |
ชนิดของ entity ใน ex.Entries | Order |
ตาราง 2 — ฉากขอบอ้วน สอง context แก้คนละบรรทัดของออเดอร์ 12 บรรทัด · วัดบน SQLite 3.49.1 · .NET 10.0.10 · EF Core 10.0.10
สี่แถวแรกของตารางนี้ถูกกำหนดโดยการออกแบบฉาก ไม่ใช่โดยผลการวัด — ฉากมีสอง context ก็ต้องได้ save 2 ครั้ง แถวสุดท้ายต่างหากที่เป็นสิ่งที่เครื่องเป็นคนตอบ: ชนิดของ entity ใน ex.Entries คือ Order ทั้งที่ไม่มีใครสั่งแก้ Order เลยสักคน
ex.Entries มีกี่รายการ
หัวข้อที่มีชื่อว่า “ex.Entries มีกี่รายการ”จำนวนนี้คือประโยคที่สั้นที่สุดที่บอกว่า token อยู่ตรงไหน
บทเรียน EF Core ใช้ ex.Entries.Single() แล้วอ่านค่าจากตัวเดียวนั้น แต่ไม่ได้บอกว่าทำไมมันถึงมีตัวเดียว
คำตอบคือ exception รายงาน แถวที่ถูกเทียบแล้วไม่โดน ไม่ใช่ของที่คุณแก้ และแถวที่ถูกเทียบมีแถวเดียวเสมอ คือแถวของราก
รันซ้ำโดยเปลี่ยนจำนวนบรรทัดลูกที่ฝั่งที่แพ้แตะ ตั้งแต่ 1 ถึง 10 บรรทัด แล้วตัวเลขไม่ขยับ
| บรรทัดลูกที่แตะ | conflict | รายการใน ex.Entries | ชนิดของ entity |
|---|---|---|---|
| 1 | 1 | 1 | Order |
| 2 | 1 | 1 | Order |
| 5 | 1 | 1 | Order |
| 10 | 1 | 1 | Order |
ตาราง 3 — แตะลูกกี่ตัวก็ตาม exception ยังชี้ไปที่รากตัวเดียว · วัดบน SQLite 3.49.1 · .NET 10.0.10 · EF Core 10.0.10
ประโยคที่เอาไปใช้ได้เลยคือ exception บอกชื่อ entity ที่ถือ token ไม่ใช่ชื่อของที่คุณแก้
conflict ที่ไม่เกิด — เงื่อนไขที่ทำให้ฉากแรกเป็นจริง
หัวข้อที่มีชื่อว่า “conflict ที่ไม่เกิด — เงื่อนไขที่ทำให้ฉากแรกเป็นจริง”ถ้าอ่านฉากแรกว่า “ขอบอ้วนทำให้การแก้ลูกที่ไม่เกี่ยวกันชนกันเสมอ” จะผิด และตัววัดเดียวกันนี้เป็นคนบอกว่าผิด
รันซ้ำอีกรอบด้วย mapping ชุดเดิม ออเดอร์ใบเดิม สอง context เหมือนเดิม แก้คนละบรรทัดเหมือนเดิม เปลี่ยนแค่ ชนิดของการแก้
รอบนี้เปลี่ยนแค่ข้อความกำกับบรรทัด ซึ่งไม่มี invariant ข้อไหนของรากผูกอยู่ด้วย รากจึงไม่ถูกเขียนใหม่
ผลคือ conflict 0 ครั้ง และ save ผ่านทั้ง 2 ครั้ง
EF Core เทียบ token เฉพาะบนแถวที่มันสั่ง UPDATE จริงเท่านั้น เมื่อไม่มี UPDATE บนแถวของราก ก็ไม่มีอะไรถูกเทียบ และไม่มีใครถูกรายงาน
conflict ในฉากแรกเกิดขึ้น เพราะ invariant ข้อ ② บังคับให้รากถูกเขียนใหม่ — Order.ChangeQuantity(...) คำนวณ Total ใหม่ทุกครั้งตามที่บทเรียน Aggregate กับการรักษา invariant กำหนด
contention กับ invariant จึงไม่ใช่สองเรื่อง มันคือเรื่องเดียวกันที่มองจากคนละด้าน
การแก้ที่ไม่แตะกฎของราก ก็ไม่แย่งอะไรกับใครแม้ aggregate จะอ้วนแค่ไหน ส่วนการแก้ที่แตะกฎของราก จะแย่งกันเสมอแม้จะอยู่คนละบรรทัด
จำนวนชุดนี้คือจุดที่หน้านี้แก้บทเรียนของไซต์เอง บทเรียน จัดการ concurrency เขียนว่า token ตัวเดียวบนรากคุ้มครองทั้ง graph เพราะรากจะแก้ column ของตัวเองเสมอ — 0 conflict กับ 2 save ที่ผ่านในฉากนี้บอกเงื่อนไขที่ประโยคนั้นเป็นจริง คือเมื่อการแก้บังคับให้ column ของรากเปลี่ยน ไม่ใช่ทุกการแก้ที่อยู่ในขอบ
เส้นแบ่งนี้สำคัญ เพราะตำราให้คุณ เลือก ว่าจะให้รากขยับ version ตามการแก้ลูกหรือไม่ แต่ EF Core ตัดสินให้จากสิ่งที่ column เปลี่ยนจริง โดยไม่ถามว่าคุณตั้งใจแบบไหน คนที่วาง token ไว้บนรากตามกฎ จึงไม่ได้ความคุ้มครองทั้ง aggregate อย่างที่คิดว่าซื้อมา
ขยับขอบ แล้วนับใหม่
หัวข้อที่มีชื่อว่า “ขยับขอบ แล้วนับใหม่”ให้
OrderLineเป็น root ของตัวเอง ถือ token ของตัวเอง แล้วรันการแก้ชุดเดิมซ้ำ
การแก้ชุดเดิมเป๊ะ ๆ ที่เคยได้ conflict 1 ครั้ง คราวนี้เรียก save ไป 2 ครั้ง ผ่าน 2 ครั้ง และได้ conflict 0 ครั้ง
ถ้าหน้านี้จบตรงนี้ มันก็คือใบปลิว จึงมีฉากคุมอีกฉากที่ห้ามตัดทิ้ง คือให้สองฝั่งแก้ บรรทัดเดียวกัน
| ฉากบนขอบที่แยกแล้ว | save ที่ผ่าน | conflict | รายการใน ex.Entries |
|---|---|---|---|
| แก้คนละบรรทัด | 2 | 0 | — |
| แก้บรรทัดเดียวกัน | 1 | 1 | 1 |
ตาราง 4 — ฉากคุมบรรทัดที่สองรายงาน entity ชื่อ OrderLine ไม่ใช่ Order · วัดบน SQLite 3.49.1 · .NET 10.0.10 · EF Core 10.0.10
ข้อสรุปที่ตัวเลขรองรับจึงไม่ใช่ “แยกแล้ว conflict หายไป” แต่เป็น conflict ย้ายที่ไปอยู่บนของที่ชนกันจริง
การชนที่เหลืออยู่คือการชนที่ธุรกิจต้องการให้ชน ส่วนการชนที่หายไปคือผลข้างเคียงของขอบ ไม่ใช่กฎของธุรกิจ
ราคาของการแยก — invariant ② ตายต่อหน้า
หัวข้อที่มีชื่อว่า “ราคาของการแยก — invariant ② ตายต่อหน้า”ราคาของการย้ายขอบไม่ได้จ่ายเป็นความยุ่งยากของ code แต่จ่ายเป็นกฎธุรกิจที่หายไปหนึ่งข้อ
Order ในคอร์ส DDD ใน code จริง ถือ invariant ไว้สี่ข้อ ข้อ ② คือ Total ต้องตรงกับผลรวมของทุกบรรทัดเสมอ
ฉากนี้แก้ Quantity หนึ่งครั้งบนขอบทั้งสองแบบ แล้วอ่านกลับจาก file SQLite เพื่อนับแถวที่ Total ไม่ตรงกับผลรวมของบรรทัด
| รูปทรงของขอบ | แถวที่ Total ไม่ตรง |
|---|---|
ราก Order ถือทั้งกอง | 0 |
OrderLine เป็น root ของตัวเอง | 1 |
ตาราง 5 — การแก้จำนวนหนึ่งครั้ง บนขอบสองแบบ นับจากค่าที่อ่านกลับจาก file · วัดบน SQLite 3.49.1 · .NET 10.0.10 · EF Core 10.0.10
บนรูปทรงที่แยกแล้ว Order.Total ไม่มีใครแตะ เพราะ Order เป็นคนละ aggregate และไม่มีใครเรียกมัน
ทางกู้มีสองทาง และทั้งสองทางมีราคา
- เขียนสอง root ให้จบ — ตัววัดเดินทางนี้จริง ต้องแตะ 2 root และเรียก save 2 ครั้งตามกฎ 1 aggregate ต่อ 1 transaction หลังกู้เสร็จเหลือแถวที่พัง 0 แถว
- ยอมรับหน้าต่างที่
Totalยังไม่ตรง แล้วปิดทีหลังด้วย domain event ตามแนวทาง eventual consistency
ทางแรกดึงทั้งสอง root กลับมาอยู่ในการตัดสินใจเดียวกันอีกรอบ ซึ่งก็คือการเอา contention ที่เพิ่งย้ายออกไปกลับเข้ามา
ทางที่สองไม่ได้แพงที่ code แต่แพงที่สัญญา เพราะมันเปลี่ยนคำตอบของคำถามว่า “ระบบยอมให้ Total ผิดได้นานแค่ไหน” ซึ่งเป็นคำถามของธุรกิจ ไม่ใช่ของทีมพัฒนา
นี่คือจุดที่การย้ายขอบเลิกเป็นเรื่องเทคนิค
จำนวนแถวที่โหลด ต่อรูปทรงการโหลด
หัวข้อที่มีชื่อว่า “จำนวนแถวที่โหลด ต่อรูปทรงการโหลด”ตารางนี้มีไว้เพื่อกันการสรุปว่า “แยกแล้วโหลดน้อยลง” ซึ่งจริงแค่กับ use case แบบเดียว
รูปทรงที่วัดมีสามแบบ วัดที่ออเดอร์สี่ขนาด — 1, 10, 100 และ 1000 บรรทัด
fat-full— โหลดOrderพร้อมบรรทัดทั้งหมด เพื่อแก้บรรทัดเดียวsplit-line-only— โหลดเฉพาะOrderLineที่จะแก้split-whole-order— โหลดทั้งใบบนรูปทรงที่แยกแล้ว สำหรับ use case ที่ต้องการทั้งใบจริง ๆ
| รูปทรง | บรรทัดในออเดอร์ | entity ที่โหลด | command ที่ส่ง |
|---|---|---|---|
fat-full | 1 | 2 | 1 |
fat-full | 10 | 11 | 1 |
fat-full | 100 | 101 | 1 |
fat-full | 1000 | 1001 | 1 |
split-line-only | 1 | 1 | 1 |
split-line-only | 10 | 1 | 1 |
split-line-only | 100 | 1 | 1 |
split-line-only | 1000 | 1 | 1 |
split-whole-order | 1 | 2 | 2 |
split-whole-order | 10 | 11 | 2 |
split-whole-order | 100 | 101 | 2 |
split-whole-order | 1000 | 1001 | 2 |
ตาราง 6 — entity ที่ถูก materialise เข้า ChangeTracker และจำนวน DbCommand ที่ถูกส่งจริง · วัดบน SQLite 3.49.1 · .NET 10.0.10 · EF Core 10.0.10
สามอย่างที่ตารางนี้บอก และไม่มีอันไหนเป็นข่าวดีล้วน ๆ
split-line-onlyโหลด 1 entity ในทุกขนาด เพราะมันไม่ต้องรู้จักบรรทัดอื่นเลยsplit-whole-orderโหลด 1001 entity ที่ 1000 บรรทัด ซึ่งเป็นจำนวนเดียวกับfat-fullแต่ส่ง command 2 ครั้งแทนที่จะเป็น 1 ครั้ง เพราะไม่มี navigation ให้ EF join ให้ ตัววัดจึงถามสองคำถามแยกกัน (เขียน join ข้ามDbSetเองก็ได้ แต่ต้องเขียนเอง)fat-fullส่ง command 1 ครั้งในทุกขนาด — EF Core ไม่ได้เลือกแตก query ในฉากนี้
ข้อสรุปที่ซื่อสัตย์คือ คุณจ่ายตามรูปทรงที่ use case ส่วนใหญ่ของคุณต้องการ ไม่ใช่ตามขนาดของ aggregate
นี่คือจุดที่ตารางนี้แก้อีกประโยคหนึ่งของไซต์เอง บทเรียน Aggregate เขียนว่า aggregate ที่เล็กกว่าถ่ายโอนข้อมูลน้อยลง — จริงเฉพาะกับ split-line-only เท่านั้น ส่วน split-whole-order ซึ่งเป็นขอบที่เล็กกว่าเหมือนกัน โหลด entity เท่ากับ fat-full ทุกขนาด และส่ง command มากกว่าด้วยซ้ำ ขนาดของขอบจึงไม่ได้กำหนดปริมาณที่ถ่ายโอน รูปทรงของการอ่านต่างหากที่กำหนด
อีกจำนวนหนึ่งที่ควรรู้ก่อนตัดสินใจ คือของที่ฝั่งแพ้จ่ายไปแล้ว — บนออเดอร์ 1000 บรรทัด context ฝั่งที่แพ้ materialise ไว้แล้ว 1001 entity ซึ่งยังค้างอยู่ใน ChangeTracker หลัง exception ถ้าเลือกทาง reload + retry ตาม บทเรียนจัดการ concurrency คุณใช้มันต่อได้ ถ้าไม่ ทั้งกองนั้นคือของที่จ่ายไปฟรี
เส้นแบ่งเจ้าของพฤติกรรม
หัวข้อที่มีชื่อว่า “เส้นแบ่งเจ้าของพฤติกรรม”ก่อนเอาตัวเลขในหน้านี้ไปอ้างกับ provider ของคุณ ให้ดูก่อนว่าข้อไหนเป็นของใคร
จริงกับทุก provider ที่ EF Core รองรับ
หัวข้อที่มีชื่อว่า “จริงกับทุก provider ที่ EF Core รองรับ”สามข้อนี้เป็นพฤติกรรมของชั้น optimistic concurrency ที่ EF Core เป็นเจ้าของเอง จึงเหมือนกันข้าม provider
- token ถูกเทียบตอน
SaveChangesในรูปWHEREของคำสั่งUPDATEไม่ใช่ตอนโหลด UPDATEที่โดน 0 แถว กลายเป็นDbUpdateConcurrencyExceptionบน relational provider ทุกตัว เพราะ EF Core นับแถวที่ได้รับผลกระทบแล้วเทียบกับที่คาดไว้ex.Entriesชี้ไปที่ entity ที่ถือ token ไม่ใช่ entity ที่คุณแก้
เป็นของ SQLite
หัวข้อที่มีชื่อว่า “เป็นของ SQLite”- ไม่มี
rowversionและไม่มีxminตัววัดนี้จึงประกาศ property ของตัวเองแล้วมาร์กด้วยIsConcurrencyToken()และเพิ่มค่าเองใน code ตามทางที่ Microsoft Learn ระบุไว้สำหรับฐานข้อมูลที่ไม่มี token อัตโนมัติ - หมายเลขรุ่นของเอนจิน ที่กำกับทุกตารางในหน้านี้เป็นของเครื่องที่รัน ไม่ใช่ค่าคงที่ของบทความ
ส่วนพฤติกรรมการล็อกของแต่ละเอนจิน หน้านี้ ไม่อ้างอะไรเลย ทั้ง row lock, isolation level และ MVCC อยู่นอกสิ่งที่ตัววัดนี้แตะ
ตัวเลขทุกตัวข้างบนมาจาก harness ที่อยู่ใน repo ของไซต์นี้ที่ scripts/aggregate-size/ และรันซ้ำได้ด้วย npm run gen:aggregate-size
provider คือ file SQLite 3.49.1 จริงบนดิสก์ ไม่ใช่ in-memory provider เพราะ in-memory provider ไม่บังคับ transaction จริง ทั้งฉากแรกและฉาก invariant จะไม่มีความหมายบนมัน
ข้อมูลตั้งต้นไม่มีการสุ่ม รันกี่รอบก็ได้ผลชุดเดิม และมีเทสที่รัน harness ซ้ำแล้วเทียบ file ข้อมูลทีละ byte
ยกเว้นสามบรรทัดที่เป็นป้ายของเครื่อง — SQLite 3.49.1 .NET 10.0.10 EF Core 10.0.10 — ซึ่งถูกยกเว้นจากการเทียบทีละ byte แล้วตรวจแยกว่ารูปร่างถูก เพราะเครื่องที่รันต่างกันย่อมรายงานรุ่นต่างกัน และนั่นไม่ใช่ข้อมูล
ถ้าใครแก้ตัวเลขในหน้านี้ด้วยมือ เทสจะแดงทันที
ทำไมหน้านี้ไม่มีตัวเลขเวลา
หัวข้อที่มีชื่อว่า “ทำไมหน้านี้ไม่มีตัวเลขเวลา”เพราะสิ่งที่ขอบเป็นเจ้าของคือ conflict จะเกิดหรือไม่เกิด ไม่ใช่ว่ามันเกิดถี่แค่ไหน
conflict จะเกิดหรือไม่ เป็นคุณสมบัติของ ขอบที่คุณวาด เทียบกับ การแก้ที่ระบบคุณทำ สองอย่างนี้ตัววัดนี้ยืนยันได้
ส่วนจะเกิดถี่แค่ไหนเป็นคุณสมบัติของ traffic ของคุณ ซึ่งขึ้นกับจำนวนผู้ใช้พร้อมกัน รูปแบบการใช้งาน และเครื่องที่รัน ตัววัดนี้ยืนยันไม่ได้ และไม่ควรแกล้งยืนยัน
ไซต์นี้เคยเผยแพร่ตัวเลขที่ขึ้นกับเครื่องมาแล้วหนึ่งครั้ง และต้องแก้ทีหลัง กติกาตั้งแต่นั้นคือ ยกมาอ้างได้เฉพาะจำนวนของสิ่งที่นับได้
ตัวเลขของระบบคุณ ให้วัดบนเครื่องคุณ ด้วย traffic ของคุณ แล้วติดป้ายกำกับไว้ว่าวัดที่ไหนเมื่อไร
เอาไปใช้กับระบบของคุณยังไง
หัวข้อที่มีชื่อว่า “เอาไปใช้กับระบบของคุณยังไง”สี่ข้อนี้ตอบได้ด้วยของที่คุณมีอยู่แล้ว ไม่ต้องรื้อ model ก่อน
- หา entity ที่ถือ token ให้เจอก่อน ไล่ดูว่ามี
IsConcurrencyToken(),IsRowVersion()หรือ token ของ provider อยู่บน entity ตัวไหน นั่นคือหน่วยของ contention จริงของคุณ ไม่ใช่ตัวที่ผังในหัวบอก - เขียนรายการการแก้ที่ระบบคุณทำจริง แล้วขีดว่าแต่ละอันต้องเขียนแถวของรากใหม่หรือไม่ — อันที่ต้องเขียน จะแย่งกันเองแม้จะเป็นคนละ use case
- เปิด
ex.Entriesตอนเจอ conflict แล้วอ่านชื่อ entity ถ้าชื่อที่ได้ไม่ใช่ของที่ผู้ใช้กำลังแก้ แปลว่าขอบของคุณกว้างกว่าเหตุผลทางธุรกิจที่รองรับมัน - ก่อนจะแยก ให้ถามว่า invariant ข้อไหนจะตาย แล้วถามต่อว่าธุรกิจยอมให้มันผิดชั่วคราวได้นานแค่ไหน ถ้าคำตอบคือไม่ยอมเลย ขอบนั้นห้ามแยก และ conflict ที่เจอคือ conflict ที่ถูกต้อง
ข้อ 4 คือข้อที่ตัดสินจริง ๆ เพราะสามข้อแรกให้จำนวน แต่ข้อ 4 ให้คำถามที่เจ้าของธุรกิจต้องเป็นคนตอบ
- Vaughn Vernon — Effective Aggregate Design, Part I: Modeling a Single Aggregate (2011) — PDF ที่มาของกฎ “ทำให้เล็ก” ที่หน้านี้เอามาวัด
- Vaughn Vernon — Effective Aggregate Design, Part II: Making Aggregates Work Together (2011) — PDF การอ้างอิงด้วย id และ eventual consistency ข้าม aggregate
- Vaughn Vernon — Effective Aggregate Design, Part III: Gaining Insight Through Discovery (2011) — PDF และ หน้ารวมของชุดบทความ
- Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003) — นิยามของ aggregate ในฐานะ consistency boundary (หนังสือ ไม่มีฉบับออนไลน์ทางการ)
- Eric Evans — Domain-Driven Design Reference (ฉบับ 2015-03) — PDF
- Microsoft Learn — Handling Concurrency Conflicts — EF Core (เผยแพร่ 2022-10-19 ปรับปรุงล่าสุด 2025-10-30) — เอกสาร แหล่งหลักของเส้นแบ่งในหน้านี้ และเป็นที่มาของทาง
IsConcurrencyToken()ที่ harness ใช้บน SQLite - Microsoft Learn —
DbUpdateConcurrencyException— นิยามของEntriesที่หน้านี้รายงานเป็นจำนวน
อ่านต่อ
- จัดการ concurrency — กลไกเต็มของ token, reload + retry, การตอบ 409 และ lost update
- transaction กับ unit of work — กฎ 1 aggregate ต่อ 1 transaction ที่ฉาก invariant พาดพิงถึง
- Aggregate กับการรักษา invariant — invariant สี่ข้อของ
Orderใบเดียวกับที่หน้านี้ใช้ - Aggregate ในคอร์ส Tactical Patterns และ DDD Distilled — ที่มาของกฎการออกแบบ และขั้นตอน right-sizing ที่หน้านี้เป็นเครื่องมือป้อนให้
- Aggregate ในคลังคำ DevIQ — นิยามสั้นสำหรับอ้างอิงเร็ว
- ทำไม index ที่คุณเพิ่งเพิ่มถึงไม่ถูกใช้ — อีกหน้าที่ตอบด้วยผลจากเครื่อง ไม่ใช่ด้วยกฎที่ท่องกันมา