バイブコーディングでOSSコントリビュートができた話（後編）

# 導入

https://qiita.com/ryotab22/items/42f51aaf423de69b1dc3

前回記事の続きです
ここまでで、Pull Requestを出すところまで完了しました。

:::note
この記事で伝えたいこと
1. 生成AIの登場でOSSコントリビュートの敷居が下がって楽しくなってきたよ
2. メンテナーからAIの知見を超えたレビューがもらえるよ（生成AI時代を生きるジュニアエンジニアにとってはとても貴重なもの）
:::

# コードレビューを受けて

リポジトリのメンテナーから以下のようなレビューをいただきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/d0eaa771-10bd-4433-bad5-e8830130c67e.png)

レビュー内容を把握するために、GPT4oに翻訳してもらったところ次のような内容でした

> 全てのエピソードを読み込む処理が非常に非効率です。なぜなら、何十万ものエピソードや何百ものポッドキャストがある場合、それぞれが追加のデータベースクエリとJavaオブジェクトへのパースを伴うからです。
> 統計情報を収集するためにすべてのエピソードをスキャンする必要がある場合、データベース側で直接処理した方がはるかに効率的です。そのため、統計データを生成しているSQLクエリを確認してもらえますか？

お恥ずかしながら、これは完全に初歩的なミスでした。
**UI層にループを含むロジックを書くべきではない**という基本を忘れていました。

> 僕がこのような実装をしてしまった理由は、対象のコードの周囲にも for 文が存在していたためです。
それを見て「このプロジェクトでは責務分離がそこまで厳格ではないのかも？」と、勝手に思い込んでしまったのです。

いわゆる“割れ窓理論”ってやつですね。今後は気をつけたいところです。

:::note
割れ窓理論とは
1枚の割れた窓ガラスを放置しておくと、さらに割られ、やがて街全体が荒れてしまうという理論。

実は、生成AIもこの“割れ窓”の影響を受けやすいと言われています。
周囲のコードに引っ張られる傾向があるため、指示を出す際にはその点にも気を配る必要がありそうです。
:::

ちなみに、レビューに対する返信内容は以下のように日本語で書いた後、GPT4oに翻訳してもらっています。

> レビューありがとうございます。
> おっしゃる通り、こういった処理はUI層でやるべきではないですね。
> SQLクエリ側で対応できるか確認してみます。改善できたらPRを更新します。
レビュー感謝です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/c53df822-47a8-4d08-8121-16fda454bbd1.png)


# SQLクエリの修正へ

ということで、AIに指示を出しながらSQLの修正に取り掛かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/0a1b33c4-59d3-4d88-b177-7fbc571f4bab.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/e2089415-a718-4b5f-aebf-d7b7e8542e6f.png)

DBReader.getStatistics() のSQLクエリを調査し、どこを修正すればよいかを洗い出してくれました。ナイス 👍

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/d0650c53-4508-48af-b858-a2aa366c0842.png)

提案に乗ってそのまま調査を依頼します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/b9920346-c0b9-42e3-bd68-e54b69c4a21e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/3fbfe01d-747d-4713-9a93-a8d708ca952e.png)

具体的な修正提案を受け、統計情報を計算するロジックをSQL側で処理することでパフォーマンス向上が見込めそうです。

---

ただし、**気になるのは変更による影響範囲**。
DBReader.getStatistics() は7箇所から呼ばれており、安全性が気になったのでAIに依頼して調査してもらいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/7997aa91-d490-4579-b254-ed791de3717f.png)

結果、SQLのカラムを追加するだけで既存処理への影響はないとわかり、安心して進めることができました😊

# SQLの修正とJava側の対応

ここからSQLの具体的な修正案を考えていくフェーズになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/bf3fca6d-fadf-4327-8d05-4b150e32b5c4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/703c6c51-6ac5-4059-b7c2-6cf31430079d.png)

追加するカラムの実装イメージを出してくれました。
次のステップを確認したところ良さそうだったのでこのまま進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/d608298a-ebc1-4e96-a646-5e6896979f51.png)

PodDBAdapter.getFeedStatisticsCursor のSQLクエリに
「6ヶ月以内に未再生エピソードが1つでもあるか」を判定する has_recent_unplayed カラムを追加してくれました。
新しい引数 sixMonthsAgo を追加し、Java側から値を渡せるようにしてくれました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/9dad18c0-50a5-4d13-afa4-bb5a6a5ba8a7.png)

Java側で受け取る処理を書いてくれました！期待通りです！

---

ただ、生成されたコードに不明点が残るのは避けたいので、率直に聞きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/6d0bae08-3564-4184-ada7-8f4dff1b212b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/ca9d825d-6852-4158-865b-80104b23eb61.png)

なるほど、これは後方互換用のコンストラクタとのこと。
既存コードを壊さずに、新しいフィールドを導入するための配慮だとわかりました。勉強になります。

ということで修正をpushしてレビューも完了！
これで、**アプリの端末DB参照時のパフォーマンスはかなり改善されたはず**です。



:::note
ここまでの実装まとめ：
SQLクエリ内で EXISTS を用いたサブクエリを使い、「各番組に、6ヶ月以内の未再生エピソードが存在するか」を判定し、has_recent_unplayed カラムとして返す形に。
:::

# さらなる改善提案（AIを超えた知見！）

しばらくして、メンテナーからさらに有益なコメントが届きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/65d91cb8-1208-40ae-928c-c4c556cd60e5.png)

> 翻訳
> このサブクエリを使う場合、おそらくテーブルを自己結合する必要があるでしょう。SUM(CASE WHEN (<read> AND <recent>) THEN 1 ELSE 0 END) のような式を使えば、追加のテーブルスキャンをせずに「最近のエピソードの数」を求められるはずです。

:::note info
非常にありがたいご指摘です。
メンテナーのおっしゃる通りで、サブクエリ（EXISTS）を使うと、番組（Feed）ごとに毎回エピソード（FeedItems）のテーブルを参照しに行くことになってしまいます。

一方、番組とエピソードをJOINした上で、すべてまとめて「条件に合う行だけを数える（SUM(CASE WHEN ...)）」ようにすれば、1回のスキャンで済むためパフォーマンスが大きく改善されます。
:::

この指摘、実はCursorで自動生成されたコードには含まれていませんでした。
**つまり、AIを超えた知見と言えるでしょう！**

---

さっそく修正に取り掛かり、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/a00c8e4b-f62e-4300-9f55-0f83e4f34d36.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/10f6d4dc-4d8f-40f1-84a1-38205fba7967.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/7a13d6a6-3e4d-48e4-a0a7-e0e2140d20a1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/611b95a3-8cf0-409f-a866-165c2e0d227f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/00e5cf8f-a0d0-47a4-ae55-64494aa29f0d.png)

実装完了！再度pushした結果、無事マージされました🎉

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/a136dea1-57b5-42e3-8258-2cc9cb014ff0.png)

# おわりに
AIと人間の知見が補完し合うことで、はじめてのコントリビュートとしては非常に学びの多い体験になったと感じています。
「バイブコーディング × OSS」の可能性を、改めて実感できた今日この頃です。

もしこの内容が面白かったら、前編もぜひどうぞ！

https://qiita.com/ryotab22/items/42f51aaf423de69b1dc3

今回お世話になったメンテナーの ByteHamster さん

https://github.com/ByteHamster