patorashのブログ

方向性はまだない

5月になってやっていること

4月のことを、この前書いた。 patorash.hatenablog.com

もう3週間くらい経ってしまった。なかなかブログを書く時間も取れないほど、日々は慌ただしい。主に、家庭面で。まぁ岡山県も新型コロナの影響で緊急事態宣言が出てしまい、それもあって幼稚園が登校自粛になったりなど、色々あった。

まぁそれは置いといて。

5月になって取り組んでいること

前回の反省点を踏まえて、5月に取り組んでいることを並べる。

  • 週単位でのスプリントプランニング
  • アジャイルボード作り
  • Rubyに関する読書会を開いた
  • レビューする曜日を決めた
  • モブレビューをした

一つずつ紹介する。

週単位でのスプリントプランニング

まず、前回の反省で、細かなPRが沢山きてコードレビューだけで1日が終わってしまうという課題があった。 そこで、流量を決めるために、月曜日にスプリントプランニングを行うようにした。先のような課題はあるものの、簡単なプロダクトバックログであっても「急ぎではないが重要なこと」であったりするので、無闇に進める速度をゼロに落としたくはない。

ルールとしては、

  • 月曜日にスプリントプランニングする
  • 決められたプロダクトバックログを全てこなしたら、その週の仕事は終了とする
  • 残り時間は自己研鑽・技術調査・急ぎではないが重要なこと等に取り組んでもよい
  • あまりにチケット数を減らしすぎるのもよくないので、適度にアサインしてほしい
  • しかし、技術調査のために敢えて担当チケット数を少なくすることがあってもよい。それはスプリントプランニング時に要相談

とした。

アジャイルボード作り

弊社ではRedMineを使っているのだけれど、アジャイルプラグイン(?)が入っている。スプリントの設定を今までやっていなかったので、それを設定して週単位で見やすい形にした。

Rubyに関する読書会を開いた

コーディング力と、コードレビュー力を上げるために、Rubyに関する読書会を業務時間内にやりたいと考えていたので、それをメンバーに説明して、やることにした。お題の本は、Effecive Rubyにした。

Effective Ruby

Effective Ruby

これを選んだ理由は、私が読んだことがなかったことと、みんな初級の本はこなしているので、よりよい書き方にフォーカスしたかったからだ。メタプログラミングRubyだと妙なテクニックに走りかねないかも…という懸念もある(第二版はまだ読んでないが)。

とりあえず今月は今のところ2回やっている。週に1回のペースでやっていく。

レビューする曜日を決めた

PRの量を調整したところで、毎日のようにレビューがあるとスイッチングコストが大きくなってしまう。それは他のメンバーもそうだと思うので、レビューする日としない日を決めた。

曜日 やること
月曜 打ち合わせ、読書会、残りはタスクに取り組む
火曜 タスクに取り組む(相談はOK)
水曜 モブレビュー、残りはタスクに取り組む
木曜 タスクに取り組む(相談はOK)
金曜 モブレビュー、残りはタスクに取り組む

水曜と金曜をモブレビューの日にして、火曜・木曜を動けるようにしたので、とりあえず集中して作業できる日が確保できたので、ある程度進捗を出せるようにもなった気がする。とはいえ、これはあくまでも開発チームとしての決まりごとなので、他のチームからの問い合わせには柔軟に対応している(データ出してー、とか)

モブレビューした

モブレビューは、モブプログラミングみたいに、ワイワイとチームでレビューする会である。オンライン会議で画面共有しながらレビューをするので、どういう意図でこう実装したのか聞いたり、より良いコードはこう、とか、歴史的経緯でこう実装されているという話をメンバー全員に共有できるので、話が早い(まぁ私含めて3名だけど)。PR作成者に実際の動作を画面共有しながら見せてもらうこともできるので、こちらで下準備する手間とかもない。

また、指摘のほうが間違っている場合にも、即座に反論というか意見交換できるのでレビュワーが勘違いしてた場合も助かる(主に勘違いするのは私)。

本来の目的は、ベテランの視点でのレビューで指摘されるところをメンバーに共有して、若手のレビュー力を向上することにあるのだが、週報のときに感想を聞いてみたら、「文字だけで指摘されるよりも腑に落ちやすい」ということだった。

