ゆとり日記

心にゆとりを持って生きたいプログラマーの雑記です。気が向いたら書きます。

外部キーNightに参加してきた

https://connpass.com/event/380098/

外部キー制約に興味があったので参加してきた。

以前は「外部キー制約はとりあえず作っておくもの」程度の認識でいたが、昨年の秋〜冬に外部キー制約がほぼ存在しないテーブル群の手入れをしていた時に「外部キー制約を作らない方が良い場面はあるのか?」と考えるようになった。これがイベントに参加しようと思ったきっかけ。

参加してどうだったか?

外部キーは必要だなと再認識した。

不要な場面が存在しないとまでは言わないが、データの不整合が発生するリスクを呑みで外部キーを設けない選択は今のところ取り得ない。少なくとも今日時点ではそう。

結論に至った経緯

昨年、外部キー制約がほぼ存在しないテーブル群の手入れをしていたことは冒頭で述べた。具体的には製品の利用を解約した顧客の利用データを削除する仕組みを作っていて、これがなかなかに大変だった。オープンな場で語っても問題ない形で詳細を書いておく。

外部キー制約が無いテーブル

自分が関わっている製品はとあるSaaSに依存していて、SaaSの方に残るログを自社側のデータベースに残している。なお、ログは顧客の解約に伴って削除する必要がある。

テーブルA(コアデータ)とテーブルB(関連データ)の1:Nの関係性

実際の構成図は載せられないので、詳細をぼかしつつ構成を単純にしたイメージを載せた。製品利用に伴って生成される利用データのコア部分がテーブルA、コア部分に1:Nの関係性で紐づく関連データをテーブルBに永続化している。core_data_idによる外部キー制約がないことが影響して、以下のような問題が起きた。

  • 外部キー制約がないので、テーブルAに関連するテーブルBのデータを厳密に特定できない。
  • テーブルAのid、テーブルBのcore_data_idは等しくなるようにアプリケーションで実装がされているが、データベースによる制約が無いのでデータの完全性は失われている。実際、親データとなるレコードが存在しないテーブルBのレコードが存在していた。さらに補足すると、依存先のSaaSのログは一定期間で消える仕様なので、データの復元は不可能だった。

「外部キー制約があれば、こんなことには・・」という場面が実際にあったという話である。幸いなことに、テーブルBに顧客IDのカラムがあったので消すべきデータの特定が可能なのが救いだが、顧客IDのカラムは「Row Level Securityを将来的に設けるため」に作ったものだったので救われたのは完全に偶然だった。

まとめ

こうした経緯があり、「データの整合性が第一」という思考に則って外部キーは作る派に一票を投じておく。ただ、外部キー制約起因のテーブルロックによるパフォーマンス影響は確かに存在するので、将来の大きな問題にならないように考慮はしておくべきは思うので、現状のデータベース構成に改善点がないかどうかを改めて見直すことにする。

 

コードレビューをAIと伴走する

概要

AIによるドキュメント駆動の開発、vibe-kanbanなどを利用したコーディングがメジャーになり、コード生成は凄まじいスピードで行えるようになりました。しかし、コード生成が高速になっても、人間のコードレビューの高速化には限界があるので、そこがボトルネックになってしまいます。なので、今回はコードレビューに対するスタンスを自分の中で言語化することにしました。

AIによるレビューはアリなのか?

結論からいうとアリだと思います。なので、自分は以下のようにコードレビューをしています。

  • コードを一通り読んで頭に入れる。
  • Claude Codeのレビュー機能でPull Requestをレビューしてもらう。
  • ここまでの手順を終えて違和感や不安を感じるなら、他のコーディングエージェントを利用してクロスレビューをしてもらう。

この手順を踏み、人間の目・AIの双方でコードレビューをするようにしています。

