ข้าม​ไป​ยัง​เนื้อหา

บรรทัด​เดียว​ใน​ฝั่ง​เทสต์ ทำให้​ด่าน​กลับ​มา​เขียว โดย​ของ​ที่ deploy ยัง​รั่ว​เท่า​เดิม

บท​ที่ 1 ทิ้ง​เรื่อง​หนึ่ง​ค้าง​ไว้ ตอน​นั้น​เรา​วัด​คำ​ตอบ​กับ​จำนวน​แถว แต่​ยัง​ไม่​ได้​พูด​ถึง​ด่าน​ที่​คอร์ส​ก่อน​หน้า​ทิ้ง​ไว้​ให้

บท​นี้​เล่า​เรื่อง​นั้น​ให้​จบ ตั้งแต่​ตอน​ที่​ด่าน​แดง จนถึง​บรรทัด​ที่​คน​แนะนำ​กัน​ทั่วไป ซึ่ง​ทำให้​มัน​กลับ​มา​เขียว​โดยที่​รู​ยัง​อยู่​ครบ

ทรง cached ทำให้ IsolationTest แดง ไม่ใช่​เขียว

IsolationTest คือ​ของ​ที่ คอร์ส​คัด​กรอง​ผู้​เรียก​ของ​คลินิก ส่ง​มอบ​มา มัน​มี​ข้อ​อ้าง​เดียว คือ​ผู้​เรียก​เห็น​แถว​ของ​ผู้​เช่า​ตัว​เอง​เท่านั้น

เทสต์​ที่​คอร์ส​ที่ 35 ทิ้ง​ไว้ · assert สอง​บรรทัด
@Test
fun `ผู้เรียกเห็นแถวของผู้เช่าตัวเองเท่านั้น`() {
assertEquals("{\"count\":2,\"patients\":[\"พิมพ์ใจ\",\"สมชาย\"]}", body("kaen-a"))
assertEquals("{\"count\":1,\"patients\":[\"วีระ\"]}", body("kaen-b"))
}

บรรทัด​ที่​สอง​คือ​บรรทัด​ที่​แดง ผู้​เรียก​ของ clinic-b ควร​ได้ วีระ แต่​ได้ พิมพ์ใจ กับ สมชาย ซึ่ง​เป็น​สอง​แถว​ของ clinic-a

นี่​คือ​ข่าวดี​ข้อ​เดียว​ของ​บท​ที่ 1 ด่าน​ที่​มี​อยู่​แล้ว​จับ​รู​นี้​ได้​เอง โดย​ไม่​ต้อง​มี​ใคร​เขียน​อะไร​เพิ่ม

application.properties ของ​เทสต์​บัง​ของ main ทั้ง​ใบ ไม่ใช่​ทับ​ที​ละ​บรรทัด

classpath ของ​เทสต์​มา​ก่อน และ Spring หยิบ​ใบ​แรก​ที่​เจอ​เพียง​ใบ​เดียว เขียน​บรรทัด​เดียว​ลง​ไป​ที่​นั่น แล้ว​ค่า​ของ datasource หาย​ทั้ง​ชุด

❌ version ดิบ · app/src/test/resources/application.properties
spring.cache.type=none

แอป​ตาย​ด้วย​ข้อความ Failed to determine a suitable driver class ใน​ข้อความ​นั้น​ไม่มี​คำ​ว่า cache สัก​คำ คน​อ่าน​จึง​ไป​ไล่​หา​สาเหตุ​ผิด​ที่

ของ​จริง​คือ​คัด​ทุก​บรรทัด​ของ main มา​ก่อน แล้ว​ค่อย​เติม​บรรทัด​เดียว​ต่อ​ท้าย

app/src/test/resources/application.properties · ของ main ครบ​ทุก​บรรทัด บวก​บรรทัด​สุดท้าย
spring.datasource.url=jdbc:postgresql://127.0.0.1:55436/clinic
spring.datasource.username=postgres
spring.datasource.password=clinic
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.generate_statistics=true
spring.main.banner-mode=off
spring.cache.type=none

spring.cache.typespring.cache.typeคีย์​ใน config ที่​เลือก​ว่า​จะ​ใช้ CacheManager ตัว​ไหน · ค่า none ให้ NoOpCacheManager · หนึ่ง​บรรทัด​นี้​ใน​ฝั่ง​เทสต์ ตัดสิน​ว่า​เทสต์​ของ​คอร์ส​ที่ 35 แดง​หรือ​เขียว เลือก​ว่า​จะ​ได้ CacheManager ตัว​ไหน และ​ค่า none แปล​ว่า​ไม่​เก็บ​อะไร​เลย

@Cacheable("visits") ยัง​ติด​อยู่​ที่​เดิม @EnableCaching ยัง​อยู่​ที่​เดิม starter ยัง​อยู่​ใน build file ตัว​เดิม code ของ​แอป​ไม่​ขยับ​สัก​ตัว​อักษร

