No Solution for Life

プログラマを目指すアラサーが、学んだことを初心者(=昨日の自分)にわかるように書き綴ります。

『達人に学ぶDB設計 徹底指南書』輪読会をやった

達人に学ぶDB設計 徹底指南書

達人に学ぶDB設計 徹底指南書

2020年の年末から、フィヨルドブートキャンプ内で『達人に学ぶDB設計 徹底指南書』の輪読会をやった。

Slack で輪読会の話が出たときに、DB関係の本を読みたい!と言ったところ、フィヨルドブートキャンプのアドバイザーであるベテランエンジニアの方がメンターをやってくださることになり、開催に至った。

初学者で集まって読むのも楽しく一人で読むよりは知識も深まるが、その分野の知識や経験が豊富な方に参加してもらえたことで、さらに何倍も有意義な会になったと思う。

ものすごく勉強になったのを独り占めするのはもったいないのと、自分の復習のためにも特に印象に残ったところだけでも書き残しておくことにしたい。

学んだこと〜データベースを制する者はシステムを制す

データベースは事実を保存するもの

プログラムと比べ、データはあまり変化しない。というか、そう設計されるべきだ。データ設計(データベース設計)は、プログラム設計に先立って行う。

たとえば、ユーザーの氏名(事実)が変わることはあまりないが、名前の表示の仕方(プログラム)を変えたくなることはよくある。

名簿はフルネームだけど、担当者名のところには苗字だけ表示させたい、とか。

可能な限り NOT NULL 制約をつける

事実を保存する前提に立つと、基本的にNULL(データが空っぽ)となることはない。 NULLを許容したくなったときは、データ設計自体を見直した方がよい。

たとえば、以下のような社員テーブルを考える。

社員ID 年齢 部署
200500123 山田 太郎 45 総務
201000111 高橋 花子 36 広報
202100567 鈴木 一郎 20 ???

今年入社したばかりの鈴木さんは、研修後に配属が決定されるので、入社日時点では部署が決まっていない。

NULLを許容しないとなると、部署が決まるまで鈴木さんを社員テーブルに追加することができない。

こういうときは、このテーブルの設計自体がまずい可能性が高い。

社員テーブルに「部署」を入れる必要があるのか?配属先が変更になったら?複数の部署に配属される可能性はゼロか?

ということを考慮すると、「配属」というイベントが存在していることに気づく。

部署テーブル

部署コード 部署名
010 総務
020 広報
以下略

配属テーブル

配属日 終了日 社員ID(外部キー) 部署コード(外部キー)
2005-06-01 2011-3-31 200100123 070
2011-04-01 2018-03-31 200100123 050
2018-04-01 200100123 010
2010-06-01 2015-03-31 20100111 030
2015-04-01 20100111 020

現在進行中のところの終了日はNULLを入れたくなるからまた設計がおかしいのか…

と思うかもしれないが、こういうところには最大値を入れておくというテクニックがある。「事実」ではないが、つっこまないで…。

未来に起こる「かもしれない」ことを全て考慮するのは難しく、NULLを許容することが絶対に悪いということではない。

何かを優先すれば何かが犠牲になる「トレードオフ」の世界なので、部署を未定で登録しておくケースもあるかもしれない。

何を優先するかによって、考え方・正解がたくさんある世界だという点も大事。

可逆 / 不可逆を考える

上の社員テーブルでは、社員名の姓と名を別々のカラムにした。

データはできるだけ分解した方がいいというのはなんとなくわかるが、姓だけ、名だけでは誰かわからないので意味が失われてしまうのではないか?

カラムを分けるか、テーブルを分割するか考えるときは、可逆的か(元に戻せるか)どうか?を考えてみるとよい。

姓・名を別々のカラムにしておいたとして、フルネームが必要なときは、2つのカラムのデータを単純にくっつければいい(可逆的)。

逆に、氏名という1つのカラムに「山田太郎」と保存していた場合、社員の姓だけが必要になったときに、プログラムで姓だけを取得することは不可能。 絶対にフルネームでしか使わないということであればいいのかもしれない。

ちなみに、「年齢」はあまりよくなさそう。誕生日が来たらいちいち更新するのか?「生年月日」にしておけば、年齢はプログラムで求めることができる。

正規化は絶対ではない

データベースの勉強をすると必ず出てくる「正規化」という言葉。

ざっくりいうと、冗長性(重複)をなくしデータの不整合を防げるように整理すること。

正規化にはいくつか段階があり、正規化を進めれば進めるほどデータ整合性を高められるが、検索性能は劣化する。ここもトレードオフ

