- Ruby / Rails関連
週刊Railsウォッチ(20201201前編)switch_pointがActive Record 6.0でサポート終了、Rails DBトランザクションの落とし穴ほか
こんにちは、hachi8833です。本日よりTechRachoアドベントカレンダー2020が始まりました。どうぞよろしくお願いします。
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
- お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇
⚓ Rails: 先週の改修(Rails公式ニュースより)
今週は公式更新情報から見繕いました。
- 元記事: Bugfixes, improvements and more! | Riding Rails
-
6.1.0マイルストーン: 6.1.0 Milestone -- 98%完了、残り2件(つっつき時点)
つっつきボイス:「6.1.0のマイルストーンが先週見た後で急に進捗進んでました」「リグレッションも出ているけど、issueはもう残り2件か」「リリースがだいぶ近づいてきたようですね」
なお、つっつきの翌日はissueが4件に増え、ウォッチ公開日には再び減って1件(99%完了)になっていました。
⚓ partitionedテーブル定義でschema:load
の問題を修正
# activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb#L47
def add_table_options!(create_sql, o)
create_sql = super
create_sql << " DEFAULT CHARSET=#{o.charset}" if o.charset
create_sql << " COLLATE=#{o.collation}" if o.collation
- add_sql_comment!(create_sql, o.comment)
+ add_sql_comment!(super, o.comment)
end
つっつきボイス:「テーブル定義にPARTITION BYを書けるのか↓」「これはいわゆるシャーディングの機能」「この機能を実際に使って起きた問題をissueにあげてくれたんでしょうね」
# activerecord/test/cases/adapters/mysql2/table_options_test.rb#53
test "charset and partitioned table options" do
@connection.create_table "mysql_table_options", primary_key: ["id", "account_id"], charset: "utf8mb4", collation: "utf8mb4_bin", options: "ENGINE=InnoDB\n/*!50100 PARTITION BY HASH (`account_id`)\nPARTITIONS 128 */", force: :cascade do |t|
t.bigint "id", null: false, auto_increment: true
t.bigint "account_id", null: false, unsigned: true
end
output = dump_table_schema("mysql_table_options")
expected = /create_table "mysql_table_options", primary_key: \["id", "account_id"\], charset: "utf8mb4", collation: "utf8mb4_bin", options: "ENGINE=InnoDB\\n(\/\*\!50100)? PARTITION BY HASH \(`account_id`\)\\nPARTITIONS 128( \*\/)?", force: :cascade/
assert_match expected, output
end
参考: Shard (database architecture) - Wikipedia
「PARTITION BYはシャーディングのときにどのキーで分散させるかを指定するものだったと思います」「なるほど」
参考: PostgreSQL 12 ドキュメント: 5.11. テーブルのパーティショニング
⚓ 新機能: Active Storageにstrict loadingを追加
# activestorage/lib/active_storage/attached/model.rb#L63
- has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy
- has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy, strict_loading: strict_loading
+ has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob, strict_loading: strict_loading
つっつきボイス:「strict_loading: true
を指定するとlazy loadingを禁止するようになったのか」
# activestorage/lib/active_storage/attached/model.rb#43
class User < ApplicationRecord
has_one_attached :avatar, strict_loading: true
end
「これまではデフォルトでlazy loadingが効いていて恐らくストリーム処理されていたけど、ユースケースによってはファイルが完全にアップロードを完了してから処理したいというケースがあるので、それに対応したということだと思います」「後処理やバリデーションが重くて、しかも一般ユーザーが頻繁に巨大ファイルをアップロードするサービスでは、こういうオプションがあるといいでしょうね👍」
⚓ 同じクラスへの複数のbelongs_to
でautomatic_inverse_of
の挙動を修正
このプルリクは、foreign_keysが同じ
automatic_inverse_of
にバリデーションを追加する。
同じクラスへの複数のbelongs_to
がクラスにあると、automatic_inverse_of
で誤ったinverse_name
を検索することがある。
class Room < ActiveRecord::Base
belongs_to :user
belongs_to :owner, class_name: "User"
end
class User < ActiveRecord::Base
has_one :room
has_one :owned_room, class_name: "Room", foreign_key: "owner_id"
end
user = User.create!
owned_room = Room.create!(owner: user)
p user.room
現在の
automatic_inverse_of
は、関連付けで見つかったリフレクションをバリデーションする。
しかし、このバリデーションは外部キーが同一であることをバリデーションしていない。
外部キーのバリデーションを追加することでこのissueを修正できる。
同PRより大意
つっつきボイス:「上のサンプルコードのように、Roomというクラス内にbelongs_to :user
とbelongs_to :owner, class_name: "User"
という同じクラスを参照するbelongs_to
が複数あって、さらにautomatic_inverse_of
を使ったときの挙動に問題があったのね」「選択肢が複数あるときにautomatic_inverse_of
が違う方を返すことがあったらしい」「修正は1行追加だけでした↓」
# activerecord/lib/active_record/reflection.rb#L626
def valid_inverse_reflection?(reflection)
reflection &&
+ foreign_key == reflection.foreign_key &&
klass <= reflection.active_record &&
can_find_inverse_of_automatically?(reflection)
end
⚓ 新機能: FFmpegのシーン検出機能による動画プレビューの自動頭出し
- PR: Use FFmpeg scene detection for video previews by jonathanhefner · Pull Request #39096 · rails/rails
つっつきボイス:「RailsであのFFmpegを使うのか!」「フェードインで始まる動画だとプレビューが真っ黒になってしまうので、シーンの始まる位置を検出して変えられるようになったそうです」「Active Storageにデフォルトで動画プレビュー機能があるとは知らなかった」
「FFmpegのバージョンも指定されている↓」
# guides/source/active_storage_overview.md#L554
-WARNING: Extracting previews requires third-party applications, FFmpeg for
+WARNING: Extracting previews requires third-party applications, FFmpeg v3.4+ for
⚓ FFmpegよもやま話
「これはFFmpeg職人の出番かな」「FFmpegは今でこそエンジニア以外も使うツールになっていますけど、昔は知る人ぞ知るマニアックなツールでしたね」「懐かしいです」「FFmpegはかなりいろんな画像・音声処理をこなすことができて、画像を集めてアニメーションGIFを作ることも、動画からアニメーションGIFを生成することもできたと思います」「ここで使っているような、指定のキーフレームを画像として取り出したりすることもできます」「FFmpegはスイスアーミーナイフ的に強力なツールなので、その分コマンドラインオプションがすごく長くなったりしましたね」「そうそう」
「Active StorageをインストールするとFFmpegも入るんでしょうか?」「FFmpegはRails外部のソフトウェアなので自動では入らないでしょうね」「コマンドラインで呼び出すと思われるので、あればそれを使うし、入ってなければエラーになるんじゃないかな」「なるほど」「FFmpegが入っているかどうかをチェックするロジックぐらいはありそう(ffmpeg_exists?
)」
「FFmpegは普通のWebサーバーにはなかなか入ってないと思います」「それにFFmpegで動画コーデックを使う場合はコーデックのライセンスをチェックしないといけなくなるんですよ」「あ、たしかに」「コーデックなども含めたFFmpeg環境を完全に再現するのは地味に面倒」「WindowsやLinuxだとコーデックがひととおり入ったパッケージがあったと思います」「サイズ大きくなりそう...」
参考: コーデック - Wikipedia
参考: おすすめのコーデックパック - k本的に無料ソフト・フリーソフト -- Windows用
参考: Install Multimedia Codecs Ubuntu 20.04 LTS – Linux Hint -- Linux用
「動画コーデックの中にはライセンスがかなり複雑なものもあった覚えがあります」「昔は設定をあれこれこねくり回しながら使ってましたけど、最近はこの辺であまり悩まなくなった気がします」「QuickTime動画を再生するためだけにAppleの有料プレーヤーを買ったこともあります」「有料版、そういえばありました」「国によってメジャーなコーデックが違ったりするんですよ」
⚓Rails
⚓ BasecampのHEY stack
The HEY stack:
- Vanilla Ruby on Rails on the backend, running on edge
- Stimulus, Turbolinks, Trix + NEW MAGIC on the front end
- MySQL for DB (Vitess for sharding)
- Redis for short-lived data + caching
- ElasticSearch for indexing
- AWS/K8S— DHH (@dhh) June 24, 2020
つっつきボイス:「HEY.comはBPS社内Slackに貼っていただいたやつです」「BasecampのHEYというサービスはこれまで今ひとつわからなかったんですが、複数のメールボックスをここに集約するといい感じにメールボックスを横断的に検索したり管理したりできるようですね」
「そのHEY.comのGemfileをDHHが公開しています↓」「ありがたい🙏」
Here's our Gemfile for HEY: https://t.co/EvSG3WrWeG
— DHH (@dhh) June 24, 2020
- Gist: HEY's Gemfile
⚓ HEY stackのGemfile
「このGemfileで興味深いのは、たとえばsasscやsassc-railsを使っているところ↓: Webpackerは入っているけど、sassはWebpackerではなくsprocketsでコンパイルさせている」「ホントだ」「sassの中にRubyのコードを仕込むのであればsprocketsで処理するしかないでしょうね」
# 同Gistより
# JavaScript and assets
gem 'webpacker', '~> 5.1.1'
gem 'sprockets', github: 'rails/sprockets'
gem 'sprockets-rails', github: 'rails/sprockets-rails'
gem 'jbuilder', '~> 2.9', '>= 2.9.1', github: 'rails/jbuilder'
gem 'sassc-rails', '~> 2.1'
gem 'sassc', '<= 2.1'
gem 'local_time', '~> 2.0'
gem 'turbo', github: 'basecamp/turbo'
「他にも見慣れないgemがいくつか見える」「ジョブの処理にresqueを使ってますね」「同じくジョブ処理でよく使われるsidekiqだと、マルチプロセスなどの高度な機能を使う場合は有料版を買うことになりますけど(機能表)、resqueはオープンソースのみなのでHEY.comで採用したのかもしれないと想像してみました」
# 同Gistより
# Jobs
gem 'resque', '~> 2.0.0'
gem 'resque-multi-job-forks', '~> 0.5'
gem 'resque-pool', github: 'nevans/resque-pool'
gem 'resque-scheduler', github: 'resque/resque-scheduler'
gem 'resque-pause', github: 'basecamp/resque-pause'
gem 'resque-web', require: 'resque_web'
gem 'resque-scheduler-web', github: 'mattgibson/resque-scheduler-web'
gem 'sinatra', github: 'sinatra/sinatra'
「resqueのリポジトリを見ると、同じくジョブ処理用のdelayed_jobとの比較は載っているけど、sidekiqとの比較は載ってなかった」「resqueは、delayed_jobから強くインスパイアされて作ったと書かれてる、へ〜」「知らなかった」「世の中的にはsidekiqの方がメジャーな印象があって自分も最近はよく使いますけど、sidekiqは企業がサポートしているという安心感があるからよく使われているのかもしれないとちょっと思いました」
「GistのコメントのFAQを見ると、sidekiqの機能は不要、resqueで十分だからというDHHのツイート↓が引用されてる」「sidekiqは高機能ですけど、それが不要ならresqueで十分というのは理解できる」
Don't need any features from Sidekiq. Happy with Resque.
— DHH (@dhh) April 20, 2020
「別のコメントには、なぜMySQLにしたのかというFAQでもDHHのツイート↓が引用されてますね」「PostgreSQLとMySQLについて特に思うことはない、だからMySQLを使っているという感じかな」
No. That's why I use MySQL 😂
— DHH (@dhh) June 24, 2020
⚓ MySQLの使いどころ
「HEY.comでMySQLを使う理由について今自分が思ったのは、データがものすごく多くて、しかもメールという書式の整ったデータだからという要因もあるんじゃないかということですね: そのような場合はMySQLの方が速くなる見込みがあるんですよ」「おぉ」
「MySQLは、しくみや特性を理解して正しく用いればPostgreSQLより速くなることがよくあります: 全般にPostgreSQLは複雑なクエリを書いたときに速度を出しやすいけど、クエリが比較的単純な場合や複雑なクエリが不要な場合はMySQLの方が速度を出しやすい傾向がありますね(※あくまで経験による主観です)」「なるほど!」
「あと考えられるとすれば、HEY.comを作ったときにAWS Auroraを使いたかったのかもしれませんね」
参考: Amazon Aurora(高性能マネージドリレーショナルデータベース)| AWS
「MySQLもPostgreSQLもRailsでは広く使われているので、データの特性やユースケースなどに応じて適している方を使えばよいと思います」「その意味で、MySQLかPostgreSQLかという二者択一を迫る問いはあまり重要ではないでしょうね」
⚓ Rails DBトランザクションの落とし穴(RubyFlowより)
つっつきボイス:「トランザクションのいい書き方と悪い書き方のコード例が紹介されています」「以下のように↓トランザクションでStandardError
を書くことも一応できるけど、StandardError
だとエラーが意味する範囲が広すぎるので、独自のエラークラスを作ってActiveRecord::RecordInvalid
で出力する方がずっとよいという話、たしかに」「独自のエラークラスを定義するのはちょっとだけ手間ですが」
# 同記事より
record = MyModel.last
error_for_user = nil
begin
ActiveRecord::Base.transaction do
# ...
record.save!
end
rescue StandardError => e
# do something with exception here
error_for_user = "Sorry your transaction failed. Reason: #{e}"
end
puts error_for_user || "Success"
「ダメな例のひとつが、データベースレベルのエラーであるActiveRecord::StatementInvalid
をトランザクションの中でrescue
すること↓」「トランザクションの外でbegin
〜end
を書いて、トランザクションの外でrescue
せよという話、これもそのとおり」「begin
〜end
の場所が違うということですね」
# 同記事より: 悪例
record = MyModel.last
error_for_user = nil
ActiveRecord::Base.transaction do
begin
# ...
record.save!
rescue ActiveRecord::StatementInvalid => e # DON'T DO THIS !
error_for_user = "Sorry your transaction failed. Reason: #{e}"
end
end
puts error_for_user || "Success"
「よい例として、トランザクションの中でraise ActiveRecord::Rollback
して明示的にトランザクションをロールバックする↓: これもよく使います」
# 同記事より
def add_bonus(tomas)
ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback if john.is_not_cool?
tomas.update!(money: tomas.money + 100)
end
end
begin
add_bonus(tomas)
rescue ActiveRecord::Rollback => e
puts "Sorry your transaction failed. Reason: #{e}"
end
「#transaction
、MyModel.transaction
、ActiveRecord::Base.transaction
は互いにエイリアスなので動作は全部同じという話、おっしゃるとおり」「自分は昔からActiveRecord::Base.transaction
で書くのが好きなんですが、SQLのトランザクションを理解していれば、そもそもトランザクションの単位がモデルではないことがわかるので、その意味でもモデルを指定しないこの書き方が好きですね」「なるほど」
理由は、トランザクションの単位はモデルではなく、データベースコネクションを単位とするからだ。
ActiveRecord::Transactions::ClassMethodsより大意
「次はネステッドトランザクションは避けることをおすすめしたいという話」「たしかにネステッドトランザクションはいろいろ難しい: たとえばこの記事↓にもありますけど、ネストの内側でActiveRecord::Rollback
しても握りつぶされる😇」
参考: ネストしたトランザクション内で ActiveRecord::Rollback を raise しても握りつぶされるだけだ | TECHSCORE BLOG
参考: 【翻訳】ActiveRecordにおける、ネストしたトランザクションの落とし穴 - Qiita
「そもそもネステッドトランザクションは、RDBMSの実行レベルでサポートされていないことがほとんどです↓」「あ〜」「上の記事にrequires_new: true
を使えば真のネステッドトランザクションを使えると書かれてはいるけど、それでも大変...」(中略)
ほとんどのデータベースは真のネステッドトランザクションをサポートしていない。執筆時点では、真のネステッドトランザクションをサポートしていることがわかっているのはMicrosoft SQL Server(MS-SQL)だけだ。
ActiveRecord::Transactions::ClassMethodsより大意
「記事の最後で、以下のようにトランザクションの中にsave
だけを書く意味はないとあるのはどういうことだろう?🤔」
ActiveRecord::Base.transaction do
user.save!
end
「お、引用されているAPIドキュメント↓によると、save
やdestroy
は自動的に設定されているコールバックごとトランザクションでラップされるのか!」「Active Recordよくできてる〜」「それならたしかに上のように書く必要はありませんね😋」
#saveおよび#destroyのどちらも、ひとつのトランザクション内にラップされ、バリデーションやコールバックで行うあらゆる操作はこのトランザクションの保護下に置かれます。これによって、トランザクションが依存する値をチェックするためのバリデーションを使うことも、
after_*
などのコールバックで例外をraise
してロールバックすることもできます。
ActiveRecord::Transactions::ClassMethodsより大意
「これはなかなかいい記事だと思います👍」
同記事で参照されているAPIドキュメントを翻訳しました↓。
⚓ rails-erd: ER図を生成
つっつきボイス:「rails-erdは前からあったような気がしたんですが、ウォッチで扱ったことがありそうでなかったので取り上げてみました」「こういうER図を生成するのね↓」「使いたい人はどうぞ」
「ちなみにJetBrainsのRubyMineでも右クリックでダイアグラムを生成してくれますヨ↓(rails-erdとは違う図ですが)」「なるほど、IDEならgemをインストールせずにできますね」
⚓ switch_pointはActive Record 6.1以降をサポートせず
つっつきボイス:「BPS社内Slackにebiさんが貼ってくれた記事です」「switch_pointは、Railsでマルチプルデータベースを行うときによく使われるgem」「たしかにRailsが公式にマルチプルデータベースをサポートするようになればswitch_pointでやる必要はなくなりますね」
「記事によると、Rails 6.1でついにswitch_pointが壊れるようになったのか...」「Rails 6.0まではswitch_pointが動いてたというのもスゴい」「少し前にRailsにR/W(リード/ライト)Splittingが入っていたぐらいなので、Railsのマルチプルデータベース機能とswitch_pointの共存はヤバそう」
「シャーディングも含めてswitch_pointの機能がRailsで使えるようになってきたので、今後新しいRailsプロジェクトでマルチプルデータベースが必要なときはRailsの機能を使うことになるでしょうね」「マルチプルデータベース機能の開発リソースがswitch_pointとRailsで分かれてしまうのはもったいないので、switch_pointが6.1以降のサポートを終えたことでRailsでの開発にリソースが集約されることが期待できそう」
前編は以上です。
バックナンバー(2020年度第4四半期)
週刊Railsウォッチ(20201124)strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか
- 20201117後編 Rubyのパターンマッチングが3.0で本採用に、AWS Lambdaサイズを縮小する、AppleのM1チップほか
- 20201116前編 6.1のActive Storageでimage_processing gemが必須に、Webアプリ設計の変遷とフロントエンド領域の再定義ほか
- 20201111後編 RubyConf 2020が11/17〜19オンライン開催、GitHub Container Registryベータ開始、スマートロックほか
- 20201110前編 Rails 6.1 RC1がリリース、Railsアプリに最適なEC2インスタンスタイプ、n_plus_one_control gemほか
- 20201028後編 RuboCop 1.0.0 stable版がリリース、Ruby DSLのGUIフレームワークGlimmer、Keycloakほか
- 20201026前編 Shopifyのerb-lint gem、Form Objectを使いやすくするyaaf gem、railsrcの機能追加ほか
- 20201021後編 webpack 5リリースでWebpacker対応開始、AWS Lambda Extensions発表、Pythonにマクロ構文追加提案ほか
- 20201020前編 Percona Toolkitは優秀、Active Admin非公式ガイド、Railsをリアクティブにするガイドほか
- 20201013後編 ruby-type-profilerがtypeprofにリネーム、AWS API Gatewayの実行ログは便利、M5Stackほか
- 20201012前編 Railsの隠し機能routing visualizer、action_args gem、N+1用goldiloader gemほか
- 20201006後編 Rubyの
defined?
キーワード、Ractorベースのジョブスケジューラ、Caddy Webサーバーほか - 20201005前編 Ruby 2.7.2がリリース、Shopifyのモジュラー化gem「packwerk」、stimulus_reflexほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。