人間の目を通している理由はいくつかありますが、主な理由は以下の2つです。

  • 顧客に提供する製品のコードに対して責任を持ちたい
  • AIは既存コードを模倣してコードを生成する傾向があるので、量産されたくないコードを検知して修正する機会の逃したくない

私は内容を把握してないコードをリリースフローに載せるのは危険だと考えています。全てをAI任せにしていると、AIがたまに書く力任せなコード(例: テストコードに無理矢理なassertを書く)を見逃してしまいますし、コードの細かい部分が頭に残らないので脳内にインデックスを作ることもできません。また、インシデントが起きた場合に「AIがOKと言ったのでリリースしました」という説明しかできないならば、開発者としての責任を果たせていないとも感じます。

2つめの理由も個人的にはかなり気にしているところです。 Claude CodeでもCodexでもそうですが、AIはコードを生成する際に既存のコードを参考にする傾向にあります。長期間の運用を続けているプロダクトだと、「現時点の開発スタイルと食い違う」コードがどうしても出てきます。AIにレビューを任せきりだと、AIが避けたい書き方で書いたコードを出力した時に見逃してしまうリスクがあります。

ただし、PoC(Proof of Concept)で作っているプロトタイプ開発程度であれば、コードの詳細を事細かに把握する必要はない場合もありえます。開発の目的が価値検証ならば、中身の精密さよりも顧客にプロトタイプを触ってもらう事が最優先だからです。

PRを作る側で考える

自分がレビューを依頼する側にはなることも当然あるので、その際もAIを利用したクロスレビューはそれなりに有効だと考えています。

人間にはどうしてもバイアスがあるので、セルフレビューには限界があります。そうした時に複数のコーディングエージェントを活用したクロスレビューは存外に役に立ちます。 また、他のチームメンバーのレビュー前に自分の手前でクロスレビューをしておくことで周りの負担を減らすメリットもあります。

まとめ

ということでAIを任せきりにせずにコードレビューをAIと伴走する進め方について簡単に纏めてみました。

しかし、現状では従来の開発フローの効率化しかできていません。AIありきの新たな開発フローが今後出てきたら、今回のブログで書いた工夫も不要になるかもしれませんね。

2026年の目標: 言語化の習慣をコツコツと作る

元日ということで寝る前にささやかな目標をブログに書いてから寝ることにした。

今年の抱負

2026年は「週に一度はブログを書く」を目標として一年を過ごしたいと思う。酔った勢いで「毎日書く」を目標に掲げかけたが、毎日書くのは単純に大変だし、やむを得ない事情で書けない日が一日でも書けなければ達成不可能になってしまうので、無理のない目標に落ち着かせることとした。目標のファーストステップはできるだけ小さくするに限る。

この目標を立てた理由は、思考の言語化を本格的に習慣にしたいと考えたから。昇進に伴って仕事で関わる人間が増え、さらに生成AI(主にClaude Code)と関わる時間が増えたことで、自分の考えを自分以外に伝える機会が明らかに増えた。そうした場面では「いい感じにして」といった抽象的な表現では意図が伝わらず、思考の結果だけでなく、その過程も含めた正確な表現が必要になる。そこで、練習の場を意識的に設けることにした。

Helpfeel Cosenseには自分向けのメモを日々書いていて、言語化の練習自体はしている。しかし、そこには他人の目がない。つまり、サボっても誰にも分からない、緊張感の無い環境だ。筆不精な自分への戒めとして、ブログというオープンな場を使うのが良いのではないかと漠然と考えた。単純な愚痴やネガティブさが強い内容は控えるが、それ以外のことは技術的な内容であろうとなかろうと、カジュアルに書き出していくつもりだ。この習慣をまずは一年続けてみたい。

雑談

目標の話からは逸れるが、放送大学の勉強はコツコツと続けている。