正規化するのが絶対に正しいというわけではなく、パフォーマンスを優先してあえてデータを冗長に保持することもある。

たとえば、Twitter の「いいね!」数は、ある特定のツイートに対し誰が「いいね!」しているかをデータベースに問い合わせて計算すれば求めることができる。ただ、何万もの「いいね!」がつくことも珍しくない Twitter で、一つ一つのツイートに対してそんな処理をやっていると、結果が表示されるのに時間がかかったりデータベースに負荷がかかったりする。

こういう集計データ(サマリデータ)は、計算後の結果を別途保持しておくことによって、パフォーマンスを上げることができる。

トレードオフを理解した上での非正規化は、アンチパターンではない。

ER図をいきなり描くのは難しい

フィヨルドブートキャンプでは、Twitter の ER 図を描くという課題がある。苦戦した過去の自分へのアドバイスをいくつか。

とにかく、まずは具体的なデータを置いてみること。どういう形でもいいから、具体的な例を考えてデータの置き場所を作ってみること。

エクセルのような表を作って、セルに複数のデータが入ったりしてもいいから、とりあえず書いてみる。

その後、正規化したりリレーションシップを考えたりしていく。いきなり ER 図を描くのは難しい。

この本では、2種類の表記方法が紹介されているが、ググってみると、いろんな描き方をしている人がいて、どれが正しいの?となる。

ER図に関しては、厳密にどれが正しいというより、伝わることが一番大事。

主キーの考え方がRailsとは違う

この本を読むときに注意が必要なのが、主キーの考え方。

主キーの考え方には、ナチュラルキー(自然キー)とサロゲートキー(代理キー)というものがある。

参考:「ナチュラルキー」と「サロゲートキー」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

ナチュラルキーは、事実を(必要に応じ組み合わせて)主キーにするのに対し、サロゲートキーは、事実とは関係のない番号などを人工的に主キーとしてつけるもの。ナチュラルキーの例は、上の社員テーブルでいうと「社員ID」や「部署コード」。これは、プログラムのためにつけたものではなく、現実世界に存在するID(コード)。

この本は、ナチュラルキーを使うべきで、サロゲートキー(代理キー)は推奨しない立場で書かれている。詳しくは本の「8-2 代理キー〜主キーが役に立たないとき」を参照。

一方、Rails はデフォルトでサロゲートキーを使うようになっているので、この立場の違いを知らないと理解が難しいところが多々ある。 自分も1回目に一人で読んだときはそれを知らず、理解できないところが多かった。

現在、現場ではサロゲートキーが使われることも多いようだ。

サロゲートキーは連番か

ちょっと話は逸れるが、サロゲートキーを連番にするかランダムな数字にするかという話題で興味深いツイートを教えてもらったのではっておく。

たとえば、ユーザーIDが連番だと、登録した人が「全ユーザーが何人いるか」わかってしまうので、あえてランダムな数字にすることもよくある。

難しい用語にこだわらなくていい

この本では、部分関数従属とか推移的関数従属のように難しい用語がいくつか出てくる。

現場ではあまり使わないようなものもあるので、細かい用語を一つ一つ覚える必要はない。難しい言葉は、ざっくり理解して次出合ったら覚えるぐらいでよさそう。

輪読会の概要

毎週土曜日22時〜23時に Discord で開催し、全13回。

参加者は、多い日は20人弱、少ない日は3人ぐらい。

項目ごとに5〜10分の時間を取って各自読みながら疑問点などを HackMD に書いていき、時間が来たら意見交換を繰り返す流れで進行した。

第7〜8回は、参加者を2チームに分けてモブプロっぽく ER図の作成会をやった。

参加してくれた皆さん、bluerabbit さん本当にありがとうございました。

『トヨタの失敗学 「ミス」を「成果」に変える仕事術』を読んだ

どんな本か

「失敗=悪」。これが世間の常識です。だから誰もが「失敗などしたくない」と思いながら仕事をしています。 「失敗したら上司に叱られる…」 「 問題を起こせばまわりに迷惑をかけてしまう…」 「 失敗したら責任をとらされる…」

このような事態が想定できるからこそ、 「失敗しないように無難に進めよう」 「失敗したら、バレないように隠してしまおう」 「いざとなったら力技でごまかそう」 という発想になってしまいます。しかし、「失敗は避けなければならないもの」という意識でいるかぎり、失敗から目をそらす姿勢が強くなります。そして同じような失敗を繰り返すことになり、事態は悪化していきます。また、新しいことや困難なことへのチャレンジにしり込みするので、職場のチームも自分自身の成長も減速します。

