[{"data":1,"prerenderedAt":593},["ShallowReactive",2],{"content-/state-vs-events-revision":3,"all-pages-for-dir":591,"og-image-/state-vs-events-revision":592},{"id":4,"title":5,"body":6,"category":574,"description":575,"extension":576,"meta":577,"navigation":532,"ogImage":578,"path":501,"project_name":579,"published":580,"publishedAt":581,"seo":582,"stem":583,"tags":584,"todo":578,"unpublished":580,"updatedAt":578,"__hash__":590},"pages/2026-05/2026-05-28/state-vs-events-revision.md","「テーブルに状態を持たせるな」記事を反論コメントで鍛え直した日",{"type":7,"value":8,"toc":560},"minimark",[9,18,21,26,29,32,35,38,42,45,48,51,64,67,71,74,77,80,113,116,123,339,343,349,352,358,361,372,392,395,398,407,410,413,416,419,427,430,434,441,456,459,462,479,482,485,509,512,515,518,521,556],[10,11,12,13,17],"p",{},"前日の夕方に公開した ",[14,15,16],"code",{},"state-vs-events.vue"," の記事に、夜のうちに反論コメントが3本届いた。「イベントソーシングの一言で済む話」「理想論でしかない」「体育館の人数管理ならステータスで数えるべき」。朝、ベッドの中でその3本を読み返したとき、頬が一瞬熱くなった。書き手として腹が立ったのではなく、3本ともに芯を喰っていたからだ。反論を本文に取り込んで書き直すしかない、と起きながら決めた。",[10,19,20],{},"きょうの作業は、ひとことで言えば「コメントを本文に編み込む」だった。一行で書ける主張をひっくり返されたら、ひっくり返した相手の言葉を地の文に混ぜて、もう一回立たせる。コードを書く以上に、文章を直す手数のほうが多かった一日だった。",[22,23,25],"h2",{"id":24},"反論1-イベントソーシングの一言で済むを取り込む","反論1: 「イベントソーシングの一言で済む」を取り込む",[10,27,28],{},"最初の反論は、いちばん切れ味が鋭かった。「お前が長々と書いた話は、要するにイベントソーシング/CQRSだろう。一語で済む」と。",[10,30,31],{},"これは反論として正しい。ただ、記事の中でイベントソーシングという用語に触れていなかったのは、書き手としては手抜きだった。読者にとっては「知っているなら最初からそう書け」となる。",[10,33,34],{},"そこで本文に章を増やし、Martin Fowler の Event Sourcing 解説と Microsoft Azure Architecture Center のドキュメントへの言及を埋めた。「私の言葉で延々と説明したことは、業界ではすでにイベントソーシングと CQRS という名前がついている」と書き、用語の出典で殴られないように先回りした。",[10,36,37],{},"Claude Code に「Fowler の event sourcing と CQRS のページを根拠として段落を起こして、用語の初出に必ず一文の定義を添えて」と指示して書かせた。書かせたあと、こちらで「読み返し用の橋渡しの一文」を冒頭に足した。",[22,39,41],{"id":40},"反論2-理想論でしかないを現実的な境界線で受け止める","反論2: 「理想論でしかない」を「現実的な境界線」で受け止める",[10,43,44],{},"二つ目の反論は、感情のこもったやつだった。「全部イベントで持つなんて、現場のコストを知らない人間の理想論だ」と。",[10,46,47],{},"これも、その通りだった。元の記事は「テーブルに状態を持たせるな」と断言する勢いで書いていて、断言の代償として現場の重さを書いていなかった。",[10,49,50],{},"そこで「現実的な境界線」という章を増やした。書いたことは3つ。",[52,53,54,58,61],"ul",{},[55,56,57],"li",{},"規模が小さく、更新頻度も少ないなら、状態テーブル直書きでよい",[55,59,60],{},"ログを永久保持するつもりがないなら、イベントテーブルにも保持期間を切ってよい",[55,62,63],{},"どこで方式を切り替えるかは、ドメインの不可逆性と監査要求の強さで決める",[10,65,66],{},"ここで会計の話に橋を渡した。会計には「期末締め」と「翌期繰越仕訳」という発想があり、過去の仕訳を全部頭から辿り直さないで、期首残高というスナップショットから始める。これはイベントソーシングのスナップショット最適化と同じ構図だ、と一段落で書いた。会計士フォロワーが過半数を占める読者層に「自分の世界の言葉と同じだ」と感じてもらうための橋だ。",[22,68,70],{"id":69},"反論3-体育館の人数管理ならステータスで数えるべきをデモで答える","反論3: 「体育館の人数管理ならステータスで数えるべき」をデモで答える",[10,72,73],{},"三つ目の反論は、もっとも反応に困った。「お前が言うイベントソーシングは大げさだ。体育館の入退館人数を数えるのに、入館/退館のイベントを全部保持するなんて狂気の沙汰だ。今いる人数を整数で1つ持てばいい」と。",[10,75,76],{},"これは半分正しく、半分が落ちていた。「今いる人数」だけがほしいのか、「今日の入退館の履歴」も後で必要になるのか、で答えが変わる。",[10,78,79],{},"文章で説明するより、画面で見比べたほうが速い。そう思って、体育館の人数管理を題材にしたインタラクティブデモを追加した。3列でならべる。",[52,81,82,93,107],{},[55,83,84,88,89,92],{},[85,86,87],"strong",{},"方式A:"," カウンタだけ（",[14,90,91],{},"gym.current_count"," を increment/decrement）",[55,94,95,98,99,102,103,106],{},[85,96,97],{},"方式B:"," イベントだけ（",[14,100,101],{},"gym_event"," テーブルに enter / leave を追記し、",[14,104,105],{},"count"," は集計で導出）",[55,108,109,112],{},[85,110,111],{},"方式C:"," ハイブリッド（イベントを追記しつつ、現在人数のスナップショットも持つ）",[10,114,115],{},"3列のボタンを押すと、同じ操作（5人入る、2人出る、1人入る）が3方式に同時に流れて、内部テーブルがどう変わるかを左右に並べて見せる。「今いる人数を見たいだけならAで足りる」「監査で『18時の入館者を全部出して』と言われたらAは詰む」ということが、画面を見れば1秒で伝わる。",[10,117,118,119,122],{},"ロジックは ",[14,120,121],{},"useGymStory"," というコンポーザブルに切り出して、Claude Code に純粋関数として実装させた。テストは15件、ぜんぶグリーン。",[124,125,130],"pre",{"className":126,"code":127,"language":128,"meta":129,"style":129},"language-typescript shiki shiki-themes vitesse-light vitesse-light","// app/composables/useGymStory.ts（抜粋）\nexport const applyGymStep = (state: GymState, step: GymStep): GymState => {\n  const eventA = step.kind === 'enter' ? +step.count : -step.count\n  return {\n    counter: state.counter + eventA,\n    events: [...state.events, step],\n    snapshot: state.snapshot + eventA,\n  }\n}\n","typescript","",[14,131,132,141,197,249,257,282,306,327,333],{"__ignoreMap":129},[133,134,137],"span",{"class":135,"line":136},"line",1,[133,138,140],{"class":139},"sxvE3","// app/composables/useGymStory.ts（抜粋）\n",[133,142,144,148,152,156,160,163,167,170,174,177,180,182,185,188,191,194],{"class":135,"line":143},2,[133,145,147],{"class":146},"sHkkW","export",[133,149,151],{"class":150},"stQ0i"," const ",[133,153,155],{"class":154},"senZ8","applyGymStep",[133,157,159],{"class":158},"shFtX"," =",[133,161,162],{"class":158}," (",[133,164,166],{"class":165},"s4oTP","state",[133,168,169],{"class":158},": ",[133,171,173],{"class":172},"sSkh3","GymState",[133,175,176],{"class":158},",",[133,178,179],{"class":165}," step",[133,181,169],{"class":158},[133,183,184],{"class":172},"GymStep",[133,186,187],{"class":158},"):",[133,189,190],{"class":172}," GymState",[133,192,193],{"class":158}," =>",[133,195,196],{"class":158}," {\n",[133,198,200,203,206,208,210,213,216,219,223,227,229,232,235,237,239,242,244,246],{"class":135,"line":199},3,[133,201,202],{"class":150},"  const ",[133,204,205],{"class":165},"eventA",[133,207,159],{"class":158},[133,209,179],{"class":165},[133,211,212],{"class":158},".",[133,214,215],{"class":165},"kind",[133,217,218],{"class":150}," === ",[133,220,222],{"class":221},"sMJiu","'",[133,224,226],{"class":225},"sdGka","enter",[133,228,222],{"class":221},[133,230,231],{"class":150}," ? +",[133,233,234],{"class":165},"step",[133,236,212],{"class":158},[133,238,105],{"class":165},[133,240,241],{"class":150}," : -",[133,243,234],{"class":165},[133,245,212],{"class":158},[133,247,248],{"class":165},"count\n",[133,250,252,255],{"class":135,"line":251},4,[133,253,254],{"class":146},"  return",[133,256,196],{"class":158},[133,258,260,264,266,268,270,273,276,279],{"class":135,"line":259},5,[133,261,263],{"class":262},"sz8Xr","    counter",[133,265,169],{"class":158},[133,267,166],{"class":165},[133,269,212],{"class":158},[133,271,272],{"class":165},"counter",[133,274,275],{"class":150}," +",[133,277,278],{"class":165}," eventA",[133,280,281],{"class":158},",\n",[133,283,285,288,291,293,295,298,301,303],{"class":135,"line":284},6,[133,286,287],{"class":262},"    events",[133,289,290],{"class":158},": [...",[133,292,166],{"class":165},[133,294,212],{"class":158},[133,296,297],{"class":165},"events",[133,299,300],{"class":158},", ",[133,302,234],{"class":165},[133,304,305],{"class":158},"],\n",[133,307,309,312,314,316,318,321,323,325],{"class":135,"line":308},7,[133,310,311],{"class":262},"    snapshot",[133,313,169],{"class":158},[133,315,166],{"class":165},[133,317,212],{"class":158},[133,319,320],{"class":165},"snapshot",[133,322,275],{"class":150},[133,324,278],{"class":165},[133,326,281],{"class":158},[133,328,330],{"class":135,"line":329},8,[133,331,332],{"class":158},"  }\n",[133,334,336],{"class":135,"line":335},9,[133,337,338],{"class":158},"}\n",[22,340,342],{"id":341},"次へ方式のステップ送りで読み手の脳を待たせる","「次へ」方式のステップ送りで読み手の脳を待たせる",[10,344,345,346,348],{},"前日の ",[14,347,16],{}," には「ストーリー再生」ボタンがあった。押すと一気に履歴が流れて、見終わったときには「結局何が起きた?」となる作りだった。これも反論コメントで「速すぎる」と言われた。",[10,350,351],{},"今日は「次へ」ボタンに変えて、1ステップずつ進む方式にした。読み手のクリックが、説明文の進行と同期する。アニメーションを派手にするより、こちらが手を止めるほうが、読み手の脳は追いつく。",[10,353,118,354,357],{},[14,355,356],{},"useMembershipStory"," というコンポーザブルに切り出して、テストを6件足した。「next() を呼ぶとカーソルが1進む」「終端で next() を呼んでも進まない」など、純粋関数だけで検証できる範囲に閉じた。",[22,359,360],{"id":360},"方式間の比較を公平にするための地味な修正",[10,362,363,364,367,368,371],{},"方式B/Cのデモには ",[14,365,366],{},"membership_event"," テーブルしかなくて、誰の入退会なのかが見えていなかった。これも反論コメントで「方式Aには ",[14,369,370],{},"member"," テーブルがあるのに、B/Cには無いのは不公平だ」と指摘された。",[10,373,374,375,377,378,300,381,384,385,387,388,391],{},"そこで方式B/Cのデモにも ",[14,376,370],{}," テーブル（",[14,379,380],{},"id",[14,382,383],{},"name","）を追加して、",[14,386,366],{}," に ",[14,389,390],{},"member_id"," を生やした。外部キーの矢印を画面に描いて、方式A/B/Cが同じ「会員」概念を共有していることを目で確認できるようにした。地味だが、ここを揃えないと「方式Bは情報が足りない」という誤解で読まれて終わる。",[22,393,394],{"id":394},"複式簿記アナロジーを補足ボックスに昇格させる",[10,396,397],{},"きょうの作業で、いちばん書いていて手応えがあったのが、複式簿記アナロジーの強調だった。",[399,400,401],"blockquote",{},[10,402,403,406],{},[85,404,405],{},"会計士の方へ:"," 仕訳帳と残高試算表の関係を思い出してほしい。仕訳帳は追記専用ログ（=イベント）、残高試算表は時点の状態（=導出値）だ。残高だけ書き換えて仕訳を残さない会計帳簿は存在しない。残高は年齢に近く、仕訳は生年月日に近い。年齢は毎年書き換わる導出値、生年月日は一度確定すれば二度と変わらない事実。テーブル設計でも、書き換える数字と、保存すべき事実は分けたほうがよい。",[10,408,409],{},"これを記事の中盤に「会計士向けの補足ボックス」として独立させた。文章の真ん中で読者層が分岐するときは、地の文に混ぜるより、囲みで「ここから先は会計の話」と宣言したほうが、読み手の負荷が軽くなる。",[10,411,412],{},"「ドメインの言葉に論理削除はない」という既存セクションの末尾には、「これは方式Bを支持する」と一文だけ足した。書き手として方針を曖昧にしたまま終わらせない、という押し付けがましさを残した。",[22,414,415],{"id":415},"タイトルから断言を抜く",[10,417,418],{},"朝の段階で、タイトルも変えた。",[52,420,421,424],{},[55,422,423],{},"前: 「テーブルに状態を持たせるな」",[55,425,426],{},"後: 「テーブルに状態を持たせてはいけない（は本当か）――3つの設計を見比べる」",[10,428,429],{},"断言を残した上で「は本当か」を括弧で挟む形にした。SEO的には長くなったが、訂正記事だと冒頭で宣言したほうが、前日の記事のリンクを踏んできた読者に対して誠実だ。リード文も「テーブルに状態を持たせる設計は間違いだ」から「テーブルに状態を持たせる設計はケースバイケースだ。前日の記事を書き直す」に変えた。",[22,431,433],{"id":432},"左ボーダーを全廃してスキルに恒久ルールを追記","左ボーダーを全廃して、スキルに恒久ルールを追記",[10,435,436,437,440],{},"文章とは別に、CSSの掃除もした。前日の記事には、引用や注釈に左ボーダー（縦線の色付きアクセント）を多用していた。今日の作業で並べてみたら、強調が過剰で「ここが大事、ここも大事、ここも大事」と全部叫んでいる状態だった。記事から左ボーダーを全部抜いて、",[14,438,439],{},"background-color"," の薄い差で区別する方式にそろえた。",[10,442,443,444,447,448,451,452,455],{},"ついでに ",[14,445,446],{},"vue-pages"," スキルに「左ボーダー（border-left のアクセント）禁止」を恒久ルールとして書き加えた。",[14,449,450],{},".claude/skills/vue-pages/SKILL.md"," と ",[14,453,454],{},".agents/skills/vue-pages/SKILL.md"," の両方に同じ文言を入れて同期させた。今後の自分が同じ過ちを繰り返さないための保険だ。",[22,457,458],{"id":458},"試行錯誤の構図",[10,460,461],{},"きょう一日の構図を一行で書くと、こうだった。",[52,463,464,467,470,473,476],{},[55,465,466],{},"書いた記事に反論が来る",[55,468,469],{},"反論を本文に取り込む",[55,471,472],{},"さらに反論が来る",[55,474,475],{},"デモを追加する",[55,477,478],{},"もう一回、反論を取り込む",[10,480,481],{},"反論は最初の段階では「攻撃」に見えるが、本文に編み込んだあとで読み返すと「補強の鉄筋」に変わる。書き手が一人で書いた記事よりも、反論を3本くぐらせた記事のほうが、構造が太くなる。",[22,483,484],{"id":484},"検証の数字",[52,486,487,496,503,506],{},[55,488,489,490,492,493,495],{},"ユニットテスト全43件 green（",[14,491,356],{}," 6件 + ",[14,494,121],{}," 15件 + 既存22件）",[55,497,498,499,502],{},"dev サーバ起動、",[14,500,501],{},"/state-vs-events-revision"," を 200 で確認、コンソールエラーなし",[55,504,505],{},"カバレッジ: 関数 100%、行 90% 超",[55,507,508],{},"E2E は追加せず（純粋関数中心の修正のため）",[22,510,511],{"id":511},"振り返り",[10,513,514],{},"書いた記事に反論が来たとき、最初の反応で「無視するか」「取り込むか」の分岐が訪れる。今日のケースでは、3本ともに具体例（イベントソーシング、現場コスト、体育館人数）が乗っていたから、無視はできなかった。具体例を持って反論してくる読者がいることの幸運を、本文に編み込むことで返した。",[10,516,517],{},"会計の世界では「期末締めをして翌期繰越仕訳を切る」のが当たり前で、過去の仕訳を全部消すことは決してない。テーブル設計の話を会計のメタファーで説明できると確信したのが、今日の収穫だった。",[22,519,520],{"id":520},"明日やること",[52,522,525,535,550],{"className":523},[524],"contains-task-list",[55,526,529,534],{"className":527},[528],"task-list-item",[530,531],"input",{"disabled":532,"type":533},true,"checkbox"," note の下書きに同じ記事を流し込んで、文中リンクの整形だけ手で直す",[55,536,538,540,541,451,543,545,546,549],{"className":537},[528],[530,539],{"disabled":532,"type":533}," ",[14,542,356],{},[14,544,121],{}," のロジックを ",[14,547,548],{},"app/utils/storyEngine.ts"," に共通化できるか検討する",[55,551,553,555],{"className":552},[528],[530,554],{"disabled":532,"type":533}," 反論コメントの引用元（X の投稿）に「本文に取り込みました」とリプライする",[557,558,559],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sSkh3, html code.shiki .sSkh3{--shiki-default:#2E8F82;--shiki-dark:#2E8F82}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":129,"searchDepth":143,"depth":143,"links":561},[562,563,564,565,566,567,568,569,570,571,572,573],{"id":24,"depth":143,"text":25},{"id":40,"depth":143,"text":41},{"id":69,"depth":143,"text":70},{"id":341,"depth":143,"text":342},{"id":360,"depth":143,"text":360},{"id":394,"depth":143,"text":394},{"id":415,"depth":143,"text":415},{"id":432,"depth":143,"text":433},{"id":458,"depth":143,"text":458},{"id":484,"depth":143,"text":484},{"id":511,"depth":143,"text":511},{"id":520,"depth":143,"text":520},"dev","前日公開した設計記事に「イベントソーシングで済む」「理想論だ」「人数管理ならステータスで十分」と反論が刺さった。反論をそのまま本文に取り込み、複式簿記アナロジーと体育館デモを追加して書き直した記録。","md",{},null,"mdx-playground",false,"2026-05-28T00:00:00.000Z",{"title":5,"description":575},"2026-05/2026-05-28/state-vs-events-revision",[585,586,587,588,589],"イベントソーシング","CQRS","Vue","複式簿記","ドメイン設計","S5BRYH1nkwHkm8NzwbdnBHNjhufCHIRMBh9GGYTsMsw",[],"https://log.eurekapu.com/og/blog/state-vs-events-revision.png?v=2026-05-28T00%3A00%3A00.000Z&title=%E3%80%8C%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AB%E7%8A%B6%E6%85%8B%E3%82%92%E6%8C%81%E3%81%9F%E3%81%9B%E3%82%8B%E3%81%AA%E3%80%8D%E8%A8%98%E4%BA%8B%E3%82%92%E5%8F%8D%E8%AB%96%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E3%81%A7%E9%8D%9B%E3%81%88%E7%9B%B4%E3%81%97%E3%81%9F%E6%97%A5&author=Kei%20Komatsu&sig=7e362f4941a29eb9",1782528844048]