全科履修生の卒業までに必要な単位のうち、まだ1/3しか取得できていないため、卒業まではさらに3年ほどかかる見通しだ。入学前は4年で卒業するつもりでいたが、自分は人よりも理解に時間がかかるタイプなので、特に苦手科目に手こずっている。とはいえ、少しずつでも前には進んでいる。諦めずに日々の勉強をコツコツ続けていけば、いずれは卒業できるだろうと楽観的に構えている自分もいる。

まとめ

ということで、今年は小さい目標をコツコツ積み上げる心意気で頑張っていきます。 イベントや勉強会の登壇も一度くらいはしておきたいが、そちらは無理のない範囲でやれたらということで。

きのこカンファレンスの当日スタッフをやってきた

きのこカンファレンス」の当日スタッフに参加してきた。イベントの開催側に関わるのが初めての経験で緊張しっぱなしで疲れたので今すぐにでも寝たいけれど、熱が冷めないうちにブログを書く。

https://x.com/kinoko_conf/status/1898597516758896669

我ながら表情が硬すぎて言葉がないんですが・・こんな感じで受付を午前・午後のあちこちで担当しました。

参加した理由

2024年はカンファレンスにほとんど参加できなかったので、2025年は積極的に参加しようと考えている。 その思考に加えて、一般参加・登壇側でしか関わってこなかったカンファレンスに対して開催側の立場で関わるのも新鮮で楽しそうという思いが湧いて今回の参加に至った。

コアスタッフにいきなり首を突っ込むのは敷居が高すぎたので、当日の運営を手伝うくらいの無理のない関わり方がベストではあった。そういう関わり方を許容してもらえるイベントだったのも凄く大きかった。

セッションからの学び

登壇やアンカンファレンスのレポートは他の人が綺麗にまとめてくださっていると思うので、僕はイベントを通して得た学びをレポートとして書くことにする。

仕事の成果を伝えないと評価されない話

今回のカンファレンスのセッションでは「チャンスを掴む」というキーワードを度々耳にした。

仕事を頑張っているのに評価されないとか、自分にはチャンスは回ってこないと考える人は多いだろう。そういう時は「上司が(組織が)悪い」といった他責思考になる前に自分の普段のふるまいを省みた方がいい。成果のアピールが上手くできていなかったり、やりたい事が周りに伝わってない可能性があるかもしれないから。 アピールの観点で1つ補足しておくと、「新しいチャレンジをしました」だけでは弱く、「ふーん、そうなんだ」で終わってしまう。でも、「新しいシステムを導入した結果、業務効率が30%向上し、1人あたりの作業時間が月10時間削減されました!」と言えば、一気に伝わり方が変わる。具体的な成果が示されると、評価者は「それはすごい!」となって受け取り方が変わるし、周りも「あの人は結果を出せる人なんだな」と認識してくれる可能性が高まる。

つまり、行動するだけでなく、その先の「何が変わったのか」ほ言語化が自分を適切に評価してもらうために非常に大事になる。その結果、自分にチャンスが回って来やすい状態を作れるという話だ。

技術の変化は早くても、基礎は裏切らない

よく「技術の変化は速い」と言われる。確かに、毎年のように新しいフレームワークやライブラリが登場し、流行り廃りも激しい。だけど、根本的な部分――例えばネットワークのプロトコルやデータ構造、アルゴリズムなどはそう簡単には変わらない。

もちろん、量子コンピュータや生成AIのように、一部の技術分野ではシンギュラリティ級の変化が起こることもある。しかし、そんな劇的な変化が毎年のように起こるわけじゃない。

だからこそ、キャリアを積み上げて戦っていくためには、 流行している技術の表面を攫うだけでなく、基礎を固めることが大事だ。たとえば、「アルゴリズムとデータ構造」「ネットワークの基本」「OSの仕組み」みたいな知識は、10年経っても腐る可能性はかなり低い。

基礎を固めておけば、新しい技術が出ても応用が効く。結局、変化があっても柔軟に対応できる人になることが、長く戦っていくための大事な事だと思う。

終わりに