まとめ

これらの取り組みによって、

  • チームのRuby力の向上
  • チームのレビュー力の向上
  • 割込を少なくして開発効率を上げる
  • 自己研鑽・技術調査をタスクに入れてもいいと認識してもらう

というところを狙っている。

個人の力量が上がらないことには、チームの力量も上がらないので、私としては、まずはチームとしての力を底上げしつつ、各々が自分の好きな技術分野で得意を増やしていってもらえたらなあ、と考えている。

リファクタリングデーでキャッシュ化に取り組んだ話

第三回リファクタリングデーを開催したのだけれど、これがすごくよかったのでまとめておく。

お題はキャッシュ化

後輩氏とパートナーさんは、まだキャッシュにすることの経験がなかったので、過去にそれぞれが実装したところ等でキャッシュ化できそうなところを探してみよう、という感じにした。まぁ目に付けば別に他のところでもいい。

なぜキャッシュ化するのか?

この点から説明しておいた。

Webアプリケーションの開発はDBへのアクセス回数を減らすことが大事っていうのはだいぶ浸透していて、メンバー全員が気を付けて開発できるようになってきた。しかし、そもそもアクセスしなくてもいいようにするのが理想である。つまりはコンテンツの変更がなければ、以前に作ったデータを再利用すればよい。これがキャッシュ化。

キャッシュ化のメリット

キャッシュ化が効くと何が嬉しいか?

  • アプリケーションサーバーの負荷が減る
  • データベースサーバーの負荷が減る
  • ユーザーへのレスポンスが早くなり体験がよくなる

まさにいいこと尽くめ。

キャッシュ化のデメリット

デメリットも、なくはない。

  • データが更新されているにも関わらず、変更が画面上に反映されない等の事故が起こりうる
    • キャッシュの寿命の設計が必要に
  • 本来見えてはいけないデータが見えてしまう等の事故が起こりうる
    • キャッシュの適用範囲の設計が重要に

キャッシュキーに何を持たせるか?

上記のデメリットは基本的に全て事故となりうることばかりなので、そうならないようにするには?をまず教えた。

  • ユーザー個人に紐づくデータをキャッシュする場合は、キャッシュキーに個人を特定できる情報を付加する(ユーザーIDなど)
  • ログイン状態に紐づくデータをキャッシュする場合は、キャッシュキーにログイン状態を特定できる情報を付加する(Deviseでいえば、user_signed_in?の結果等)
  • オブジェクトをキーとして指定すれば、オブジェクトに変更があれば過去のキャッシュは無視されるので有効
  • ActiveRecord::Relationの状態のオブジェクトをキーにしても意味がないので注意(クエリ発行してないから)
  • 何かしらのオブジェクト、配列、Hash等にしておくとキーにしやすい
  • データ更新系のコールバックでキャッシュを削除することを忘れないこと

何をキャッシュするか?

View

基本的にはレンダリング後のViewをキャッシュしておくといいが、会員サイト等ではページキャッシュはやりづらい。ユーザーに依存する情報をAjaxで取ってくるようにでも実装していない限りは。その実装も面倒なので、基本的にはフラグメントキャッシュを使う。

- cache(post) do
  h1 = post.title
  = simple_format post.content

計算結果など

計算結果というと仰々しいが、例えばカウント数とか。毎回カウントを取得してもいいが、何気にカウントするためのクエリは遅い。

user_count = Rails.cache.fetch(:user_count) { User.count }

更に、これをフラグメントキャッシュで使う。

- cache(:"user_count_#{user_count}") do
  p = "今までに#{number_with_delimiter(user_count)}人が活用しているノウハウ"

キャッシュの廃棄もやると…

class User < ApplicationRecord

  after_commit :delete_cache

  private

    def delete_cache
      Rails.cache.delete(:user_count)
    end
end

初期表示データ

例えば、検索などで使う場合であっても、デフォルトで設定されている検索条件の結果等はキャッシュ化しておくとよい。どうせ毎回実行されるからだ。

マスタデータ

マスタデータは、なかなか更新されにくいので、キャッシュのし甲斐がある。