トヨタの現場の第一線で活躍してきたトレーナーたちが、口をそろえて証言していることがあります。 「トヨタの現場では、たくさんの問題やトラブルが起きる。でも、『失敗』という言葉はほとんど聞かなかった」 もちろん起きた現象だけをとらえれば、トヨタにもミスやトラブルなど大小さまざまな失敗がありますが、少なくとも現場レベルでは「失敗」という概念は存在しません。失敗したことをそのまま放置したら、それは文字通りの「失敗」に終わってしまいます。しかし、失敗に正面から向き合い、次に活かすことができれば、その失敗は改善プロセスのひとつとなります。「失敗」が「失敗」で終わらないのです。

現場のメンバーが知恵を絞って、「なぜ不良が起きたのか」を徹底的に考えます。そして、問題を引き起こしていた原因を突き止めて、二度と不良が出ないような対策を実施し、そのしくみは、他のラインや工場にも展開することで、組織として強くなっていくのです。

つまり、トヨタの現場では、問題やトラブルをよりよいモノづくりをするための「改善の機会」ととらえているのです。 トヨタの現場で働く人たちにとって「失敗」とは、改善へとつなげるチャンスであり、成果に結びつける「宝の山」です。 (はじめにより)

Amazon.co.jp より

所感

いろんな職場で使えそうな実践的な内容だった。上の抜粋を読めば大事なところがなんとなくわかるかもしれない。

「失敗=悪」ではない

失敗は改善のタネで宝の山。これが一番大事だと思った。今まで、失敗についてじっくり考えたことがなかったが、当然のように失敗は悪いことだと思っていた。

できれば失敗は避けたいし、もし失敗したら隠したい。そう思っていた気がする。

この本を読んで失敗は悪ではないとわかっても、実際に仕事をする中で、失敗を隠したい気持ちをゼロにできるだろうか?

自分の無知やミスをさらけ出しても責められたりバカにされたりすることがない、心理的安全性が確保されている職場なら、可能だと思う。この本の中で「心理的安全性」という言葉自体は出てこないが、トヨタでは、工場で異常を見つけてラインを止めた作業員を誰も責めたりすることはなく、むしろ奨励されているという。そうやって問題を小さいうちに発見し、改善を続けていくことが大事なのだと知った。

心理的安全性が低く、失敗したとき個人が責められるような職場だと、失敗を宝の山と考えるのは難しいだろう。先日読んだ 不機嫌は罪である (角川新書) という本にも似たようなことが書いてあったのを思い出した。みんながニコニコして明るい職場は、離職率が低くなり業務効率が上がるそうだ。Googleによる調査で、生産性の高い職場にもっとも重要な要素が「心理的安全性」とされたことにも触れられていた。

心理的安全性が高いと生産性が高まると聞いて、メンタルが仕事の生産性にまで影響を与えるのかと漠然と考えていたが、今回『トヨタの失敗学』を読んで、生産性が高まる理由がわかった。それは、失敗を隠すことがなくなるだけでなく、日頃からさまざまな情報が共有され、ちょっとした気づきの段階で失敗の芽を摘み取ったり改善したりしていくことが可能となるからだろう。

エンジニアは、気をつけていてもバグを一度も出さない人はいないので、この「失敗学」の考え方はとても大事だと思った。

失敗から学び、改善してよりよくすることができれば、それはもはや失敗ではなく成長の1ステップ。

大事だと思ったことをメモ

  • 「失敗=悪」ではない
  • 失敗を放置したら文字通りの「失敗」になってしまう
  • 失敗は宝の山、改善のタネ
  • 失敗したら真因を追求する
    • 何が起こった?
    • なぜなぜ5回
  • 失敗しても個人の責任にしない
    • 真因ではない
    • 失敗が隠されるようになる
    • チャレンジしない組織になってしまう
  • 「人はミスをするもの」という前提で失敗しないしくみをつくる
  • 小さい問題は繰り返されたり、後で大きい問題になったりする
  • 問題が小さいうちに、自らの意思で問題を明るみに出す
  • よその失敗を自分ごとととらえて対策を講じる
  • 明るい職場は「失敗」が隠れない
  • あきらめずに前進を続ければ「失敗」にはならない

大人の勉強法について考えた

学生の頃の勉強といえば、覚えるものだった。覚えなければテストで点数が取れないので、きちんと理解して覚えるようにしていた。

大人の勉強は覚えることが最終目標ではないと気づいたのが自分はかなり遅かったので、ブログに書いておくことにした。ここでいう大人の勉強というのは、プログラミングとかその周辺の話。

大人の勉強の目的は未来の自分へのカンニングペーパーを作ること