久々のカンファレンス参加で、しかもスタッフ側ということもあって緊張は結構してました。ですが、コアスタッフの方々の入念や準備や仕切りだったり、周りの当日スタッフの方々のフォローに助けられ、なんとかやれた気がしています。

次も何かしらの形で関われたら嬉しいです。

Go1.22のfor文と変数スコープ

asakusago.connpass.com

asakusa.goで話すLTのネタとして調べていたメモが一定の分量になったので、ブログに書いておくことにした。

for文の中のクロージャ

import "fmt"

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    Print123()
}

いきなりだが、このコードをGo 1.21以下で実行するとどうなるか?

4
4
4

出力はこうなる。

挙動の解説

Go 1.21までは、for文のループでは同一のiを参照する。Print123関数の実行時にはforループが回り終わっているので、クロージャはi = 4を参照してしまうという話。 1 2 3を出力するために用いられるテクニックとして、以下の回避方法がある。

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        i := i // この行を追加
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    Print123()
}
1
2
3

i := iと書くと期待通りの出力が得られるが、「治ったからいいか」で済ませるのもスッキリしない。せっかくなので、この1行で解決する理由を言語化しておく。

変数のシャドーイング

for文の解説をする前に変数のシャドーイングについて触れておく。

import "fmt"

func main() {
    x := 10
    if x >= 10 {
        fmt.Println(x)
        x := 100
        fmt.Println(x)
    }
    fmt.Println(x)
}
10
100
10

コードを実行すると、このような実行結果が得られる。

if文のブロック内でブロック外のxと同名の変数xを宣言すると、宣言後に記述されているfmt.Printlnが参照するxはif文のブロック内で宣言されたxを参照される挙動に変化する。 その結果、2つめのfmt.Printlnではブロック内で宣言されたxに再代入された100が出力される。

ここでいう変数xのような変数は「シャドーイング変数」(内側のブロックで同じ名前をもつ変数)と呼ばれる。では、先ほどのPrint123関数がシャドーイング変数を利用することで予期した挙動に変わるのは何故か?

Print123の復習

シャドーイング変数を踏まえ、先ほどのコードを見直してみる。

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        i := i // クロージャから参照されるiはここに移った
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

forループも変数スコープなので、i := iをforループでシャドーイング変数として宣言すると、for文のループでは同一のiを参照していた挙動が変化し、参照先がシャドーイング変数として宣言されたiに移る。ループごとでiを保持するように変化したおかげで、無名関数のfmt.Println(i)の出力結果が1 2 3になる。

他のプログラミング言語ではどうか?

参考情報として、他のプログラミング言語で同様の実装がどう動くかを試す。今回はTypeScriptで試した。

function print123() {
     var prints: (() => void)[] = [];
     for (let i = 1; i <= 3; i++) {
         prints.push(() => {
             console.log(i);
         });
     }
 
     prints.forEach(print => print());
 }
 
 print123(); // 1 2 3 が出力される

ちなみに小ネタとして、for文のletをvarに書き換えると出力結果が変わる。

function print123() {
     var prints: (() => void)[] = [];
     for (var i = 1; i <= 3; i++) { // この行のletをvarに変更
         prints.push(() => {
             console.log(i);
         });
     }
 
     prints.forEach(print => print());
 }
 
 print123(); // 4 4 4が出力される

varで宣言した変数はprint123関数全体がスコープになるが、letで宣言した変数はforループそれぞれにスコープが限定される。スコープが限定された結果、console.logが1 2 3をそれぞれ参照できるようになり、期待通りの挙動を得られるという話。

とはいえ、TypeScriptを使用した開発環境でvarをあえて書きたい場面はないので、この罠を踏み抜く機会はないだろう。

Go1.22での挙動

ここでgolangでの挙動に話を戻す。

