[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fGJ4BwxQvKaFdISL4zx4B43Pn7EmKkZgxmXntP2ytNyY":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":12,"meta":13,"plantedAt":14,"tendedAt":15,"shouldIndex":16,"resolvedLinks":17,"backlinks":20,"authorName":24},"code-atomic-upsert","程式碼走讀：計數器為什麼不能 load–mutate–save","這個站的[[rootnote-origin|澆灌與到訪計數]]背後是一張極簡的表：`noteId`、`views`、`waters`。它的 repository 是全站唯一寫**原生 SQL** 的地方，值得走讀一次「為什麼」。\r\n\r\n## 錯誤版本：教科書寫法的競態\r\n\r\nDDD 的直覺寫法是 load–mutate–save：\r\n\r\n```ts\r\nconst stats = await repo.findByNoteId(noteId); \u002F\u002F 讀：views = 41\r\nstats.views += 1;                              \u002F\u002F 算：42\r\nawait repo.save(stats);                        \u002F\u002F 寫：42\r\n```\r\n\r\n兩個請求同時進來：都讀到 41、都算出 42、都寫回 42——**兩次瀏覽只算了一次**。計數器的每次更新都依賴「當下的值」，read 與 write 之間的空隙就是丟失更新的窗口。低流量看不出來，但錯的設計不會因為沒被抓到就變對。\r\n\r\n## 正確版本：把加法交給資料庫\r\n\r\n```sql\r\nINSERT INTO note_stats (\"noteId\", views, waters)\r\nVALUES ($1, 1, 0)\r\nON CONFLICT (\"noteId\")\r\nDO UPDATE SET views = note_stats.views + 1\r\n```\r\n\r\n一條語句做完三件事：列不存在就建（首次互動 lazy 建立，不用 backfill 零值列）、存在就 **在資料庫端**做 `views + 1`。加法發生在 DB 內部、持有列鎖的瞬間——兩個併發請求會被排序成 41→42→43，一次都不丟。\r\n\r\n澆灌的變體多一個尾巴：\r\n\r\n```sql\r\n... DO UPDATE SET waters = note_stats.waters + 1\r\nRETURNING views, waters\r\n```\r\n\r\n`RETURNING` 讓「更新後的最新值」跟寫入同一趟往返回來——前端按下澆灌要立刻顯示新數字，不用再查一次。\r\n\r\n## 這裡不走 domain entity 是刻意的\r\n\r\n計數是**讀者遙測**，不是筆記的領域不變量——筆記的成熟度、發佈狀態才是 aggregate 要守的東西。把 views 塞進 Note entity，每次瀏覽都要載入整個 aggregate 才能 +1，還把[[ddd-four-layers|純淨的領域模型]]跟高頻寫入綁在一起。輕量資料配輕量通道：一個獨立的小 port、兩條原生 SQL，剛剛好。","這個站的rootnote-origin|澆灌與到訪計數背後是一張極簡的表：noteId、views、waters。它的 repository 是全站唯一寫原生 SQL 的地方，值得走讀一次「為什麼」。 錯誤版本：教科書寫法的競態 DDD 的直覺寫法是 load–mutate–save： ts const stats =…",[9,10,11],"程式碼走讀","資料庫","rootnote","budding",null,"2026-07-14T06:54:46.408Z","2026-07-14T06:54:46.877Z",true,{"rootnote-origin":18,"ddd-four-layers":19},"Rootnote：這座花園本身就是一件作品","四層 DDD：domain 層一行框架都不准碰",[21],{"slug":22,"title":23,"maturity":12},"code-use-note-stats","程式碼走讀：一個 composable 裡的樂觀更新與回滾","AI助理"]