Webエンジニアが仕事をするときを考えてみると、学生の頃と違ってググってOK、カンニングし放題なので、仕事に生かすための勉強をするのであれば、一度理解したことをいかに早く正確に思い出せるようにしておくか、が一番重要だと思う。

つまり、大人の勉強とは、対象を理解した後に未来の自分へのカンニングペーパーを作ること、そして必要なときにすぐにそれを出せるよう整理しておくことだと思うに至った。

未来の自分を信用しないことも大事で、「さすがにこれぐらいは覚えていられるだろう」というような基本的なことも、迷ったら書いておいた方がいい。その後学んだ他の言語とごっちゃになってわからなくなってしまうかもしれないから(経験談)。

個人的にScrapboxはおすすめ

メモツールはいろいろあるので何でもいいと思う。自分が愛用しているのは Scrapbox

scrapbox.io

フォルダ分けがなく階層構造を持たないことや見出しなどが作れないことにはじめはびっくりしたが、これが非常に使いやすい。書き出すまでの時間がゼロ。そして検索やページ間リンクもしやすい。もっと早く使っとけばよかった!と思っている。そういえば前にも書いた気がする。

masuyama13.hatenablog.com

書くハードルが低すぎる分、後から見ると意味不明になってることもあるけど、なんかの拍子にそういうのを見つけて修正したりするのも面白い。

公開プロジェクトにしておけば共有も楽だし、意外な場面で話題になったりすることも?

最近は技術的なことに限らず、何でも放り込んでいる。

タブ開きすぎ症候群にも効くかも

それから、自分の場合は、Scrapbox を使い始めてから、「気づいたらブラウザのタブを大量に開きっぱなしにしてしまう問題」も解決した。後から見返したいようなページであれば Scrapbox にリンクを張ったり必要な部分をメモしたりすることで、また見る「かもしれない」タブを閉じることができる(検索で上位に来ない公式ドキュメントのページなど)。検索すればすぐにたどり着けるようなページならそこまでしなくてもいい。

情報の粒度みたいなところも気にしなくていいからいいのかもしれない。ほんのちょっとしたメモからブログみたいな長文まで、内容も問わず何でも放り込めるところが好き。

