丁寧な暮らしをしたいブログ

ITと日々の暮らしについて書きます。

ChatGPTと文章を書く

最近、公開する文章はすべてChatGPTを通すようになった。ブログもXも、ChatGPTとのやりとりを経てから公開している。

自分で書いた文章を、そのまま公開することはなくなった。

推敲不足の文章は読まれない

文章というものは、徹底的に推敲して初めて価値が出るものだと思っている。

元々文章を書くことは好きだったが、仕事が忙しくなってからは、じっくりと推敲する時間を確保できなくなった。

ブログに投稿するようなまとまった分量の文章は書けない。Xでは、誰にも読まれないつぶやきを垂れ流す。そんな自分が本当は嫌だった。

そんな中で、いつからかChatGPTは日本語の文章を高い精度で扱えるようになっていて、文章の推敲にも使えることに気づいた。

今は、文章を書く喜びを思い出しつつある。

今のChatGPTは文章の「創造」には使えない

ChatGPTというと、文章をまるごと生成させる使い方を思い浮かべる人が多いだろう。

しかし、実際にライティングで使い倒してみると、今のChatGPTは、むしろ意外なほど新しい文章の「創造」には使えないことがわかる。一文でも、新しい文を追加させると、とたんに違和感が出てしまう。

私は基本的に文章の内容レベルではすべて自分で書いている。その一方で、初稿の粗い表現を整えて公開可能な水準まで持っていく部分は、ChatGPTに投げることでずいぶんと楽になった。

自分の文体がChatGPTに近づいていく

最初は、ChatGPTの推敲能力の高さに驚いていた。文章を入力すると、まるでプロの編集者の目を通したように、読みやすくすっきりとした文の区切り、表記、言い回しで返ってくる。

それが段々と、ChatGPTに修正された文体を自分の方が学習して、ChatGPTに投げる前の段階から、すでに文体がChatGPT的になることが増えてきた。

この記事も、文章の意味内容はすべて自分で考えて書いているので、いかにもAI生成らしい印象は受けないだろうと思うが、過去の自分が書いたブログ記事と見比べてみると、文体が大きく異なることがわかる。

文章として読みづらくなる方向での自分の文体を信じることはできなくなった。かつて美しいと信じていたこだわりは、今では単純な稚拙さに感じられる。

それでも、文章についてはまだ、人間である自分が主役として書ける。全体を丸ごとAIに書かせるようになったプログラミングとは違う。

ChatGPTと文章を書くことが、今は楽しい。

データで見る『北斗の拳』新アニメ、第2クールはどこまで進むのか

原作再現度が非常に高い北斗の拳の新アニメ

hokuto-anime.com

アニメ『北斗の拳 -FIST OF THE NORTH STAR-』の第1クール全14話が終わった。

『北斗の拳』の新作アニメだが、ストーリー展開が原作漫画に非常に忠実な点に特色がある。一部の異同を除けば、原作のコマ単位で完全再現していると言えるほどだ。

実は私は『北斗の拳』が一番好きな漫画で、原作漫画は最初から最後まで何度も読み返している。

そんなわけで、今回のアニメを楽しみにして、第1話から視聴していた。

当初は失望するも、途中から奥深さに気づく

正直、序盤のシン編については失望に近いガッカリ感があった。

全体的に、原哲夫の絵と比べてCGアニメの画面がスカスカに見えた。シンの南斗聖拳をケンが腕でガードする描写など、深刻な「解釈違い」を感じさせる場面もあった。

そんな印象を変えたのが、第7話のカーネル編だった。

カーネルのスタイリッシュなアクションは、CGとの相性がよかった。また、権力者に失望し、復讐を誓ったエリート軍人という内面も、現代的に再解釈できていた。

このあたりから、アニメ制作陣の原作解釈を楽しむ気分で見られるようになった。

アニメの進行度と原作のページ数に対応関係があることに気づく

このアニメは非常に原作に忠実なので、アニメ視聴後に、手元の漫画で差分を詳しく確認するのが面白かった。

そんな中で、シン編のようなアニオリ要素が多い回を除くと、アニメ1話で消化できる原作ページ数が、ある程度の幅に収まってきていることに気づいた。

原作のページ数を見れば、アニメがどこまで進むかを事前に予想できる。アニメの進行度を予想するのも、いつの間にか楽しみ方に加わっていた。

第1クール全14話のデータを整理すれば、第2クールでどこまで進むかも予想できそうだ。ここで改めて、第1クールの原作進行度を詳しくまとめてみたい。

『北斗の拳 -FIST OF THE NORTH STAR-』第1クールの原作進行度

集計ルール

  • 原作はジャンプ・コミックス版を基準にする
  • ページ数は原作目次のページ表記に従う
  • 扉絵は含める
  • 表紙・目次は除外する
  • 扉絵の前後に存在する場合がある空白ページは除外する
  • 巻をまたぐ場合は、各巻の進行ページ数を合算する
  • アニメの前後の話で原作上の同一場面が重複する場合は、原作章の区切りを優先して「原作対応範囲」を判定した。第6話 / 第7話の「究極の暗殺者」、第10話 / 第11話の「南斗の男!」は、後の話に含めている

巻ごとの集計対象ページ数

総ページ数 除外ページ 集計対象ページ数
1巻 189p 表紙・目次 187p
2巻 181p 表紙・目次・p160空白 178p
3巻 185p 表紙・目次・p42空白 182p
4巻 193p 表紙・目次 191p
5巻 193p 表紙・目次・p90・p92空白 189p

第1クールの原作進行度