ユーザーに関連するデータ

ユーザーに関連するデータは頻繁に参照するのでキャッシュ化すると速くなりやすい。その分、キャッシュの寿命と適用範囲には気を付けなければならない。

多少ずれていても問題ないデータ

常に最新のデータが表示されていなくてもいいような場合は、キャッシュの寿命を1分とかにしておくと、1分間はDBへのアクセスも減らせるし、1分後には最新のデータになるのでサイトとしても動きがあるように見せられる。ランキングとかで使えるだろうか。

実際にやってみた

以上のようなことを、ざっと説明した後に作業に取り掛かってもらった。実際にSQLの発行回数が減っていることをrack-miniprofilerなどで確認すること!と伝えた。

メンバーはあまりキャッシュを使うことを意識したことがなかったようなので、目に見えて速くなることは楽しかったようであった。

成果

リファクタリングデーの成果としては、主にマスタデータのキャッシュ化や、初期表示データのキャッシュ化をやってもらった。私自身はユーザーに関連するデータをキャッシュ化したりした(閲覧履歴などのデータ)。頻繁に参照する部分でキャッシュを活用できるようにしたのはかなり良かったと思う。

また、これ以降のバックログに取り組む際に、新たな機能においてもキャッシュ化できる箇所は対応してからPRを送ってくれたので、これが最も大きな成果であったと思う。

メンバーが順調に育ってきていると感じられた。

最近取り組んできたことと新たな課題について

4月のまとめ的な感じになるけれど。

予備知識

現在の開発体制が、私がリーダー、後輩氏、パートナーさんの3名体制である。

  • 私はRails歴9年で今のプロジェクトを最初から担当している。40歳とはいえ新米リーダーである。
  • 後輩氏は4月より新卒入社3年目に入ったくらいで、経験はまだ浅いしプロジェクト歴は半年過ぎたくらい。
  • パートナーさんは今年2月くらいから開発に参加。他言語の経験はあったがRubyRailsは初めて。

前はこういうことに取り組んでいた。

patorash.hatenablog.com

現在

チケットの粒度を私側で調整して、それを割り振るようにしていたおかげで、チームメンバーが何をしたらいいかについてはハマりにくくなったので、その点は良かった。事前に仕様を考えて実装前にチェックするので、妙な手戻りも起きにくい。そのため、実装速度も上がってきた。これはいい成果である。

そして、最近のことでいうと、よく言われる

  • 緊急で重要な仕事
  • 緊急ではないが重要な仕事
  • 緊急で重要ではない仕事
  • 緊急でも重要でもない仕事

についての話をして、端から見たら「やらないとなー」とは思うけれど急ぎではないからと放置され続けてしまうようなチケット(つまり緊急ではないが重要な仕事)についても、仕事の具合を見つつ、徐々に取り組んでください。と伝え、最近はそれをやってもらえるようにもなってきた。これもいい成果である。まぁ具体的には、

  • 仕様変更が起きているためにバージョンを固定化しているgemのバージョンアップ
  • Rubyのバージョンアップ
  • Railsのバージョンアップ
  • JSライブラリのバージョンアップ
  • ミドルウェアのバージョンアップ
  • 修正範囲は小さいけれど大量にやらなければならないリファクタリング

というところ。

また、PR*1をチェックする場合は私がレビューする前に後輩氏とパートナー氏の間で先に見てもらうというルールにしていて、それが通ったら私がチェックしてマージする、というふうにしている。

まぁやってほしいところをしてもらえるようになって、成果も出ているので、チームとしては成長していると思う。

ただちょっと、自分の中で色々と課題が出てきている。

課題

コードレビューで一日が終わる

さきほど上げた、修正範囲は小さいけれど大量にやらなければならないリファクタリングに取り組んでもらっている。まぁでも課題自体は二人に任せており、配分もそれぞれで話し合って担当範囲を決めてくださいってことにもしていて、子チケットは二人が作ってくれているので、その点も非常に助かっている…のだが、修正範囲が小さいであるが故に修正もすぐに終わり、PRがバンバン飛んでくる。