こういうの(WebページをMacアプリっぽくする - masuyama's notes)を使うと、アプリっぽく使うこともできたりする。

今のところ、自分にとって Scrapbox 最強。これがないと多分何もできない人になりつつある。(バックアップ取っとかないと…)

masuyama's notes

間違いを見つけたら教えてください〜

「【iCARE Dev Meetup #18】技術顧問が語る、Ruby on Rails実践開発」に参加

2月17日、オンライン開催された【iCARE Dev Meetup #18】技術顧問が語る、Ruby on Rails実践開発 - connpassに参加した。

所感

RailsバージョンアップやSQLなど興味のある話題が多かったので参加してみた。

Railsバージョンアップを実践してみて」では、テストの大切さを再認識した。バージョンアップの難しさをあまりイメージできていなかったが、Gem同士の依存関係の具体的な話もあり、作業のイメージができた。思っていたより大変そう。やはり、テスト大事。

SQLActiveRecordについて」は、SQLをゴリゴリ書いていた人がRailsに入門するときに、SQLを組み立ててからそれをどうRailsで表現するかという内容。例がこのMeetupの参加者や登壇者だったのでわかりやすかった。

自分の場合、SQLの理解が浅くActiveRecordに頼りっきりだが、SQLをわかっている人の考え方を知るのは興味深かった。ActiveRecordは便利で、SQLを知らなくても大概のことができてしまうが、SQLを意識せずにいるとパフォーマンスが悪くなってしまうことがあるので、SQLについても勉強したいと思った。

「HotwireからDHHが考えるこれからのRailsとJSとの付き合い方を知る」では、TurboとStimulusの詳しい話を聞くことができた。Stimulusは、「控えめなJaveScriptフレームワーク」としてパーフェクト Ruby on Rails 【増補改訂版】に載っていたので理解していたが、Turboについてはまったく知らなかったのでとても勉強になった。

Hotwireを使うと、ロジックをサーバーサイド側に集中させ、フロントエンドをわかりやすく書けそうだと思った。モバイルにも一応対応できるようなので、今後使われることが増えるかも?

全体的に勉強になったので復習したいが、発表スライドを1つしか見つけられず残念。 YouTube にアップされていたのでリンクをはった。

内容メモ

Railsバージョンアップを実践してみて

youtu.be

バージョンアップ前の状況

  • Gem更新など3年ぐらい止まっていた
  • 新機能開発が優先になっていた
  • 使用しているGem、150以上
  • 管理画面側はテストが少なく人力で確認しないといけない状態

バージョンアップ

  • Railsガイドがわかりやすかった
  • Change log
  • dependabotの導入
  • CIを通す
  • Gemをアップデート
    • Gem同士の依存関係によりスムーズにいかないことも多い
  • bundle outdatedで確認
    • 使っていないGemは削除
  • 本番環境に影響が出るものは1個ずつ

まとめ

  • テスト大事!
  • 不要なGem入れない
  • 使わなくなったら削除する
  • メンテナンスされているGemを使う

Rails APIモードにおけるToken認証機能について

youtu.be

認証Tokenをどこに保存するか?

  • LocalStorage
    • 実装が簡単
    • XSS脆弱性があった場合、Tokenを容易に盗むことができる
  • Cookie
  • In-memory
    • JavaScriptでブラウザのメモリ内に保存する方法
    • 永続化されない(リロードすると消滅)
  • Auth0のSilent Authentication
  • それぞれの目的や扱う情報に応じて選ぶ

SQLActiveRecordについて

youtu.be

SQLからActiveRecordの使い方を考える。

  • Meetupと参加者、登壇者を例にしたER図
  • SQLを組み立ててからRailsのコードにする

HotwireからDHHが考えるこれからのRailsとJSとの付き合い方を知る

youtu.be

Hotwireは、Basecamp社製のjsフレームワーク。複数のフレームワークから構成されている。

“最小限の労力でユーザが求めているサービスを提供すること”に特化しているライブラリ。Railsと同じ方針をフロントエンドにも持ち込んだものといえる。小〜中規模のサービスに向いている。

Hotwireを使うと、

  • サーバサイドにロジックを寄せることができる
  • クライアントサイドのコードは最小限におさえることができる
  • それでいて、それなりにSPAができる
  • 結果として
    • ひとつのチームですべてを担当できる(かも)
    • 好きな言語を使える(Rails以外でも使える)
  • VueやReactと比べると細かいことはできない

Hotwireのアーキテクチャ

  • Turbo
    • Turbo Drive(Turbolinksを改善したもの)
    • Turbo Streams
    • Turbo Frames(HTMLの一部を差し替えることができる)
      • 遅延評価も可能(loading="lazy")
    • Turbo Native(iOSAndroidでTurboを使うためのライブラリ)
  • Stimulus
    • 学習コストが低い
    • ファイルが自然と整理される
    • 特定のページだけで発火させたいjsが書ける
  • Strada(未発表)

Turbolinks

  • 全てのリンクをAjaxに置き換えるライブラリ
  • E2Eテストやformの挙動がわかりにくい
  • ハマる人が多く、基本消されるライブラリに...

(参考)TurbolinksからTurboへの移行 - おもしろwebサービス開発日記

Gemfile に書く require: false とは何か

久しぶりに Rails アプリを1から作るにあたり、Rubocop を導入しようとしたらいろいろと疑問が出てきたので調べた。

Rails アプリに Rubocop を入れる文脈で、こんな書き方をよく見かける。

Gemfile

gem 'rubocop', require: false
gem 'rubocop-rails', require: false

今さらだけど、require: falseって何だったっけ??

require をしないようにする設定っぽいが、そういえば、Rails アプリの場合各ファイルで require しなくていいのはなぜ?

手元にあった RubyRails の本を4冊見てみたところ、Bundler の基本的な使い方などはどの本にも書いてあったが、Bundler を使うとrequireがいらなくなるなんていう記述はない。

Bundler のドキュメントを読んでみる。

Gemfile の require についての項目。

Each gem MAY specify files that should be used when autorequiring via Bundler.require. You may pass an array with multiple files or true if the file you want required has the same name as gem or false to prevent any file from being autorequired.

gem "redis", :require => ["redis/connection/hiredis", "redis"]
gem "webmock", :require => false
gem "byebug", :require => true

The argument defaults to the name of the gem. For example, these are identical:

gem "nokogiri"
gem "nokogiri", :require => "nokogiri"
gem "nokogiri", :require => true

Bundler: gemfile #Require As

なるほど〜

gem 'rubocop'は、gem 'rubocop' require: 'rubocop'gem 'rubocop' require: trueと同じだったのか!

で、引用の前半部分。

requireにファイル名の配列かtrue(ファイル名がGem名と同じ場合)を渡すことで、Bundler.requireで自動読み込みするファイルを Gem ごとに指定できると書いてある。自動読み込みしないようにするにはfalseを渡す。

やっぱり自動読み込みしてくれるっぽい。でもBundler.requireなんて書いた覚えはない…。Rails のことだから自動でやってくれてるのかな?

これもドキュメントに書いてあった。

Bundler makes sure that Ruby can find all of the gems in the Gemfile (and all of their dependencies). If your app is a Rails app, your default application already has the code necessary to invoke bundler.
For another kind of application (such as a Sinatra application), you will need to set up bundler before trying to require any gems.

Bundler: How to use Bundler with Ruby #Setting Up Your Application to Use Bundler

前半を訳してみるとこんな感じだと思う。

「Bundler は、Gemfile の中のすべての Gem(とそれが依存するもの全部)を Ruby が見つけられるようにします。Rails アプリならデフォルトで Bundler を呼び出すのに必要なコードが含まれています。」

やっぱり!!さすが Rails

Sinatra アプリなどで、Gemfile 内の全Gem を自動読み込みさせる設定は以下とのこと。

require 'rubygems'
require 'bundler/setup'
Bundler.require(:default)

手元の Rails アプリのコードをよく見たら、ちゃんと書いてあった。


さて、ようやく本題。

require: falseを指定すると、自動読み込みしないようにする。

Rubocop は通常コマンドで使用するもので、Rails アプリの中から Rubocop のメソッドを呼び出すことはない。ということは自動読み込みさせる必要がないので、require: falseにすればよいということ。

プログラミング学習中の人が稼働中のシステムに不具合を発生させた話

これは、フィヨルドブートキャンプ Advent Calendar 2020(Part 1) 12日目の記事です。昨日は、ksm さんの プログラミング学習サービスに参加して100日経過した話 - improve.design でした。 フィヨルドブートキャンプ Advent Calender 2020(Part 2)もあります。

アドベントカレンダーというのは、12月1日からクリスマスまで、特定のテーマに沿った記事を公開していくという企画です)