話数 原作対応範囲 原作章タイトル ページ数 進行率
第1話 1巻 p3-p47 「心の叫び」 45p 1巻の24.1%
第2話 1巻 p48-p97 「怒り天を衝く時!」
「秘拳!残悔拳」
「KINGの逆襲(途中まで)」
50p 1巻の26.7%
第3話 1巻 p98-p149 「KINGの逆襲(途中から)」
「サザンクロスへ!!」
「宿命の再会!」
52p 1巻の27.8%
第4話 1巻 p150-p189 「狂乱の殺人者」
「執念の炎」
40p 1巻の21.4%
第5話 2巻 p3-p39 「執念と怒り」
「巨星墜つ時」
37p 2巻の20.8%
第6話 2巻 p40-p97 「オアシスでの出会い!」
「悪魔の処刑」
「マッド軍曹」
58p 2巻の32.6%
第7話 2巻 p98-p159 「究極の暗殺者」
「死のブーメラン」
「野望を断つ涙!」
62p 2巻の34.8%
第8話 2巻 p161-p181 / 3巻 p3-p35 「故郷からの使者」
「一口の水のために・・・」
「神に捨てられた村(途中まで)」
54p 2巻の11.8% / 3巻の18.1%
第9話 3巻 p36-p41 / p43-p101 「神に捨てられた村(途中から)」
「怒り!地獄の果てまで!!」
「狂犬ども死すべし!」
「悪魔たちへの挑戦状!(途中まで)」
65p 3巻の35.7%
第10話 3巻 p102-p165 「悪魔たちへの挑戦状!(途中から)」
「悪魔のめざめ!」
「闘神(インドラ)の化身!!」
「死神は欺けない」
64p 3巻の35.2%
第11話 3巻 p166-p185 / 4巻 p3-p39 「南斗の男!」
「ふたつの凶星!」
「涙をみた男たち!!(途中まで)」
57p 3巻の11.0% / 4巻の19.4%
第12話 4巻 p40-p103 「涙をみた男たち!!(途中から)」
「死すべし群狼拳!」
「おまえは女!」
「血ぬられた罠!!(途中まで)」
64p 4巻の33.5%
第13話 4巻 p104-p171 「血ぬられた罠!!(途中から)」
「ただよう死の臭い!」
「マミヤの賭け!」
「恐るべき策略!」
68p 4巻の35.6%
第14話 4巻 p172-p193 / 5巻 p3-p60 「岩を裂く拳!」
「最期に笑う者!?」
「死のタイムリミット」
「凶悪なるまなざし!(途中まで)」
80p 4巻の11.5% / 5巻の30.7%

第1クール全体の集計

指標 数値
総進行ページ数 796p
1話あたり平均進行ページ数 56.9p
到達地点 5巻 p60

データから見る第1クールの進行度

1話あたりの平均進行ページ数は56.9pだが、第5話のシン編完了まではアニオリ描写が多く、進行が顕著に遅い。

第6話以降は進行度が上がり、話による差はあるものの、シン編ほど大きくアニオリ描写が入る回はなくなった。

一方で、最終第14話は特に速く、80pを消化して、一気に牙一族編完了からジャギ登場まで進んだ。

序盤の第5話までと第14話を除くと、第6話から第13話までの平均進行ページ数は61.5pになる。この数値が、第2クールの進行度を考える目安になりそうだ。

第2クールの進行度予想

平均進行ページ数を約60pと見て、原作章の区切りをたどると、以下のような大枠の進行度予想が成り立つ。

  • 第15話〜第16話:ジャギ編
  • 第17話〜第19話:アミバ編
  • 第20話〜第21話:カサンドラ編
  • 第22話〜第23話:トキ再会・拳王侵攻隊
  • 第24話〜第26話:ラオウ初戦

この大枠の予想をもとに、原作章の区切りベースでデータに落とし込むと、以下のようになる。

巻ごとの集計対象ページ数

総ページ数 除外ページ 集計対象ページ数
5巻 193p 表紙・目次・p90・p92空白 189p
6巻 191p 表紙・目次 189p
7巻 189p 表紙・目次 187p
8巻 191p 表紙・目次 189p
9巻 191p 表紙・目次 189p

第2クールの原作進行度予想

話数 原作対応範囲 原作章タイトル ページ数 進行率
第15話 5巻 p61-p89 / p91 / p93-p131 「凶悪なるまなざし!(途中から)」
「死闘への旅だち!」
「幼き犠牲!!」
「師父の予言!」
69p 5巻の36.5%
第16話 5巻 p132-p193 「非情の掟」
「強敵たちの血の果てに!」
「怒拳四連弾!!」
62p 5巻の32.8%
第17話 6巻 p3-p66 「乱を呼ぶ星」
「狂気の堕天使!」
「死のパワーゲーム」
64p 6巻の33.9%
第18話 6巻 p67-p127 「悲劇の再会!!」
「悲劇の星の下に!」
「仮面の裏!」
61p 6巻の32.3%
第19話 6巻 p128-p169 「悲しき天才!」
「飢えた荒野!」
42p 6巻の22.2%
第20話 6巻 p170-p191 / 7巻 p3-p44 「伝説をつくる男たち!」
「死の門をあけろ!!」
「哀しき賭け!」
64p 6巻の11.6% / 7巻の22.5%
第21話 7巻 p45-p104 「救世主たちの墓」
「眠れ 墓標なき墓に!」
「恐怖の足音!」
60p 7巻の32.1%
第22話 7巻 p105-p169 「静かなる巨人!」
「北斗有情拳!」
「失われた北斗の男」
65p 7巻の34.8%
第23話 7巻 p170-p189 / 8巻 p3-p44 「死兆星の蒼光!」
「小さな勇者!」
「迫りくる魔獣!」
62p 7巻の10.7% / 8巻の22.2%
第24話 8巻 p45-p107 「凶星炸裂!」
「見えざる魔拳!」
「死者の警告!」
63p 8巻の33.3%
第25話 8巻 p108-p169 「血を呼ぶ宿命!」
「今一瞬の命を!」
「静水のように」
62p 8巻の32.8%
第26話 8巻 p170-p191 / 9巻 p3-p23 「その秘孔縛を解け!」
「永遠の死闘!」
43p 8巻の11.6% / 9巻の11.1%