そんなPRなので、レビュー後の指摘の修正もそこそこすぐに終わり、再レビューが来る。再レビューを終えてマージする頃には次のPRが来る…という感じで、気づいたら自分のタスクがほぼ進まないまま、1日が終わっている。PRとPRの合間に自分のことをやるのでスイッチングコストもかかる。

作業者にとっては、ストーリーポイントが1のタスクをバンバンこなしている感じになるので、リズムに乗れるし、仕事やってる感もでてくると思う(単純なことなので退屈だなーと思いながら、かもしれないところはある)。しかしレビュワーにとってはキューがどんどん溜まっていくのでキツイ。レビューが遅くなると同じミスを繰り返しているPRが飛んできたりもするので、早めにレスポンスしたほうがいいなとも思って優先度を上げてレビューした結果、ミスも減って良かった…のだが更にPRが来る速度が上がる。

やってほしいと言っているのも私自身なので、「これは仕方ないのか…!?」と思う反面、レビューで終わる日が続くとちょっと精神的に、くる。

解決法?

ちょうどこの前、上司である id:tech-kazuhisa との1on1があったので、その話をしたところ、よいディスカッションができた。

  • スプリントプランニングでそれらのタスクの数を調整しておく
  • 後輩氏とパートナーさんでお互いにレビューしあってマージできるレベルまで育てる

前者は、そもそもうちのチームは「なんちゃってスクラム」みたいになっていて、スプリントプランニングなどを殆どしないで目に付いたバックログを倒していくような運用をしているので、そろそろちゃんとスプリントプランニングしたほうがいいかもなぁという気持ちになった。そこである程度小さいPRの流量を調整しておけば、確かに作業時間を捻出しつつ、PRも取り込んでいけそう。

後者は、まだ難しい…。これはモブ設計ならぬモブレビューみたいなことをしたほうがいいのかな?とか、RubyRailsの本の読書会などをしてRubyらしい視点でのレビューができるように培っていくことが必要かなとも思うので、週に1時間でも何かしらの読書会をしていくべきか…という気持ちになった。 ただ、レビュー時に気を付ける点は明文化できそうである。PRテンプレートに私なりの気を付けていること等を書いておくというのがいいかなと考えたので、GW開けたらそれをやっておきたい。

*1:プルリクエストのこと

gem auditedでrails consoleやrake taskの時にデフォルトユーザーを設定する

auditedというgemを使って、監査ログを保存するようにしたいと思い、現在調査中。

github.com

大体はよさそうなので、採用したいと思っているのだけれど、困ったのが、rails consoleとかでデータ変更されたとき。データ変更の履歴は残るものの、誰がやったかがわからない。

流石にそれだと万が一rails console経由で変な処理してしまったら困るので、誰かは分からずともrails console経由であることくらいはわかるようにしたいということで、設定してみました。

rails consoleであることをどうやって検知するか?

これが全然わからなかったのですが、stackoverflowにありました。

stackoverflow.com

これによると、defined?(::Rails::Server)でいけるとあります。

実際にやってみると…。

bin/rails c
Loading development environment (Rails 6.0.3.6)

[1] pry(main)> defined?(::Rails::Server)
=> nil

なるほど、consoleだと::Rails::Serverの定義を読み込まないということですね。

ちなみにRailsの初期化プロセスはRailsガイドにあるので、こちらも読むと参考になります。最初はこれを読んでどこかで処理を差し込むところはないか?を探ってました。

railsguides.jp

auditedのinitializerを定義する

config/initializers/audited.rbを作ります。

unless defined?(::Rails::Server)
  Audited.store[:audited_user] = "console"
end

ということで、これでめでたし、めでたし…とはなりません😢

rake taskのときにも::Rails::Serverは読みこまれないので、rake taskでもusernameが入ってしまいました。

純粋にrails consoleであることを検知するにはどうしたらいいのか…。

rake taskであることを検知する

これもまたstackoverflowでヒットしました。

stackoverflow.com

Rake.application.top_level_tasks.empty?をすることで、現在の処理がrake task経由であることがわかるようです。

ということで、config/initializers/audited.rbを修正。