参考:アドベントカレンダー - Wikipedia

先日、私の書いたコードが原因でフィヨルドブートキャンプの学習システムに不具合を起こしてしまいました。そのときの状況や学んだことについて書きます。

読んでもらいたい人

  • フィヨルドブートキャンプで勉強中の人、関係者の皆さん
  • フィヨルドブートキャンプに参加するか迷っている人
  • フィヨルドブートキャンプではどんなことをやっているか知りたい人
  • 読みたい人!

フィヨルドブートキャンプのチーム開発

私は、2019年の12月から約1年間、FJORD BOOT CAMP(フィヨルドブートキャンプ) というプログラミングスクールで勉強しています。カリキュラムの終盤に「チーム開発」があり、それまで自分がずっと使ってきたフィヨルドブートキャンプの eラーニングシステムを開発することになります。

毎週、メンター&チーム開発参加者で振り返りミーティングと計画ミーティングを行い、成果物のデモ、Issue の割り当て、プランニングポーカーなどを行って、20ポイント分マージされたら終了です。

コードはオープンソースなので誰でも見ることができます。

GitHub - fjordllc/bootcamp: プログラマー向けEラーニングシステム

不具合の内容と対応

2週間ほど前に、このチーム開発での私の PR が原因で本番環境で不具合を起こしてしまいました。

アサインされたのはこの Issue。

自分の日報・提出物・イベントをWatchすると通知が二重になる · Issue #2013 · fjordllc/bootcamp

クリック

Issue の説明を書いたら長くなってしまったので、気になる方だけここをクリックして読んでください。

フィヨルドブートキャンプ参加者は、日々ブートキャンプアプリ上で日報や提出物を提出することになっているのですが、それにコメントがつくと、アプリ上で通知されるようになっています。これまでは、日報・提出物の作成者とコメントした人とが異なる場合に、日報・提出物作成者に通知されるようになっていました。

それとは別で、このアプリには「Watch」という機能があります。誰が作成したかにかかわらず、日報や提出物のコメント通知を受け取れる機能です。人の日報などにコメントすると自動で「Watch中」になり、その後のコメント通知が受け取れるようになります。

便利なのですが、自分の日報などにコメントがきて返信したりすると、自分の日報・提出物が「Watch中」になります。つまり、その後の通知は二重になってしまうという問題が発生していました(アプリ上での通知は1件にまとめられるが、メール通知は複数くる)。

ということで、コメント通知を「Watch」だけにして、これまでの「日報・提出物の作成者とコメントした人とが異なる場合に、作成者に通知」はしないようにしたいというIssue が立てられました。

(イベントも含まれますが、今のところイベントを作成した受講生がいないので記載を省略しているところがあります)


私が作成した PR。