สิ่ง​ที่​ขยับ​คือ bean ที่ Spring ประกอบ​ให้​ตอน​เทสต์​รัน มัน​กลาย​เป็น NoOpCacheManagerNoOpCacheManagerCacheManager ที่​รับ​ค่า​ไว้​แล้ว​ไม่​เก็บ​อะไร​เลย · ทุก​คำ​เรียก​จึง​ลง​ไป​ถึง​ของ​จริง และ​เทสต์​ที่​รัน​บน​มัน​เขียว โดย​ไม่​ได้​ทดสอบ​ชั้น​ที่​จำ​คำ​ตอบ​สัก​ครั้ง ซึ่ง​รับ​ค่า​ไว้​แล้ว​ไม่​เก็บ ทุก​คำ​เรียก​จึง​ลง​ไป​ถึง​ฐาน​ข้อมูล​ของ​จริง

ช่อง​ที่​วัด​เปลี่ยน​เกือบ​ทุก​ช่อง ช่อง​เดียว​ที่​ไม่​เปลี่ยน​คือ code ของ​แอป

ช่อง​ที่​วัดทรง cachedทรง cached-test-off
code ของ​แอป@Cacheable("visits")@Cacheable("visits")
cacheManagerConcurrentMapCacheManagerNoOpCacheManager
กุญแจ​ใน cache[SimpleKey []]<NoOpCache>
clinic-b เห็นพิมพ์​ใจ, สมชาย · 200วีระ
ฐาน​ข้อมูล​ถูก​ถาม1 ครั้ง3 ครั้ง
IsolationTestแดงเขียว

ช่อง​กุญแจ​อ่าน​ไม่​เหมือน​ทรง​อื่น​เพราะ​ไม่มี​อะไร​ให้​อ่าน NoOpCache ไม่​ได้​เก็บ​ค่า​ไว้​ใน​ที่​ที่​นับ​กุญแจ​ได้ ตัวเลข 3 ใน​ช่อง​ฐาน​ข้อมูล​ถูก​ถาม จึง​เท่ากับ​ทรง scoped ของ​บท​ที่ 1 พอดี

ด่าน​เขียว​เพราะ​มัน​เลิก​ทดสอบ​ทรง​ที่ deploy จริง

ทรง​ที่​ขึ้น production ไม่มี​บรรทัด​นั้น มัน​จึง​ยัง​ได้ ConcurrentMapCacheManager ยัง​ได้​กุญแจ​ใบ​เดียว​ทั้ง​แอป และ​ยัง​คืน​สอง​แถว​ของ clinic-a ให้ clinic-b พร้อม​รหัส 200

flowchart TB
  C["@Cacheable ตัวเดียวกัน · code ของแอปใบเดียวกัน"] --> T["ทรงที่เทสต์รัน · properties ของเทสต์มี spring.cache.type=none"]
  C --> D["ทรงที่ deploy · properties ของ main ไม่มีบรรทัดนั้น"]
  T --> T1["cacheManager คือ NoOpCacheManager"]
  T1 --> T2["ทรงนี้ถูกถามฐานข้อมูล 3 ครั้งจากสามคำขอ"]
  T2 --> T3["clinic-b ได้ วีระ · IsolationTest เขียว"]
  D --> D1["cacheManager คือ ConcurrentMapCacheManager"]
  D1 --> D2["ฐานข้อมูลถูกถาม 1 ครั้งจากสามคำขอ"]
  D2 --> D3["clinic-b ได้ พิมพ์ใจ กับ สมชาย · 200"]

คำ​บรรยาย​ภาพ: code ของ​แอป​ใบ​เดียวกัน​แตก​ออก​เป็น​สอง​ทรง เพราะ properties คนละ​ใบ ทรง cached-test-off คือ​ทรง​ที่​ด่าน​ตรวจ ส่วน​ทรง cached คือ​ทรง​ที่​ผู้​ใช้​จริง​เรียก

นี่​คือ​ด่าน​ที่​ตรวจ​ของ​ที่​ไม่มี​ใคร​ใช้ ผล​สี​เขียว​ยัง​อ่าน​เหมือน​เดิม​ทุก​ตัว​อักษร คน​ที่​เห็น​แต่​สี​จะ​ไม่มี​ทาง​รู้​ว่า​สิ่ง​ที่​ถูก​ตรวจ​เปลี่ยน​ไป​แล้ว

เอกสาร​ไม่​ได้​ผิด สิ่ง​ที่​ผิด​คือ​วิธี​ที่​เรา​หยิบ​มา​ใช้

หัวข้อ​ที่​มีชื่อ​ว่า “เอกสาร​ไม่​ได้​ผิด สิ่ง​ที่​ผิด​คือ​วิธี​ที่​เรา​หยิบ​มา​ใช้”

spring.cache.type=none มี​ที่ทาง​ของ​มัน แต่​ที่ทาง​นั้น​ไม่ใช่​การ​ปิด​ทั้ง​ชุด​เทสต์