unless defined?(::Rails::Server)
  if Rake.application.top_level_tasks.empty?
    Audited.store[:audited_user] = "console"
  else
    Audited.store[:audited_user] = "rake-task"
  end
end

これで、今のところ問題なさそう!👍

と思ったけれど、この記事を書いている最中に、rails runnerのことを考慮できてないやんって気付いた…。ダメだ…😵

Railsの設定で可能だった

ここにきて、神速さんから素晴らしい情報をもらった。

これを設定したら簡単、かつ、わかりやすかった。ダメ元でrunner doも付けてみたらrails runnerの時の設定もうまくいった(ぇ。

しかし、rake taskのときの検出方法がRailsの設定ではわからなかったので、さっきのやつを使うことに。

Rails.application.configure do
  console do
    Audited.store[:audited_user] = "console"
  end

  runner do
    Audited.store[:audited_user] = "runner"
  end
end

unless defined?(::Rails::Server)
  if Rake.application.top_level_tasks.present?
    Audited.store[:audited_user] = "rake-task"
  end
end

これでバッチリでした!👍

[3] pry(main)> hoge = Hoge.find(1)
[4] pry(main)> hoge.name
=> "あああ"
[5] pry(main)> hoge.update(name: "アアア")
[6] pry(main)> Audited::Audit.last
  Audited::Audit Load (1.3ms)  SELECT "audits".* FROM "audits" ORDER BY "audits"."id" DESC LIMIT 1
=> #<Audited::Audit:0x000055b996469a88
 id: 380,
 auditable_id: 1,
 auditable_type: "Hoge",
 associated_id: nil,
 associated_type: nil,
 user_id: nil,
 user_type: nil,
 username: "console", # <= ここが重要!!
 action: "update",
 audited_changes: {"name"=>["あああ", "アアア"]},
 version: 1,
 comment: nil,
 remote_address: nil,
 request_uuid: "4f057afe-2edc-4614-88fd-0a17a7b3d93d",
 created_at: Wed, 14 Apr 2021 18:19:19 JST +09:00>

第31回中国地方DB勉強会で発表しました(録画で)

先週ですが、中国地方DB勉強会で発表してきました。

dbstudychugoku.connpass.com

開催日が平日の夜ということもあり、私自身はリアルタイムで参加できなかったのですが、発表は前からお願いされていたので、なんとか動画を録って主催者のid:ikkitang1211 に送っておいて、録画で発表させてもらいました。

あとでハッシュタグ #chugokudb を追ってみたら、結構コメントをもらえていたのでよかったです。

発表内容について

発表内容は、ここ1ヶ月くらいでやっていたテーブル・カラムにコメントをつけることにした経緯とかその手法とかについて、でした。

まぁ大体のことは、前にブログで書いてますが、それをまとめ直した形です。

patorash.hatenablog.com

録画するときの苦労

新型コロナの影響で、録画発表をすることもまだまだあるかもしれないので、録画したときの苦労とかをまとめておきます。

Chromebookの画面録画は不具合がある

2021年3月に大型アップデートがあって、Chromebookで画面のスクリーンショットや録画ができるようになりました。

support.google.com

スライドをGoogle Slideで作っていた私は、「これは全部Googleで済ませられるのではないか?」と考え、Chromebookで試しに短い時間の録画を試してみたところ、問題なし、と判断。そこで、発表資料を読みながら録画していきました。

録画後に、再生してみると、問題なさそうに見えるのですが、徐々に違和感が…。表示しているページと声が徐々にずれていくのです。最終的に、まだ読み上げているにも関わらず、「ご清聴ありがとうございました」の文字が…。どうも長丁場の録画だといけてないようです。しかし、作成された動画のファイルサイズは18分の動画にも関わらず14MBとめちゃくちゃ軽かったので、このズレが解消されるようだったら使っていけたらいいと思います。

MacQuickTimeで録り直し

仕方ないのでMacQuickTimeを使って取り直したのですが、こちらは問題なし👍実際に勉強会で流してもらったのもこちらです。ただし、動画のファイルサイズは400MBくらいになってました。

とはいえ、安心感があるので、今後はQuickTimeで録ることにするかと思います。

花粉症で喉がやられていた

苦労したのは、花粉症で喉がやられていて、話していると咳が出やすくなること…。最近私は龍角散ダイレクトがこれに効くことを知ったので、Chromebookで動画を撮る前に飲んでました。おかげで、咳をほぼすることなく録れたのですが、さっき書いた通りで使える代物ではなく…😢

Macで録り直そうとしたのですが、龍角散の効果が切れて咳が出始めてしまい…。ちなみに龍角散ダイレクトは一度飲んだら2時間はあけるように、と注意書きがあるので、もう一度飲んで録り直すには1時間ちょい待たなければなりませんでした。この時点で午前1時前。

とりあえず時間が過ぎるのを待って、午前2時になってから再び龍角散ダイレクトを飲んで録り直しました。効果が切れる前に録り終わることができてよかったです。

この時期、発表予定がある方は手元に持っておくといいかもしれません。薬局で売ってます。

【第3類医薬品】龍角散ダイレクトスティックミント 16包

【第3類医薬品】龍角散ダイレクトスティックミント 16包

  • 発売日: 2008/10/03
  • メディア: ヘルスケア&ケア用品

SQLiteのLIKE演算子はデフォルトでESCAPE文字が設定されていない

私はimyouというニックネーム管理用gemを公開しているのですが、開発時にPostgreSQLを使っていました。 しかし、RailsのデフォルトのデータベースはSQLiteなので、SQLiteで開発したほうがよかろうと考え、SQLiteに変更してテストを実行したところ、なんと落ちてしまいました。

落ちた原因の調査

テストで実行されているSQLが変わるのか?と思い、to_sqlを挟んで調査してみましたが、SQL文はほぼ同じ。PostgreSQLの場合はILIKEになるくらい。

落ちていたテストは、LIKE演算子にアンダーバーを含んだ文字列でした。sanitize_sql_likeメソッドを使っているから、_もしっかりエスケープされているから問題ないはず…。と思っていたのですが、どうもSQLiteでは、明示的にESCAPE文字列を指定しないといけないようでした。PostgreSQLでも明示的に指定はできますが、デフォルトは\(バックスラッシュ)です。

明示的に指定していないため、エスケープされた文字列はfoo\_barのようになっているものの、バックスラッシュと任意の一文字と合致する条件となってしまっていました。

対処

sanitize_sql_likeメソッドの第二引数はエスケープ文字の指定なので、'\\'を指定するように修正しました。 こうすることで、WHERE name LIKE '%foo\_bar%' ESCAPE '\'というSQLに変わり、SQLiteでも問題なくエスケープされるようになりました。

まとめ

SQLite_を含むパターンマッチを行う場合はエスケープ文字をちゃんと設定しましょう。

なお、imyouは、この問題を修正したバージョン 1.4.2 をリリース済みです。

Rubyで要素数が異なる二次元配列で行と列を入れ替える

最近のPRのレビューをしているときに、二次元配列のデータを入れ替える処理をしているところがあったので、レビュー後にチャットで意見交換していたら綺麗な感じにできたのでそれをメモして残しておく。

transposeを使うのがミソなのだけれど、transposeを使うには、配列の要素数を統一しなければならない。そのために、values_atを使う。このvalues_atを使うアイデアはパートナーさんが考えてくれた。

docs.ruby-lang.org

docs.ruby-lang.org

# 要素数が異なる配列を準備…
arr = [['a', 'b'], ['c', 'd', 'e', 'f'], [], ['g', 'h', 'i']]
# その中から最大要素数を取得
max_size = arr.map(&:size).max
# 各配列の要素数を最大要素数に合わせる
arr.map! {|it| it.values_at(0...max_size) }
# 行と列を入れ替える
arr.transpose
# => [
# ["a", "c", nil, "g"], 
# ["b", "d", nil, "h"],
# [nil, "e", nil, "i"],
# [nil, "f", nil, nil]]

最初はzipを使おうとしていたのだが、zipを使う場合は先頭要素に最も大きな配列がなければうまくいかないということを指摘してもらって、別のアイデアを考えていたら、こうなった。

縦持ちと横持ちのデータの入れ替えは時々したくなることがあるので、いざと言う時にこの記事を思い出したい。