日報・提出物・イベントのコメント通知をWatchに変更 by masuyama13 · Pull Request #2104 · fjordllc/bootcamp

この PR でやりたいことは2つありました。

  • ユーザーが日報・提出物・イベントを作成したら、自動で「Watch中」にすること(問題なし)
  • 既存の日報・提出物・イベントについて、作成者自身が「Watch中」となるようにすること(問題発生!)

問題のコード

既存の全ての日報・提出物・イベントの1件1件について、作成者自身のWatchデータを検索、なければ作成するRakeタスクです。ローカルでは問題なく動き、コードとして間違いというわけではありませんが、データベースへのアクセスが多すぎて時間がかかり処理に失敗するという結果になりました。

ActiveRecord::Base.transaction do
  Report.includes(:user, :watches).each do |report|
    Watch.find_or_create_by!(user: report.user, watchable: report)
  end
  Product.includes(:user, :watches).each do |product|
    Watch.find_or_create_by!(user: product.user, watchable: product)
  end
  Event.includes(:user, :watches).each do |event|
    Watch.find_or_create_by!(user: event.user, watchable: event)
  end
end

コードを書いた後に、全部eachで回すと結構重いのかも?という疑問が一瞬頭をよぎりましたが、 1回きりの処理だし全部で数千件ぐらいだからまあ大丈夫かな、includes を使えばOKだろうぐらいに深く考えませんでした(実際には日報だけで2万件以上ありました)。

結果として、既存の日報などのうち作成者本人がWatch中でないものにコメントがついても、通知されないという不具合が発生しました。

対応

1日目

BULK INSERT を試してみてとアドバイスいただいたので、まずそこから調べることに。言葉を聞いたことがある程度で、具体的なやり方など何も知らない状態でした。

調べた結果、insert_allというメソッドが使えそうだと思いました。

ActiveRecord::Persistence::ClassMethods

ローカル環境でいろいろ動かしてみて、全部の日報などを各作成者が「Watch中」にすることはできました。しかし、「そのデータが既にあれば作らず、ない場合だけ作る」という条件分岐的なところがわからず詰まりました。いくつか解決策案を考えてみたものの、自分の実力では実現可能か調べるだけでかなりの時間が必要な中で、方向性が全くわからず不安になりました。

N+1 問題に関しては、これまでBullet(N+1を検知するGem)頼みだったため、コードから読み取る力が自分にはありませんでした。SQL入門書を読み直したり情報収集したりしましたが、この日は結局わかりませんでした。

2日目

これ以上時間をかけても自分の力だけで解決するのは難しいと思い、フィヨルドブートキャンプの Slack の wakaranチャンネルで状況を流しながら作業を進めることにしました。

wakaran チャンネル
わからないことを雑につぶやくチャンネル。Macの使い方から自作サービスの設計の迷いポイントでもなんでもOK!初歩的な質問大歓迎です!

wakaran チャンネルは、「質問するまでのハードルが高い」という受講生の声を受けて数ヶ月前にできたチャンネルで、質問にならない「wakaran」を気軽につぶやけるすばらしいチャンネルです!

状況を書き込むと、すぐにアドバイザーの @udzura さんがアドバイスくださいました。問題を分割することや、やりたいことをまず日本語できちんと整理することなど、問題に対処するときに大事なことをたくさん教えていただきました。

おかげで少しずつ進むことができたのですが、夜中まで頑張っても答えまでは出せませんでした。

3日目

次の日起きたら、今度はメンターの @jnchito さんからアドバイスが届いていました。具体的なコードだけでなく、メンタル的な部分(励ましの言葉など)、実際の現場での対応方法、コードもバルクインサートを使わないバージョン・使うバージョンなどものすごい情報量でした!(フィヨルドブートキャンプ生なら私の日報のリンクから見られます)

数時間後、なんとか PR ができ(ほとんど伊藤さんのコードそのままですが)、マージ。不具合解消となりました。

作成したPR。

既存の日報・提出物・イベントについて、作成者自身をWatch中にする by masuyama13 · Pull Request #2163 · fjordllc/bootcamp

ポイントとしては、テストを書いたことです。伊藤さんから、「できればデータ移行のロジックはモデルに移して、テストを書くことをオススメします。」とアドバイスいただいたのでやってみたのですが、実際テストがあると安心感が全然違いました。

今回の問題はテストがあれば防げたということではありませんが、1回きりの処理だからといってテストを書かなくていい理由にはならないので、やってみてよかったです。

学んだこと(技術面)

バルクインサートとは