เอกสาร​ของ Spring Boot อธิบาย​ค่า​นี้​ว่า​ให้ no-op cache สำหรับ​บาง​สภาพ​แวดล้อม เช่น​ตอน​ที่​เรา​อยาก​ปิด​กลไก​นี้​ทิ้ง​ใน​สภาพ​แวดล้อม​เดียว​โดย​ไม่​ถอด annotation ออก​ทั้ง​ชุด

ปัญหา​จึง​ไม่​ได้​อยู่​ที่​คีย์ ปัญหา​อยู่​ที่​การ​เอา​สวิตช์​ระดับ​สภาพ​แวดล้อม มา​ปิด​สภาพ​แวดล้อม​เดียว​ที่​ทำ​หน้าที่​ตรวจ​สอบ

ถ้า​คุณ​เลือก​ใช้​มัน ให้​ตอบ​คำถาม​ข้อ​เดียว​ให้​ได้​ก่อน คือ​หลัง​จาก​นี้ ยัง​มี​ชุด​เทสต์​ไหน​เหลือ​อยู่​ที่​รัน​บน​ทรง​เดียว​กับ​ที่ deploy

ให้​ชุด​เทสต์​ที่​ยิง​สอง​ผู้​เช่า​เปิด cache ไว้​ตาม​ทรง​จริง แล้ว​ยอมรับ​ราคา​ที่​ตาม​มา

สิ่ง​ที่​ต้อง​ทำ​มี​สาม​ข้อ

  • ปล่อย​ให้​ชุด​เทสต์​ที่​ยิง clinic-a แล้ว​ต่อ​ด้วย clinic-b รัน​บน​ทรง​ที่​มี CacheManager ของ​จริง
  • แยก test class ที่​บันทึก​ค่า​ออก​จาก test class ที่ assert เพราะ​คำขอ​ของ​ใบ​หนึ่ง​ไป​เติม cache ให้​อีก​ใบ​ได้ · ข้อ​นี้​มา​จาก​คอม​เมน​ต์​ใน scripts/clinic-cache/probe.py ซึ่ง​รัน​สอง​ใบ​คนละ​รอบ gradle ด้วย​เหตุผล​นี้
  • ปล่อย​ให้​มัน​แดง​ไว้​ก่อน จนกว่า​กุญแจ​จะ​นับ​ผู้​เช่า​ด้วย

ราคา​ข้อ​แรก​คือ​ชุด​เทสต์​ต้อง​รู้​เรื่อง​ลำดับ​ของ​คำขอ ผู้​เรียก​คน​ที่​สอง​อาจ​ได้​คำ​ตอบ​ของ​คน​แรก และ​นั่น​คือ​สิ่ง​ที่​เรา​ต้องการ​ให้​มัน​จับ​ได้ ไม่ใช่​สิ่ง​ที่​เรา​ต้อง​กัน​ออก​ไป

ราคา​ข้อ​สอง​คือ test class เพิ่ม​ขึ้น และ​รอบ​การ​รัน​แยก​กัน แลก​กับ​ด่าน​ที่​ตรวจ​ของ​จริง

ราคา​ข้อ​สาม​คือ​ด่าน​สี​แดง​ค้าง​อยู่​ใน CI จนกว่า​จะ​ลง​ทรง​ที่​กุญแจ​นับ​ผู้​เช่า​แบบบท​ที่ 3 ทีม​ต้อง​ยอม​เห็น​สี​แดง​ที่​ตรง​กับ​ของ​จริง แทน​สี​เขียว​ที่​ตรง​กับ​ของ​ที่​ไม่มี​ใคร​ใช้

สัญญา​ข้อ​สุดท้าย​ของ​คอร์ส​นี้​จึง​เป็น​ข้อ​นี้ ชุด​เทสต์​ต้อง​รัน​ใน​ทรง​ที่ deploy จริง 🔁 บท​ที่ 8 รวบ​สี่​ข้อ​ไว้​ที่​เดียว และ​ปิด​ด้วย​ทรง​ที่​ผ่าน​ครบ​ทุก​ช่อง


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Spring Boot — Caching (เข้า​ถึง 2026-09-04) — ที่มา​ของ spring.cache.type=none และ​ของ CacheManager ที่ Boot เลือก​ให้​เมื่อ​ไม่มี​ใคร​กำหนด​เอง

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/clinic-cache/probe.py ซึ่ง​สร้าง​โปร​เจ​กต์​ขึ้น​ใหม่ ที​ละ​ทรง​แล้ว​รัน IsolationTest จริง​ที​ละ​รอบ · ต้อง​สั่ง scripts/clinic-cache/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55436

เช็กความเข้าใจ — บทที่ 7

ข้อ 1 / 3

ทรง `cached` ของบทที่ 1 ทำอะไรกับ `IsolationTest` ที่คอร์สที่ 35 ทิ้งไว้