第2クール予想の全体集計

指標 数値
総進行ページ数 717p
1話あたり平均進行ページ数 59.8p
到達地点 9巻 p23

第2クールはどこまで進むのか

このように、第2クールを12話構成とする予想を立ててみた。

平均進行ページ数は59.8pで、第1クール後半の進行ペースに近い。ラオウ初戦が終わるのも切りが良い。

その一方で、第1クールが14話構成だったのに、第2クールが12話構成になるのは違和感がある。

では、第2クールも14話構成だとする場合も含めると、どうなるか。考えられるパターンは、次の4つになる。

パターン 到達地点 想定総進行ページ数 1話あたり平均進行ページ数 違和感
12話構成でラオウ初戦完了 9巻 p23 717p 59.8p 第1クールと話数が変わる
14話構成でラオウ初戦完了 9巻 p23 717p 51.2p 第1クールよりさらに遅い進行度になる
14話構成でユダ編途中まで進む クールの切れ目が悪い
14話構成でレイの死まで進む 10巻 p46 929p 66.4p 進行が速すぎる

14話構成でラオウ初戦完了まで進むパターンは、平均進行ページ数が51.2pまで下がる。第1クール全体平均の56.9pよりもさらに遅いというのはあり得ないように思われる。

また、14話構成でレイの死まで進むパターンは、平均進行ページ数が66.4pになる。数字だけ見れば不可能ではないが、第1クール後半よりさらに速い。アニオリ要素をほとんど入れず、かなり急ぎ足で原作を消化する構成になる。

となると、第3クール以降につなぐことを前提として、第2クールはユダ編の途中まで進む、というパターンもあり得るだろうか。

データを見てみると、このアニメの将来について謎が深まる結果になった。

ファンとしては、ラオウ初戦までと言わず、もっと先まで見てみたい。いずれにせよ、来年の第2クールの放送が楽しみだ。

「いいね」は腐敗する

utakatanka.jp

趣味で開発している短歌投稿サイトUtakataのトップページに表示される「人気の歌」の掲載ロジックを変更した。

素朴な「いいね数ランキング」から始まった

Utakataは8年前、私がWeb開発未経験だった頃に作ったサービスだ。

サービス開始当初から、直近期間の被いいね数が多い短歌を「人気の歌」としてトップページに掲載していた。非常に素朴なロジックだったが、最初はそれで十分に機能していた。集計期間を直近で区切ることで、掲載される歌もほどよく入れ替わっていた。

できたばかりのUtakataには個性的なユーザーが集まっていて、トップページを読むと面白い作品を見つけることができた。

台頭する「互助会」

それが段々と、お互いにいいねを付け合う「互助会」ユーザーの投稿短歌が、トップページを占有するようになってきた。もはや短歌の体をなしてすらいないような、「仲間内での交流」を目的とする投稿を見かける機会も増えていった。

これはよくないな、と思っていたのだが、解決策を考えるのが若干難しいことと、仕事が忙しいこともあり、長年この問題を放置してしまっていた。

「すべての歌にいいねを付けられるのがストレス」という問い合わせ

そんな中、あるユーザーから問い合わせをもらった。すべての歌に無差別にいいねを付けるユーザーがいて、ストレスに感じる、という内容だった。

それは確かに不快だよな、と同情する一方で、無差別にいいねを付ける行為自体は利用規約違反とは言い難く、管理人として直接的に対処できることはなさそうに思った。ただ、こうした行為の誘因となっている、「お返しいいね」をもらうことに強いインセンティブが発生しているトップページの掲載ロジックについては、この機会に見直そうと思った。

相互いいねの影響を減らす

「人気の歌」の掲載ロジックを見直し、相互いいねを不利にするような調整を入れてみることにした。まずは極端なロジックで試してみようと思って、Codexに実装させて、思い立ったその日のうちにリリースした。

「短歌」が還ってきた

変更後のロジックも実際かなり素朴なので、こんなもので有意な効果があるのかどうか、リリース前は半信半疑だった。

新ロジックのリリース後、トップページを開く。

そこには「短歌」があった。「短歌」があった、というよりほかない。

長い期間見慣れていて、そんなものかと思っていたけれど、これまで相互いいねの互助会環境で浮上していた短歌は、現代詩としての「短歌」の鑑賞可能性の前提を欠いていたのだと、改めて気づかされた。また、そのような状態のトップページを長年放置してきた自らの怠慢を恥じた。

そして、こんなUtakataにも「短歌」を投稿し続けているユーザーが、今でもたくさんいることに気づかされた。

それでも「いいね」を信じたい

blog.hatenablog.com

他の人の記事にブックマークをつけてお返しとしてのブックマークを期待したり、ブログ記事で「ブックマークお願いします」と書いたり、そのような方法で応援を求めることも、はてなでは決して歓迎してはいません。

一昔前、はてなブログでも「互助会」の問題が話題になっていた。

今回自分で対処してみて、AIを活用するような高度なロジックを導入せずとも、素直に相互いいねの影響を減らすだけで劇的な効果が得られることがわかった。

その一方で、変更後も「いいね」が唯一の評価指標であることに変わりはない。ユーザー同士の鑑賞によって「良い」作品が浮かび上がる、そういう善意に支えられた世界観を信じたい気持ちは、今も変わらず抱き続けている。