今回のように DB に対し何万回もアクセスすると、パフォーマンスが悪くなり処理に失敗することもある。 そこで、大量のデータを1回の命令(問い合わせ/クエリ)でINSERTするのが BULK INSERT。

作業の過程で学んだことメモ

Report.joins(:watches).to_sql
=> "SELECT \"reports\".* FROM \"reports\" INNER JOIN \"watches\" ON 
\"watches\".\"watchable_type\" = 'Report' AND \"watches\".\"watchable_id\" 
= \"reports\".\"id\""

再発防止策案

  • 今回のPRではやりたいことが2つあった。ロジック変更とデータ移行をそれぞれ別のPRにして、データ移行を先行させる
  • 不具合に気づいた時点で一旦 revert する

今回のことを受けて、不具合発生時の対応についてドキュメントが作成されました。不具合は発生しないのが一番ですが、何かあったときにフローがあれば少しは落ち着いて行動できそうです。

学んだこと(技術以外の面)

Working Out Loud(大声作業)の大切さ

前にも一度書いたことがある Working Out Loud。フィヨルドブートキャンプの Slack で紹介されていて知ったものだったと思います。

masuyama13.hatenablog.com

  • 作業が途中であってもチームメンバーの目の触れる場所にガンガンアウトプットする
    • Slackなどのチャットツールで実況中継しながら作業したり
    • 設計やコーディングがWork in progressな状態でもフィードバックを募ったり
  • 作業で詰まったらとにかく尋ねる
    • 簡単に手伝ってもらえるのに1人で行き詰まっているというのはプロの振る舞いではない

Working Out Loud 大声作業(しなさい)、チームメンバー同士でのトレーニング文化の醸成 - Quipper Product Team Blog

他業種から転職を目指す私のような人間から見ると、信じられないような考え方かもしれません。それまでの人生経験によるところが大きそうですが、記事を読んだだけで簡単に実践できるようなものでもないと思います。

でも、エンジニアとして仕事をするなら大事なスキル。私も、最初知ったときはなかなかできませんでしたが、フィヨルドブートキャンプという心理的安全性の高い環境に長くいたおかげで、だんだんできるようになってきました。

今回、自分だけの力では無理だと思ったとき、すぐ wakaran チャンネルで作業した点はよかったと思います。毎日オンラインで行われている「質問・雑談タイム」(メンターのプロのエンジニアの方に質問できる時間)にも参加し、たくさんの方に助けていただきました。

技術的な面だけでなく、「実際の現場でもよくある話だから焦らず落ち着いて対応しましょう」といったアドバイスもいただき、ネガティブにならずに問題の解決に集中することができました。「一人でやってる感」が全くなかったです。

フィヨルドブートキャンプの学習システムでも「分報」機能が実装される予定なので、トラブルになる前から Working Out Loud したらもっとよさそうです。

自分の手がける問題について、「聞きまくれる相手」がいる、というのはスキルの一部だ。
(イシューからはじめよ)

ある意味、フィヨルドブートキャンプはお金を払うだけで「スキル」が手に入るスクールですね(怪しい響きですが、アフィリエイトは一切やっておりません)。

もし FJORD BOOT CAMP(フィヨルドブートキャンプ) に興味を持った方がいたら、冒頭にもはった ksm さんの昨日の記事がいい点・イマイチな点など詳しく書かれていて参考になると思います。まだの方はぜひ読んでみてください〜

そして輪読会開催へ

あらためてデータベースについて学ぶことの必要性を痛感したので、今月末から 達人に学ぶDB設計 徹底指南書 の輪読会を始めることになりました。フィヨルドブートキャンプの参考書籍でもあり、以前さらっとは読んだのですが、難しすぎてほとんど記憶にない…。

第1章は「データベースを制する者はシステムを制す」

そういうことらしいです。システムを制したい人、一緒にやりましょう!

RubyMine をコマンドラインから開く(Mac)

先日、RubyMine を導入した。

ちょこっとしたコードのときは VSCode を使うことが多いとはいえ、VSCodecode コマンドみたいにターミナルから開けるようにしたい。

調べたところ、RubyMine のヘルプに書いてあった。Mac の場合、自分でシェルスクリプトを置く必要があるとのこと。

コマンドラインインターフェース - ヘルプ | RubyMine

#!/bin/sh

open -na "RubyMine.app" --args "$@"

ヘルプではスクリプト名を rubymine としているが、長くない…?

うーん、ruby はだめだし、rm もかぶる…。とりあえず mine にしとくか。

これで、開けるようになった!

# カレントディレクトリを RubyMine で開く
% mine .

シェルスクリプトの作り方について詳しくは以下の記事を参照。

masuyama13.hatenablog.com