Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20200803前編)書籍『パーフェクトRuby on Rails』増補改訂版、マルチDBで抽象クラスをscaffold生成、GitLabがPumaに乗り換えほか

一週間ぶりのご無沙汰です、hachi8833です。医師がまとめた以下のPDFを知人の医者が推薦しておりました。


つっつきボイス:「今年も半分以上過ぎましたね」「やめて〜聞きたくない😆」

「上のスライドざっと見ましたけどわかりやすくていいですよね」「でも読んで欲しい人ほど読んでくれなかったりするという😆」「永遠の課題ですね…」

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。

新機能: マルチデータベースで抽象クラスをscaffold自動生成

  • マルチデータベースで抽象クラスを自動生成する

マルチプルデータベースのアプリでscaffoldによる生成を行う場合、現在のRailsでは--database=引数を渡しても抽象クラスを生成しない。この抽象クラスには設定を書き込むための接続情報が含まれ、そのデータベース用に生成されるあらゆるモデルが自動的にこの抽象クラスを継承するようになる。

使い方

  • 以下はanimalsコネクションから抽象クラスを1つ生成する
rails generate scaffold Pet name:string --database=animals
class AnimalsRecord < ApplicationRecord
  self.abstract_class = true
  connects_to database: { writing: :animals }
end
  • このAnimalsRecordを継承するPetモデルを生成する
class Pet < AnimalsRecord
end
  • 既に抽象クラスが作成済みで、Railsデフォルトと異なるパターンに従う場合、--database=を引数に取る親クラスを渡せるようになる