奈良から茅ヶ崎に引っ越して、暮らしの成り立ちの違いに気づいた

3月に奈良から茅ヶ崎に引っ越してきた。

奈良と茅ヶ崎での暮らしの違いについて、感じていることを書いてみたい。

どうして茅ヶ崎に引っ越したのか

fuyu.hatenablog.com

きっかけは、私が転職活動をすることになったことだ。最近はフルリモート可の求人が限られるため、首都圏に住みたい気持ちがあった。

また、妻の勤務先である奈良の中小企業でも、社長に東京拠点を本格運用したい意向があり、夫婦そろって東京方面で暮らしたい状況が、ちょうどよいタイミングで重なった。

首都圏の中から神奈川県の茅ヶ崎市を選んだ理由としては、

  • 私の実家が隣の平塚市にあり、湘南地域になじみがあったこと
  • 不動産屋の担当者がサーファーで、自分のインスタを見せながら茅ヶ崎を猛プッシュしてきたこと
  • 実際に現地に降りてみたら、飲食店や個人商店が充実する様子に、奈良に近いものを感じて好ましく思ったこと

このあたりが大きい。

実際に住んでみると、生活圏の成り立ちがかなり違った

実際に住み始めてみると、奈良と茅ヶ崎では生活圏の成り立ちがかなり違うことに気づいた。

奈良で住んでいた近鉄奈良駅の周辺は、商店街や個人商店が生活を支えていた。近鉄奈良駅もJR奈良駅も、駅周辺には商業施設らしい商業施設がほとんどなかった。

一方で茅ヶ崎は、駅前の複合商業施設の存在感がかなり大きい。駅ビルのラスカや、少し歩いた先にあるイオン系の複合商業施設である「そよら湘南茅ヶ崎」に、ユニクロや無印良品、成城石井、カインズ、イオンシネマのような便利なテナントが揃っている。

どちらも駅周辺が充実している街ではあるのだけれど、その暮らしやすさの作られ方は異なる。奈良は伝統的な個人商店中心の世界で、茅ヶ崎はイオン的な複合商業施設中心の世界に近い。

徒歩圏が充実しすぎていて自堕落な奈良

奈良では、近鉄奈良駅とJR奈良駅のちょうど真ん中の一等地に住んでいた。生活は徒歩圏で高度に完結していた。

近鉄奈良駅とJR奈良駅の周辺はチェーン飲食店も充実しており*1、リモートワークで猛烈に仕事をして、近くの餃子の王将などで晩ごはんを食べる日々が続いた。

休日には、近鉄奈良駅前の商店街や、ならまちのあたりの文化的なエリアを散歩すると、それだけで満足してしまうところがあった。

もともとは斑鳩、天理、明日香、吉野などの奈良市以外の地域にも魅力を感じて奈良に移住したはずなのに、実際には徒歩圏からほとんど出ないまま5年があっという間に過ぎてしまった。

フルリモート勤務で自堕落な生活を続けたことで、体力もかなり落ちてしまったように感じる。

茅ヶ崎では丁寧な暮らしを始めたい

茅ヶ崎では、駅から徒歩25分ほどの場所に住んでいる。自宅の周辺は奈良にいた頃と比べるとだいぶ不便だ。

茅ヶ崎では、その不便さを逆に活かして、丁寧な暮らしを始めたいと思っている。

まず、自転車を買った。

少し遠い駅前まで運動がてら移動して、無印良品のような店で生活を整えるものを買い揃える。コンビニやチェーン飲食店に頼りきらず、自炊も少しずつ増やしていく。そんな暮らし方をイメージしている。

首都圏のつらいところ

茅ヶ崎のような関東の地方都市を含む、首都圏の居住地のつらさも見えてきた。

まず、家賃が高い。奈良では、駅近の新築で広い賃貸を安く借りられた。茅ヶ崎では、駅から遠く、奈良の頃よりも狭い部屋を借りるのに、3万円以上高い賃料を払っている。

また、茅ヶ崎のような比較的落ち着いたところに住もうとすると、東京から遠くなる。今度の勤務先の新宿の職場に通うのに、トータルで片道2時間近くかかりそうで、とても大変な気がしている。

地方都市のポテンシャルを活かせないのはもったいない

今回、奈良から茅ヶ崎に引っ越してみてあらためて思ったのは、日本は地方都市のポテンシャルを活かせていないのではないか、ということだ。

奈良のように、文化的な独自性と暮らしやすさを両立している日本の地方都市は数多くあると思うが、仕事が東京に一極集中しているせいで、そういった魅力的な都市に住む選択肢を持てないことがある。

茅ヶ崎に住んでみて、便利で活気のある街を好ましく感じる一方で、今の自分には選択し得ない地方の魅力的な都市の様子を想像すると、なんとも言えないもったいない気持ちになってしまうのだ。

*1:松屋、松のや、餃子の王将、やよい軒、吉野家、すき家、サイゼリヤ、どうとんぼり神座、モスバーガー、鳥貴族など。

転職活動の記録と考えたこと

2026年の1月始めから2月半ばにかけて転職活動をしていたので、考えていたことを振り返ってみたい。

転職を考えたきっかけ

約5年勤めていた現職で、私は新規プロダクトの開発を中心に担う遊撃隊的な部署で、チームリーダーをしていた。

一方で会社としては、事業拡大に伴って、プロダクト全体を3つの役割に分け、共通基盤システムを介して全社的に連携していく構想が進んでいた。

昨年、この構想をさらに推し進めるかたちで、いわゆる「逆コンウェイの法則」の考え方で、開発部署を3層に整理する組織再編が実施された。その結果、私の所属部署も、プロダクトとメンバーが綺麗に3つに分かれ、既存部署に統合されてしまった。

