บรรทัดเดียวในฝั่งเทสต์ ทำให้ด่านกลับมาเขียว โดยของที่ deploy ยังรั่วเท่าเดิม
บทที่ 1 ทิ้งเรื่องหนึ่งค้างไว้ ตอนนั้นเราวัดคำตอบกับจำนวนแถว แต่ยังไม่ได้พูดถึงด่านที่คอร์สก่อนหน้าทิ้งไว้ให้
บทนี้เล่าเรื่องนั้นให้จบ ตั้งแต่ตอนที่ด่านแดง จนถึงบรรทัดที่คนแนะนำกันทั่วไป ซึ่งทำให้มันกลับมาเขียวโดยที่รูยังอยู่ครบ
เทสต์ของคอร์สที่ 35 แดงจริง
หัวข้อที่มีชื่อว่า “เทสต์ของคอร์สที่ 35 แดงจริง”ทรง
cachedทำให้IsolationTestแดง ไม่ใช่เขียว
IsolationTest คือของที่ คอร์สคัดกรองผู้เรียกของคลินิก
ส่งมอบมา มันมีข้ออ้างเดียว คือผู้เรียกเห็นแถวของผู้เช่าตัวเองเท่านั้น
@Testfun `ผู้เรียกเห็นแถวของผู้เช่าตัวเองเท่านั้น`() { assertEquals("{\"count\":2,\"patients\":[\"พิมพ์ใจ\",\"สมชาย\"]}", body("kaen-a")) assertEquals("{\"count\":1,\"patients\":[\"วีระ\"]}", body("kaen-b"))}บรรทัดที่สองคือบรรทัดที่แดง ผู้เรียกของ clinic-b ควรได้ วีระ แต่ได้
พิมพ์ใจ กับ สมชาย ซึ่งเป็นสองแถวของ clinic-a
นี่คือข่าวดีข้อเดียวของบทที่ 1 ด่านที่มีอยู่แล้วจับรูนี้ได้เอง โดยไม่ต้องมีใครเขียนอะไรเพิ่ม
อ่านคำเตือนนี้ก่อนแตะ properties ของเทสต์
หัวข้อที่มีชื่อว่า “อ่านคำเตือนนี้ก่อนแตะ properties ของเทสต์”
application.propertiesของเทสต์บังของ main ทั้งใบ ไม่ใช่ทับทีละบรรทัด
classpath ของเทสต์มาก่อน และ Spring หยิบใบแรกที่เจอเพียงใบเดียว เขียนบรรทัดเดียวลงไปที่นั่น แล้วค่าของ datasource หายทั้งชุด
spring.cache.type=noneแอปตายด้วยข้อความ Failed to determine a suitable driver class
ในข้อความนั้นไม่มีคำว่า cache สักคำ คนอ่านจึงไปไล่หาสาเหตุผิดที่
ของจริงคือคัดทุกบรรทัดของ main มาก่อน แล้วค่อยเติมบรรทัดเดียวต่อท้าย
spring.datasource.url=jdbc:postgresql://127.0.0.1:55436/clinicspring.datasource.username=postgresspring.datasource.password=clinicspring.jpa.hibernate.ddl-auto=nonespring.jpa.open-in-view=falsespring.jpa.properties.hibernate.generate_statistics=truespring.main.banner-mode=offspring.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") |
cacheManager | ConcurrentMapCacheManager | NoOpCacheManager |
| กุญแจใน 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 ทิ้งไว้