func Print123() {
    var prints []func()
    for i := 1; i <= 3; i++ {
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    Print123()
}
1
2
3

最初に記載したコードをGo1.22で動かすと、1 2 3が出力される。Go1.22におけるfor文の変化を公式のドキュメントで見てみる。

In a "for" statement, each iteration has its own set of iteration variables rather than sharing the same variables in each iteration.

https://tip.golang.org/ref/spec#Go_1.22 を見るとこのように書かれていて、日本語に直すと「for文では、各反復で同じ変数を共有せず、各反復で独自の反復変数セットを持つ」となる。つまり、forループの最初に暗黙的なi := iを追加する挙動となっていて、go1.21におけるの挙動とは根本的に異なることがわかる。

こうした小さいコードであれば、単純に「Go1.22使った方がいいじゃん!」となるが、この挙動の差によって既存の処理やテストが壊れる可能性がなくはないので、これからGo 1.22に移行を考えている方は要注意。自分が関わっている製品では大きな影響はなく、依存しているパッケージのアップデート待ち(Go1.22に対応待ち)に時間を取られていた記憶がある。

もし既存のテストが壊れることがあれば、影響のあるループをbisectを利用して特定可能。

参考: https://pkg.go.dev/golang.org/x/tools/cmd/bisect#hdr-Example

パフォーマンスはどうなのか?

func Print123() {
    var prints []func()
    for i := 1; i <= 1000000; i++ {
        // go1.21ではシャドーイング変数 i := i を記載
        prints = append(prints, func() { fmt.Println(i) })
    }
    for _, print := range prints {
        print()
    }
}

func main() {
    start := time.Now()
    Print123()

    since := time.Since(start)
    fmt.Printf("The for loop took: %s\n", since)
}

この環境にてループの回数を100万に増やし、処理時間はtime関数で計測する簡単な比較をしてみた。計測を数十回行ったところ、Go1.21でもGo1.22でも処理は3.12~3.18secの範囲に収まり、目立った有意差はみられなかった。

ちなみにproposalで例に挙がっているコードでもパフォーマンスの有意差は見られなかったらしい。ただ、各々が扱うアプリケーションに求められるパフォーマンス水準や複雑性はそれぞれなので、pprofでボトルネックの有無を計測しておくとより安心できそう。

まとめ

多くの人が踏み抜いた経験があろうfor文ループの挙動を変数スコープの観点で調べてみたメモでした。Go1.22では気にする必要はないものですが、「どういう挙動の変化があったのか」「変化する前の挙動はどういう仕組みだったのか」の観点で見返すと、興味深いトピックに巡り会える機会もあるようです。

参考リンク

休日の勉強に対して思うこと

「エンジニアは土日も勉強すべきか」という話題をSNSで見る機会があったので、思うところを書いた。

私の結論としては、個人の環境や立場において成果を出せているなら、その人が勉強するかしないかは自由だと思う。組織の一員として、期待される成果を達成しているならそれでいい。フリーランス個人事業主の場合、契約先の期待を満たすか、それを超える成果を提供していれば、その人の仕事に問題はないと考える。

勉強そのものはあくまで手段であり、目的ではないということを忘れてはいけない。学生時代、勉強時間の多さが成績や受験での評価に直結しないのと同様で、仕事においても、評価の基準は努力の量ではなく成果である。目標とする成果に至る過程で知識や技術の不足を感じたら、その際に勉強をするのが適切だと思う。

一方で、目的ではなく単純な興味がモチベーションとなる勉強にも意味があると考えている。別の形、思わぬ形で役立つことがあり得るからだ。たとえば、経営や営業に直接関わっていなくても関連書籍を読むことや、普段使用していないプログラミング言語での競技プログラミングへの挑戦してみるのはどうだろう。これにより、自分の専門領域以外の知識や思考法に触れ、新しいアイディアや技術の応用が生まれる可能性があるかもしれない。

「目的を達成するための勉強」「目的に結びつくか分からない、興味・関心による勉強」の両軸で勉強という行為を捉えて、各々の目的や興味・関心を大事にできればそれでいいと思う。