この組織分割によって、開発者としてビジネスやユーザー体験に関われる範囲が狭まり、自分が仕事を通して実現したい価値提供から遠ざかると感じたため、転職を検討するようになった。

今回の転職で重視したこと

今回の転職では、ユーザーからのフィードバックに近い距離で開発できることと、現場で開発を推進できることを重視していた。

現職では、組織の拡大にともなって部署間のコミュニケーションがボトルネックになりつつあり、そこにやりづらさを感じていたため、応募先は自然と小さめの企業が中心になった。

また、カジュアル面談を通じて、製造業DXの領域には勢いのあるベンチャー企業が多く、今かなり活発な市場になっていることが見えてきた。工場などの現場改善を通じて、ユーザーからのフィードバックに近い距離で開発できる点も、自分が重視していたことと重なっていたため、製造業DXに関わる求人を多く受けることになった。

転職活動の経過

今回の転職活動は短期で進めたかったので、転職エージェント2人とWantedlyでの自己応募という複数の経路を使って、一気に進めた。

年始から動き始め、毎週かなりの数の面接を入れながら進めた結果、ほぼ最短の1か月半で4社の内定が揃った。

今回は小さめの企業を中心に受けていたが、オファー面談まで進んでみると、特に規模の小さいスタートアップでは、仕事そのものは面白そうでも、人事労務制度が未整備で、入社後のリスクを読みきれないと感じる部分もあった。

そのため、最終的には、仕事の面白さを感じつつも、一定の規模があり、人事労務制度もある程度整っている会社を選ぶことにした。

年収は現職より100万円上がって800万円台になった。

エンジニアのキャリアは「やりたいことベース」でもよいのかもしれない

エンジニアのキャリアについて、スペシャリストとマネジメントのどちらを目指すか、みたいなテーマは誰にとっても悩ましいと思う。ただ、今回の転職で思ったのは、あまり「型」にはめて考えずに、自分の得意なこと・やりたいことベースでも意外に通用する、ということだ。

Webベンチャーのリーダー層に求められるのは、結局のところ「フワッとおまかせでいい感じに」みたいなところにつきるので、これまでの経験を活かして、技術・人・組織に謙虚に向き合うところに、おのずと道が開かれるのだと思う。

現職ではかなり仕事に打ち込んできて、年齢を重ねたことも影響しているのか、何だか根拠もなく自信があるようなところもある。新しい職場で何ができるのか、今から楽しみだ。

Rails 8.1 で schema.rb が ABC 順になったので structure.sql に移行してみた

いつものように Dependabot の自動アップデートで Rails 8.1 に上げたところ、急に schema.rb に不可解な差分が出るようになって驚いた。

差分をよく見ると、テーブルのカラムが ABC 順に並び替えられていることに気づく。

railsguides.jp

Active Recordは、schema.rb内のテーブルカラムをデフォルトでアルファベット順にソートするようになりました。これにより、マシン間でスキーマダンプが一貫するようになり、マイグレーションの順序によって左右されなくなり、結果としてノイズの多い差分が削減されます。structure.sqlは、カラム順序を厳密に維持するために引き続き利用できます。スキーマ変更のアルファベット順化の詳細については、#53281を参照してください。

Railsガイドで、8.1 で追加された仕様変更として解説されている。

github.com

さらに詳しい経緯は、該当の PR を読むと把握できる。

当初は「オプションで選べるようにする」機能として PR が立てられた。@byroot によって、オプションにすべきかどうかに疑問が投げかけられる。@matthewd は schema.rb でカラムの並び順を参照するユースケースについて言及するなどして、慎重な見解を述べている。

そんな中、DHH の鶴の一声で「オプションにはせず、常に ABC 順にする」という方針が決定される。

This should not be an option. This should just be default behavior. Schema dumping should be deterministic regardless of the platform.

これはオプションにするべきではない。これは単にデフォルトの挙動であるべきだ。スキーマのダンプは、プラットフォームによらず決定的であるべきだ。

Rails における DHH の、神のような決定権を感じさせる一幕だ。3つの文のすべてに should が入っていて、「そうあるべきだ」という強い意志がにじみ出ている。

方針決定後の @matthewd によるフォローのコメントでは、カラムの並び順を管理したい場合は structure.sql を使うべきであることが示唆されている。

変更前のschema.rbの課題

変更前の schema.rb の仕様では、複数の開発者が同時に DB マイグレーションを実装するような場合に問題があった。各開発者がローカル環境でどのような順番でマイグレーションを実行したかによって、カラムの並び順が変わってしまう可能性があったのだ。

そのため「 rails db:migrate:reset コマンドを実行した場合の並び順を正とする」といった運用ルールを決めておかないと、カラムの並び順が一意に定まるようにファイルを更新することができなかった。

この問題に対して、カラムを ABC 順に並べることで、常に一意の並び順に決定できるようにしたのが、この PR による仕様変更だ。

とはいえ、カラムの並び順は重要では?

その一方で、MySQL にはカラム追加の際に任意の並び順を指定できる機能があるため、可読性の観点などからカラムの並び順を意識的に管理しているケースもあるだろう。私のチームも、カラムの並び順を意識する運用にしていた。

カラムの並び順を意識する開発方針の場合、コードレビューでは「カラムの並び順が妥当かどうか」も確認対象になる。このとき、カラムの並び順の差分を確認できるファイルが存在しないと、レビュワーは手元で DBMS を動かしてスキーマを確認しなければならない。これは手間がかかるし、チームとして実装・コードレビューの運用方針を徹底することも難しくなりそうだ。

そのため、カラムの並び順を意識する開発方針を取るのであれば、「実際のカラムの並び順を参照できるファイルがあること」は、運用上ほぼ必須要件になりそうだ。