rails generate scaffold Pet name:string --database=animals --parent=SecondaryBase
  • これにより、AnimalsRecrdではなくSecondaryBase`という親から継承できるようになる
class Pet < SecondaryBase
end

Changelogより大意


つっつきボイス:「マルチデータベース、今使いまくってます」「おぉ、勇気ある!」「メインのデータベースをリードオンリーでやってるんですけど、クラスを上書きしないといけないとか、ハマりましたよ」「ApplicationRecordに相当するものを複数作って、接続先に応じて継承元を変えるというのは、自分もswitch_pointでそういう実装してました」

「データベースが切り替わったときにActive Storageのattachやblogも切り替えるようにしたかったんですけど」「あ、それやると面倒になりがちです」「はい、とても面倒くさくなりました😅」「内部でjoinするコードが自動生成されたりするとややこしくなりそうですし」「やりながら技術的負債になるよなこれって思いました…後で触りたくない…😇」


「で、このプルリクは、まさに今話したような、switch_point gemなんかでよく行われる、抽象クラスの作成をジェネレーターでサポートしたということですね: self.abstract_class = trueで抽象クラスを有効にしたものを作るところまでやってくれると↓」

class AnimalsRecord < ApplicationRecord
  self.abstract_class = true
  connects_to database: { writing: :animals }
end

「何と!そんなことしてくれるようになったとは…それ使いたかった〜😢」「そこは自分で抽象クラスを手書きすればいいじゃないですか😆」「まあそうなんですけど、ちょっと悔しい」「見事入れ違いになっちゃいましたね」「まあここで生成しているものはswitch_pointを使ってる人なら普通にやってることですし、みんなだいたいこう実装するんだからジェネレートをサポートしたということで」

「ここではApplicationRecordを継承する形で抽象クラスを生成してますね: ちなみに自分はApplicationRecordと並列する形で接続先に応じた抽象クラスを書いてますけど、まあそこは好みでしょうね」「なるほど、AnimalRecordを継承すると抽象クラスで指定したデータベースに切り替わるんですね↓: writingを指定する度胸はまだありませんけど😅」「writingにするかどうかは用途次第かな」

class Pet < AnimalsRecord
end

Active Storage: 添付ファイルがパージされたときに親モデルをtouchできるようになった

現在のdeletepurgepurge_laterメソッドで用いられているが、そのせいで添付ファイルがパージされたときに親モデルを更新するコールバックをトリガできない。この振る舞いが原因で、#39858で報告されているようなキャッシュ戦略上の問題がいくつか発生している。
変更点:

  • attachment#purgerecord&.touchを追加
  • attachment#purge_laterrecord&.touchを追加
  • ついでにattachment.rbの無駄な空行を削除
  • 変更前だとfailし、変更後ならパスするテストを追加

その他情報:
deletedestroyに変更することはしなかった(after_destroy_commit :purge_dependent_blob_laterのトリガを回避するため)。
同PRより大意


つっつきボイス:「touchされてないとカウンタキャッシュなんかが正しく更新されなくなってしまうので、これは必要な修正でしょうね」


「最近すっかりActive Storageおじさんになっちゃいました」「この間の記事↓ありがとうございます🙏」「いえいえ〜Active Storageネタまだまだあるんでまた書きますよ」「お〜😂」

ActiveStorageでアップロードしたファイルとプレビュー画像に認証をかける

「この記事でやってるような認証は、他のストレージエンジンでもだいたいこうせざるを得ないですよね」「S3なんかだと期限付きURLがあったりとか、ストレージエンジンによって挙動が違う部分もありますし」

Redisキャッシュストアでの複数の問題を修正

つっつきボイス:「Redisがらみで似たような修正が3つほどあったのでまとめて貼りました」


Redisキャッシュストアでfetch_multiに渡したオプションがwrite_multiには正しく渡されるがread_multiに正しく渡されなかった。このためnamespace:オプションを渡すと常に失われてしまっていた。このプルリクで修正された問題を再現するスニペットは以下のとおり(テストにも追加済み)。
同PRより大意

store = ActiveSupport::Cache::RedisCacheStore.new
store.fetch_multi("ab", "xy", namespace: "mynamespace") { |key| puts "missed cache for #{key}"; key }
store.fetch_multi("ab", "xy", namespace: "mynamespace") { |key| puts "missed cache for #{key}"; key }

「なるほど、read_multi_mgetにオプションを渡しそびれてたのね↓」

# activesupport/lib/active_support/cache/redis_cache_store.rb#L352
        def read_multi_entries(names, **options)
          if mget_capable?
-           read_multi_mget(*names)
+           read_multi_mget(*names, **options)
          else
            super
          end
        end

fetch_multiread_multiがそれぞれあるというのが何とも」「Redisの内部では最終的にMGETあたりになるんでしょうけど↓」


  • MemCacheStoreDalli gem↓の圧縮を無効にした
  • Dalliで余分な圧縮がかかる問題によって圧縮が2回行われたり、指定の圧縮スレッショルド以下にもかかわらず圧縮がかかったりする問題を修正した
    同PRより大意

  • Redisやmemcachedのキャッシュストアでraw:trueを指定して読み出すと値が圧縮される問題を修正
  • CPUに負荷をかける不要な操作を防いだことでrawキャッシュの読み出しも高速化したはず
    同PRより大意

「次の2つは、Dalliで無駄な圧縮が発生しないようにしたそうです」「Dalli最近使ってなかったな〜」「Redisが使える状況ならあえてmemcachedベースのストアを使う理由もありませんけどね」「ちなみにDalliはmemcachedストアのクライアント」「Redisにrawモードがあるって知らなかった」「修正前はrawでないときは圧縮かけてたのか」

petergoldstein/dalli - GitHub

「そもそも最近memcached使ってませんし」「Redisがあるから😆」「たしかにmemcachedは構築が楽なんですけど、後からRedis的なこともやりたくなるんだったら最初からRedisにしておく方がいいでしょうし」「memcachedのメリットは超絶シンプルなことぐらいですし」「memcachedはクラッシュすると飛んじゃいますし」「そうなんですよ…」

「でもRedisはRedisで、永続化データベースのつもりで使われまくって後で地獄を見るというのもよくありますし👹」「ちょっと前に流行った、NoSQLをMySQLみたいに使うのと似てますよね」「それそれ」「Redisを更新しないといけなくなると、アップデートの間はサービスが数時間停止したり」「まあmemcachedはプロセスを再起動しただけで消えちゃいますけど」


「たしかmemcachedってRailsよりもさらに昔からあった気がしますね」「めちゃレガシー!」「Wikipediaの英語版↓見ると2003年からですって」「GitHubより昔」「ボクの職歴より長い😆」「大学の課題で組み込み機器の内蔵フラッシュストレージに書き込めないという条件を回避するためにmemcachedに保存してたことがあったぐらいだから相当昔ですね〜」「そんなことやってたんですか😆」「そのぐらいmemcachedはシンプルですしコードベースも小さかったと思います」

参考: Memcached - Wikipedia

Action MailboxでX-Original-ToにSendgrid envelopeのrecipientを設定するようになった

SendgridではMailgunと同様、オリジナルのペイロードにあるenvelope JSONにあるパラメータとしてのみBCC受信者を渡すらしい。
このプルリクはSendgridペイロードからのrecipientをprependするコードをX-Original-Toヘッダー以下のraw_emailに追加する。
#38738に多大なヒントを得た。
関連: #38446
同PRより大意


つっつきボイス:「あら、これからSendgrid実装しようと思ってたのに」

「Action Mailboxを使う予定なんですか?」「よく考えたらAction Mailboxでやらなくてもいいかな…」「他にいろんなやり方もあるので今ならそんなにこだわらなくてもいいでしょうし、Webhookでも普通にやれますし」「Action Mailbox自身もSendgridと連携するときにはWebhook使いますし」

参考: Webhookとは? - Qiita

「Sendgridなら使っている人多いし、いいんじゃないでしょうか」「障害多いですけど🤣」「ありゃ😆」「この頃月1に近いペースで落ちるってbabaさんもぼやいてました」「まあメールサービスに障害は付き物なので」「メールの到着が遅れるだけならまだいいんですけど、APIが死ぬとメール自体が到着しなくなるのが厄介」「メールをキューに入れてから死んで欲しい😆」

コレクション関連付けがきっかり1度だけオートセーブされるよう修正

再現手順

この振る舞いはRails 6.0.3で新しく発生した。
has_many through:関連付けが用いられ、かつjoinテーブルの2つの外部キーにuniqueness制約がある場合、作成直後のレコード1件を更新するとsaveが2回行われてしまい、uniqueness制約違反が発生する。

class Team < ApplicationRecord
  has_many :memberships
  has_many :players, through: :memberships
end

class Player < ApplicationRecord
  has_many :memberships
  has_many :teams, through: :memberships
end

class Membership < ApplicationRecord
  # membershipsのplayer_idとteam_idにuniquenessインデックスがあるとする
  belongs_to :player
  belongs_to :team 
end

player = Player.create!(teams: team])
player.update!(updated_at: Time.current)

期待される動作

Rails 6.0.2.2では、新しいPlayerMembershipが1つずつsaveされ、かつPlayerからTeamへもリンクされる。以後の更新も成功する。

実際の動作

2回目のMembership#update!で書き込まれたタイミングでActiveRecord::RecordNotUnique例外が発生する。
この問題はどうやら#39124の変更に関連しているらしい。この問題は回避可能だが、これはRailsの以前の振る舞いとは異なる予想外の変更だ。
同PRより大意


つっつきボイス:「この間(ウォッチ20200720)で取り上げた#39173があの後修正されたそうです」「裏で自動的にsaveされるのってちょい怖いですよね」

「テストコードで久しぶりにHABTM見た↓」「まだ動くんだな〜😆」

# activerecord/test/models/post.rb#L289
class PostWithAfterCreateCallback < ActiveRecord::Base
  self.inheritance_column = :disabled
  self.table_name = "posts"
+ has_many :comments, foreign_key: :post_id
  has_and_belongs_to_many :categories, foreign_key: :post_id

  after_create do |post|
    update_attribute(:author_id, comments.first.id)
  end
end

参考: 2.8 has_many :throughhas_and_belongs_to_manyのどちらを選ぶか — Active Record の関連付け - Railsガイド
参考: HABTMリレーションシップは悪であるという論争 | A-Listers

Rails

書籍『パーフェクトRuby on Rails【増補改訂版】』


つっつきボイス:「前回のウォッチでは号外止まりだったので」「皆さん続々購入かけてるみたい」「まだ1/3ぐらいしか読んでません😅」

「@joker1007さんがDockerの章をたくさん書いてくれたらしいですし↓」「お〜🎉」

「こういう最新情報が書籍という形にまとまって手に入るというのはいいことだと思います!」「ホントありがたい🙏」「Railsに参入する初心者がたくさんいるからこそこういう本が出せて売れるわけですし」「Railsの強い人もこんなふうに読んでるそうです↓」「目次見ただけでもわかりみしかない」


「今日ちょうど昼の勉強会でも少し話したんですけど、Linuxカーネルの最新情報がまとまった日本語の本って(自分の観測範囲では)ここ十数年見かけていないんですよ」「そんなに🤣」「なのでLinux方面でそういう情報を追おうとするとホント大変」「自分が学生の頃に読んだ『詳解Linuxカーネル』のLinuxカーネルは2.6で、3.0が出るか出ないかの頃、あとは『Linuxデバイスドライバ』を読んでなるほどと思ったりしたんですけど、今同じ方法で勉強すると書いてあるとおりには動かない部分も出てくるでしょうし」「たしかに😢」

参考: Linuxカーネル - Wikipedia

「こういう渋い本ってやっぱり売れないのかな」「『詳解Linuxカーネル』は少なくとも日本語の情報としてあそこまで網羅している本が今のところ見当たらなくて」

「Railsでも、どこにどういう機能があるかという脳内マップが一度出来上がれば、あとは差分を追いかければ済むけど、脳内マップがまだできてない人は、パーフェクトRailsやRailsガイドの他にも、まとまった本を何冊も読んで脳内マップを育てて機能を追いかけられるようにしておくのがよいと思います💪」「1冊で終わらせないと」「そういうときに古い本しかないと悲しいですよね」

lambdaじゃなくてService ObjectやQuery Objectを使うメリットは?(Ruby on Rails Discussionsより)


つっつきボイス:「lambdaでどんだけでかいコードを書きたいのかと」「レスでも、lambdaだと大きすぎるときにService Objectとかを使うとありますね」「lambdaだとエラー時に吐くものがProcになるからデバッグ大変になるし」「処理をまとめるだけならlambdaでもできますけど、クラスや関数を使わないで全部lambdaで書いてもいいかというとそんなことはありませんし」

参考: class Proc (Ruby 2.7.0 リファレンスマニュアル)

「上の他にもdiscuss.rubyonrails.orgでいろいろ盛り上がってて、たとえば以下はapp/javascriptというディレクトリ名はちょっと違うのではという話題でした↓」

app/frontendとかapp/packsにしようぜって言ってる人も😆」「気持ちはわかるけど」「自分で好きな名前にカスタマイズすればいいと思いますけど」「この中ではapp/assets/webpackとかapp/assets/sprocketsがマシかな〜: それならwebpackerやsprocketsみたいなものを通る前のデータがここにあるというアピールになりそうですし、こういうディレクトリに置いたものはそのままの形では公開されないわけですし」

GitLabがUnicornからPumaに乗り換えた話(Ruby Weeklyより)


つっつきボイス:「GitLabのWebサーバーがPumaになったのね」「いつの間に」「世間の多くもいつの間にかPumaに乗り換えてた感ありますね」「Pumaがこんなに普及したのって何ででしょう?」「たぶんUnicornの頃はスレッドセーフでないコードが残っていたからなかなか乗り換えられなかったんでしょうけど、今はスレッドセーフでないコードを書く人がほとんどいなくなったからPumaに乗り換えても大丈夫という流れになったのかもですね」「なるほど」「乗り換えた理由はやっぱりメモリ使用量か」

Pumaにした理由
メモリ肥大化の問題を多少なりとも解決できるのではないかと信じて初期調査を開始した。Unicornのシングルスレッドプロセスから乗り換えれば、実行されているプロセス数や各プロセスのメモリオーバーヘッドを削減できる。Rubyのプロセスはかなりのメモリを消費するが、スレッドのメモリ消費量は、アプリケーションメモリの大半を占めるワーカーよりずっと小さくなる。I/Oが発生するとスレッドは一時停止するが、別のスレッドは引き続きアプリケーションのリクエストを処理できる。そういうわけで、マルチスレッドはメモリやCPUをベストな形で活用し、メモリ消費をおよそ40%も削減できる。
同記事より抜粋・大意

「そういえばGitHubはUnicorn使ってた気がしますね」「今も500エラーページに怒ったユニコーン出ますよね」「GitHubともなるとUnicornもチューニングしまくってるでしょうし、おいそれとは乗り換えられないでしょうね」

後で調べましたが、GitHubの現在のWebサーバーが何なのかは裏が取れませんでした。

参考: [GitHub] プルリクするとユニコーンが怒り出すのだが ? - Atuweb 開発 Log

GoodJob: PostgreSQLベースのマルチスレッドActive Jobバックエンド(Ruby Weeklyより)

bensheldon/good_job - GitHub


つっつきボイス:「ぐっじょぶっていう名前が😆」「ぽすぐれでDelayed::Jobみたいなことをやるそうで、ポーリングの設定もありました」

「実はPostgreSQLってこういう裏で定期的に何かするみたいなジョブバックエンドに使える機能もあるんですよ」「へ〜ぽすぐれにそんな機能があるなんて」「ぽすぐれには何でもあります」「MySQL勢だけどまたつっつき恒例のぽすぐれ乗り換え誘惑が〜😆」「使わない機能もいっぱいありますけどね」「選択肢があるのは大事: まあPostgreSQLのそういう機能にロックインされるとメンテが大変になったりもしますけど」

「このツールを使うと、データベースがぽすぐれならジョブキューもぽすぐれでやれるってことなんですね」「ですです、Redisが要らなくなる😆」

HashWithIndifferentAccess


つっつきボイス:「ついさっき公開された記事です」「ThorでHashWithIndifferentAccessexceptメソッドの挙動が微妙に変わったのか: 踏んだことはないけど」「区別しないはずなのに区別されてたんですね」

# 同記事より
# Rails 5.2以前
h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"bar"=>2}
# Rails 6.0
h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"foo"=>1, "bar"=>2}

参考: RailsのAPI ActiveSupport::HashWithIndifferentAccess

以下のプルリクが現在オープンされています。


追記(2020/08/04): koicさんから補足いただきました🙇。

その他Rails


つっつきボイス:「これも大事」「テストのメールドメインにはexample.comを使いましょう皆さん」「それ用でない他のドメインを使ったりすると攻撃されるきっかけになることもありますし、たとえ自分で持っているドメインでも何かのはずみで失効する可能性がありますし」「記事にもありますけど、テスト用に使っていいドメイン名はRFCとかで定義されてたと思います」

参考: 例示/実験用として利用できるドメイン名 - @IT
参考: JPドメイン名の活用について | よくある質問 | JPRS

「開発環境に限定するなら他のを使ってもいいのかなという気もしますけど」「でもそうする理由がありませんよね」「どうしてもexample.com以外でやりたいなら自分のメアドでやって欲しい🤣」「ドメイン階層をたどらないといけないテストは@localhostだとできないので、どっちみち@example.comとかにしないといけなくなりますし」

「ところでhoge.comって本当に実在するって知らなかった〜😆」「ありますよ〜😆」

参考: hogeとは (ホゲとは) [単語記事] - ニコニコ大百科


以下はつっつき後に見つけたツイートです。


前編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200721後編)『パーフェクトRuby on Rails』増補改訂版発売間近、scan_left gemでレイジーなinjectほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby on Rails Discussions

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。