こうした背景から、私のチームでは schema.rb から structure.sql へスキーマダンプのフォーマットを移行することにした。

structure.sqlとは

config.active_record.schema_format = :sql

config.active_record.schema_format:sql に設定すると、デフォルトの schema.rb ではなく、structure.sql によってスキーマダンプが管理されるようになる。

structure.sql では、データベース固有のツールを用いてデータベースの構造がダンプされる。たとえば MySQL の場合は、mysqldump ユーティリティ が用いられる。

structure.sql の中身は、以下の部分例のように「そのまま SQL」となる。

/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `announcements`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `announcements` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `site_id` bigint NOT NULL,
  `title` varchar(255) COLLATE utf8mb4_0900_as_cs NOT NULL COMMENT 'タイトル',
  `content` text COLLATE utf8mb4_0900_as_cs NOT NULL COMMENT '内容',
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_announcements_on_site_id` (`site_id`),
  CONSTRAINT `fk_rails_81bca04b30` FOREIGN KEY (`site_id`) REFERENCES `sites` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_cs COMMENT='お知らせ';

structure.sql に含まれる SQL を実行することで、定義されたテーブル構造を再現できる仕組みになっている。

structure.sql では環境ごとの差異をなくすために工夫が必要

実際に試してみると、 structure.sql は、開発環境ごとの差異をなくして、純粋にスキーマの差分だけを管理できるようにするために、工夫しなければいけないことがいくつかあると気づいた。

mysqldump を実行するクライアントの違い

FROM ruby:3.4.7

RUN apt-get update \
  && apt-get install -y --no-install-recommends default-mysql-client \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

例えば、上のように ruby の Docker イメージをベースに環境構築した場合、OS は Debian になるため、MySQL 互換の実装として MariaDB が標準になっている。Debian の APT の初期状態では、Oracle MySQL の mysql-client はインストールできず、default-mysql-client の実態は mariadb-client となっている。

この環境では、mysqldump コマンドは mariadb-dump へのシンボリックリンクとして提供される。

mysqldump コマンドの実態が本物の mysqldump なのか、mariadb-dump へのシンボリックリンクなのかによって、利用できるオプションやスキーマダンプの生成結果に差異が生じることに注意が必要だ。

AUTO_INCREMENT= の値が開発環境ごとにばらばらになる

CREATE TABLE `announcements` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `site_id` bigint NOT NULL,
  `title` varchar(255) COLLATE utf8mb4_0900_as_cs NOT NULL COMMENT 'タイトル',
  `content` text COLLATE utf8mb4_0900_as_cs NOT NULL COMMENT '内容',
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_announcements_on_site_id` (`site_id`),
  CONSTRAINT `fk_rails_81bca04b30` FOREIGN KEY (`site_id`) REFERENCES `sites` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_cs COMMENT='お知らせ';

mysqldump の出力結果では、上の AUTO_INCREMENT=6 のように、開発環境のデータがどう入っているかによって「次に INSERT されるプライマリキーの採番値」に差異が生じてしまう。

スキーマロードするかマイグレーションするかで CHARACTER SET の有無が異なる

`title` varchar(255) COLLATE utf8mb4_0900_as_cs NOT NULL COMMENT 'タイトル',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs NOT NULL COMMENT 'タイトル',

上のように、varchar カラムに対する CHARACTER SET utf8mb4 の有無が、Rails でスキーマロードするか、マイグレーションするかによって異なる問題に遭遇した。

zenn.dev

これは mysqldump の内部実装による複雑な事象だが、上の記事で詳しく解説してくださっている方がいたおかげで何とか理解できた。

  • 照合順序(Collation)をデフォルトから変更した場合に、この差異が生じる
  • 差分の有無による実際の DB スキーマの仕様の違いは存在しない
  • structure.sql の SQL を実行した場合と、Rails の通常のマイグレーションで実行される SQL が異なる
    • structure.sql の SQL ではカラムごとに COLLATE が明示されているが、Rails のマイグレーションでは明示されない
  • Rails では DB マイグレーション系のコマンドの実行方法によって、 rails db:schema:load が実行され、 structure.sql の SQL が実行される場合がある
  • この違いによって mysqldump の出力結果に差異が生じる

環境差異の解消方法

structure.sql を生成する際の mysqldump の挙動をカスタマイズする方法は、どのレイヤーで上書きするかによっていくつかの選択肢がありそうだ。私はその中から、config/initializers/mysql_database_tasks.rb に以下のようなファイルを配置して設定を上書きする方法を選んだ。

# development と test 環境でのみ設定する
if Rails.env.local?
  # mysqldump で structure.sql を生成する方法を設定する
  ActiveSupport.on_load(:active_record) do
    # mysqldump 実行時に TLS 接続をスキップする
    # 開発環境ではシンボリックリンクで mariadb-dump が実行されるため、 --ssl-mode オプションが存在しない
    # GitHub Actions の環境では mysqldump が実行される
    mysqldump_flags = ENV['GITHUB_ACTIONS'] == 'true' ? %w[--ssl-mode=DISABLED] : %w[--skip-ssl]

    ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = {
      mysql2: mysqldump_flags
    }

    ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = {
      mysql2: mysqldump_flags
    }

    ActiveRecord::Tasks::MySQLDatabaseTasks.prepend(Module.new do
      def structure_dump(filename, extra_flags)
        super
        content = File.read(filename)
        # AUTO_INCREMENT= の値は個別の開発者の環境のデータの入り方によって異なるため、 structure.sql から削除する
        content.gsub!(/\sAUTO_INCREMENT=\d+/, '')

        # DDL で varchar カラムに COLLATE を指定するかによって structure.sql に `CHARACTER SET utf8mb4` の有無の差異が生じる
        # Rails のマイグレーションでは COLLATE指定なし、 structure.sql をロードした場合は COLLATE 指定ありの DDL になる
        # `CHARACTER SET utf8mb4` の有無による仕様の違いは存在しないため、開発環境ごとの差異をなくす目的で、structure.sql から削除する
        content.gsub!(' CHARACTER SET utf8mb4', '')
        File.write(filename, content)
      end
    end)
  end
end
# mysqldump 実行時に TLS 接続をスキップする
# 開発環境ではシンボリックリンクで mariadb-dump が実行されるため、 --ssl-mode オプションが存在しない
# GitHub Actions の環境では mysqldump が実行される
mysqldump_flags = ENV['GITHUB_ACTIONS'] == 'true' ? %w[--ssl-mode=DISABLED] : %w[--skip-ssl]

この部分では、開発環境で実行される mysqldump の実態が mariadb-dump となっている一方で、GitHub Actions のUbuntu環境では mysqldump となっている問題に対処している。

content = File.read(filename)
# AUTO_INCREMENT= の値は個別の開発者の環境のデータの入り方によって異なるため、 structure.sql から削除する
content.gsub!(/\sAUTO_INCREMENT=\d+/, '')

# DDL で varchar カラムに COLLATE を指定するかによって structure.sql に `CHARACTER SET utf8mb4` の有無の差異が生じる
# Rails のマイグレーションでは COLLATE指定なし、 structure.sql をロードした場合は COLLATE 指定ありの DDL になる
# `CHARACTER SET utf8mb4` の有無による仕様の違いは存在しないため、開発環境ごとの差異をなくす目的で、structure.sql から削除する
content.gsub!(' CHARACTER SET utf8mb4', '')
File.write(filename, content)

この部分では、正規表現の置換を用いて、開発環境による差異が出る部分を削除している。

文字列の置換ではなく、 mysqldump のオプション指定によって解消できないかも検討したが、私の試した範囲では難しかった。インターネットで事例を検索しても、愚直に文字列を置換しているソリューションしか見つけられなかった。

この設定をすることによって、環境ごとの差異がなく、スキーマの差分のみを綺麗に確認できる structure.sql の運用が可能になった。

structure.sql を使ってみた所感

その後、structure.sql を運用してみて、実際の SQL が表示されることで MySQL 上の設定に自覚的になれることにはメリットがありそうだ。

一方で、structure.sql に書かれる SQL は情報量が多く、ごちゃごちゃしていて読みづらいのも事実だ。「schema.rb でサッとテーブル情報を確認できたのは、やっぱり良い体験だったなあ」という思いは正直なところある。

トレードオフのある論点について、ライブラリの意思決定の難しさを垣間見ることができたと感じている。

git worktree を使いこなして、AI コーディングエージェントの同時並行開発を実現する

OpenAI Codex に GPT-5-Codex が登場してから、ローカル環境で動かせる AI コーディングエージェントが、業務でも普通に使えるレベルに到達したと感じている。最近では、実装は自分で一から書くのではなく、コーディングエージェントに任せて、最後の手直しだけを自分でやることが多い。

ローカル環境でのコーディングエージェントの活用が業務の中心になってくると、ひとつ気になる制約が見えてくる。「ある Git リポジトリをコーディングエージェントが編集しているあいだに、そのリポジトリを同時に編集しづらい」という問題だ。

具体的には、次のようなことをやりたくなる。

  • 同じ Git リポジトリに対して、複数のコーディングエージェントを同時に動かしたい
    • チケット A とチケット B の開発を同時に進める
    • Codex と Claude Code に同じチケットの開発を依頼して、アウトプットの品質が良い方を採用する
  • 同じリモートブランチに対して、コーディングエージェントと人間で同時に開発したい

しかし Git では、ひとつの作業ディレクトリにつき同時にチェックアウトできるブランチは 1 つだけだ。普通に開発ブランチを切って作業している場合、そのリポジトリをコーディングエージェントが編集しているあいだに、同時に編集しようとすると、差分が混ざってしまい、訳のわからない状態になってしまう。

こういった問題を解決する存在として、2015 年にリリースされた Git 2.5 で導入された git worktree の機能が、コーディングエージェントを用いた複数同時開発の文脈で、改めて注目されている。

git worktree は、ひとつのリポジトリに対して複数の作業ディレクトリ(worktree)を作成できる機能だ。作業ディレクトリごとに別々のブランチをチェックアウトしておけば、それぞれの作業ディレクトリで、他の作業ディレクトリの編集内容に影響されることなく開発を進めることができる。

Claude Code のドキュメントでも、git worktree を使った並行開発の手法が紹介されている。

仕組みだけを見ると、「作業ディレクトリを増やして、それぞれでコーディングエージェントを動かすだけ」の簡単な話に見える。手元の開発環境で早速やってみようとなったのだが、実際に試してみると、いくつかハマりどころがあった。

試行錯誤の結果、快適に複数同時開発を回せる開発環境が整ったので、git worktree の使い方の一例として紹介したい。

この記事では、git worktree で作成されるディレクトリのことを「作業ディレクトリ」(worktreeに相当)と呼ぶ。

git worktree を初めて使ったときに困ったポイント

初めて git worktree の機能を試したときに、次のような部分が期待通りに動かなくて困った。

  • 作業ディレクトリの新規作成からコーディングエージェントに依頼すると、コーディングエージェントは作成した作業ディレクトリに移動して作業できない
    • コーディングエージェントに「起動時のカレントディレクトリ配下だけ編集可能」という制約がある場合に、それ以外のディレクトリに移動して編集することができない
  • .gitignore で指定された .env など、開発環境の動作に必要なファイルが、作成した作業ディレクトリ側には存在しない

今振り返ると、これらの問題は作業ディレクトリの新規作成からコーディングエージェントに任せようとしたことが原因だったと思う。作業ディレクトリの作成や .env の準備といった環境づくりは、自分の手で整えるのが良さそうだ。

とはいえ、開発チケットごとに毎回手作業が発生するのは面倒だ。そこで、私は「複数のチケット開発に使い回せる作業ディレクトリ」を作っておき、そこでコーディングエージェントに開発を依頼する運用にしている。以下、その作成手順を紹介したい。

コーディングエージェントを動かすための作業ディレクトリの作成手順

前提情報

Git リポジトリのデフォルトの作業ディレクトリを「人間が開発するための作業ディレクトリ」として扱い、コーディングエージェント用に新しい作業ディレクトリを追加で作成する、という想定での手順を紹介する。

以下はサンプルとして使う名称なので、実際の環境での名称に適宜置き換えてほしい。

作業ディレクトリの作成

まず、コーディングエージェント用の作業ディレクトリを git worktree で作成する。

git worktree add ../myapp-ai -b main-ai origin/main

このコマンドは、次の 2 つを同時に行っている。

  • ../myapp-ai という作業ディレクトリ(worktree)を新規に作成する
  • リモートのデフォルトブランチ(origin/main)をトラッキングする main-ai というローカルブランチを作成し、その作業ディレクトリでチェックアウトする

Git では、同じブランチを複数の作業ディレクトリで同時にチェックアウトすることはできない。そのため、デフォルトブランチと同じ名前のブランチ(main)をそのまま複数の作業ディレクトリで共有することはできないので、main-ai のように、「デフォルトブランチをトラッキングするブランチ」を作業ディレクトリごとに用意する構成にしている。

作業ディレクトリごとに「デフォルトブランチをトラッキングするブランチ」を用意しておくと、どの作業ディレクトリでも同じように、

  • デフォルトブランチ相当のブランチで git pull して最新の実装内容を同期する
  • そこから新しく開発ブランチを切って作業を始める

という開発サイクルを回せるようになる。

.gitignore されている開発環境用のファイルを整える

次に、作成した作業ディレクトリ側に、開発環境で必要なファイルを用意する。

具体的には、.env のように .gitignore で ignore されていて、かつ開発環境の動作に必要なファイルは、コーディングエージェント用の作業ディレクトリにも配置しておく必要がある。

作成した作業ディレクトリでコーディングエージェントに開発依頼する

ここまで準備できたら、myapp-ai 側をコーディングエージェント用の作業ディレクトリとして使えるようになる。

  • 人間は従来どおり myapp 側で開発する
  • コーディングエージェントは myapp-ai 側で動かす

という分担にしておくと、お互いの作業が干渉しにくくなる。

同じ手順で作業ディレクトリを増やしていけば、コーディングエージェントを 2 つ 3 つと同時に動かせるようになる。

人間とコーディングエージェントで同じリモートブランチを参照する方法

コーディングエージェントが feature/example-function というリモートブランチを作成して push したとする。このブランチを、エージェントに編集してもらいつつ、人間もローカルで編集したい、という場面がある。

その場合は、次のようにして「同じリモートブランチをトラッキングする別名のローカルブランチ」を作成すると、運用がやりやすい。

git switch -c feature/example-function-human origin/feature/example-function

このように、リモートブランチをトラッキングするローカルブランチを人間用に別名で作成しておくと、

  • リモート側では 1 本のブランチ( feature/example-function
  • 手元では、エージェント用と人間用で別々のローカルブランチ

という構成にできる。これによって、人間とコーディングエージェントで同時に同じリモートブランチに対して編集することが可能になる。

Docker Composeによる開発環境構築で困ったポイントと解決策

Docker Compose を使って開発環境を構築している場合に、worktree ごとに別プロジェクト扱いになる、という問題にもぶつかった。

Docker Compose は、デフォルトでは「ディレクトリ名」をもとにプロジェクト名を決める。そのため、 myappmyapp-ai のようにディレクトリ名が異なると、

  • コンテナ名
  • ネットワーク名
  • ボリューム名

などがそれぞれ別々に作成されてしまう。

この仕様によって、

  • 複数の作業ディレクトリで同じポートを使おうとしてエラーになる
  • 作業ディレクトリごとに Docker リソースがどんどん増えていって整理しづらくなる

といった問題が発生した。

この問題については、COMPOSE_PROJECT_NAME環境変数.env に追加することで解決できた。

COMPOSE_PROJECT_NAME=myapp

また、コマンド実行時に -p--project-name ) オプションで明示的にプロジェクト名を指定することもできる。

docker compose -p myapp run --rm app bin/rspec

こうしてプロジェクト名を固定しておくと、複数の作業ディレクトリから、同じ Docker コンテナを共有できるようになり、Docker Composeによるコマンド実行が複数同時開発時にも支障なく動作するようになった。

VSCode での git worktree のサポート

VSCode では、git worktree の機能が公式にサポートされている。独立したリポジトリを扱うような感覚で、それぞれの作業ディレクトリを別々の VSCode ウィンドウで開くことができて便利だ。

おわりに

この記事に書いた手順で、ローカル環境でのコーディングエージェントによる同時並行開発を、快適に回せるようになった。git worktree は慣れるまでに挙動が非直感的に感じられる部分もあるが、一度コツを掴んでしまえば、AI 時代の開発に必須のテクニックという印象がある。

最近はモニターを 1 枚新たに買い足して、複数のコーディングエージェントがそれぞれの作業ディレクトリで頑張って開発している様子を小脇に眺められるようにしている。仕事の風景は、ほんの半年前と比べてもすっかり様変わりしていて、時代の急速な変化を日々感じているところだ。