- Ruby / Rails関連
週刊Railsウォッチ(20200622前編)AR attributes周りの高速化進む、Active RecordでUNIONクエリを書く、Cable Ready gemほか
こんにちは、hachi8833です。GitHub Marketplaceの無料カテゴリを覗いてみたらいろいろありすぎてまごつきました。
つっつきボイス:「GitHub Marketplaceをちゃんと見たのは初めてでした」「GitHubリポジトリと連携できるアプリとかアクションとかですね👀」「IDEまであるのね」「エコシステムがあるのはありがたい🙏」「人気も表示して欲しいかなとちょっと思いました」「人気の指標次第では殴り合いが始まりそうですけど😆」「Chrome拡張みたいに😆」「誰かマーケットプレースをキュレーションしてくれるといいな」「やりたい人ならいくらでもいそうですけど」
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
⚓Rails: 先週の改修(Rails公式ニュースより)
以下のコミットリストのChangelogを中心に見繕いました。
⚓新機能: immutable_strings_by_default
設定がActiveRecord::Base
に追加
全stringカラムをデフォルトでイミュータブルにできるそうです。Rails 4のときにマージされずにcloseした#29938↓に、MySQL向けの修正を加えたそうです。
つっつきボイス:「@kamipoさんによる修正です」「Active Recordのattribute
に:immutable_string
を指定できるようになったのか👀」「コンフィグにもimmutable_strings_by_default
が入るのね😋」「この改修でちょっと速くなるみたい🚅」
Warming up --------------------------------------
user.name 34.811k i/100ms
user.fast_name 39.505k i/100ms
Calculating -------------------------------------
user.name 343.864k (± 3.6%) i/s - 1.741M in 5.068576s
user.fast_name 384.033k (± 2.7%) i/s - 1.936M in 5.044425s
29938にMySQLのbooleanシリアライズの修正を加えたもの。
attributeのパフォーマンス改善を長年手掛けてきたが、(ミュータブルな)string型キャストはattributeで効率のよくない部分のひとつだ。
インプレースの改変を検出するため、string型は値の読み込みや代入をすべてdupすることで元の値を改変から保護するが、(ミュータブルな)string型の効率が低い理由がこれ。
イミュータブルなstring型は、読み込まれたり代入されたりする値をすべてfreezeすることでdupを回避でき、それによってfrozenな値はインプレース改変できないようになる。
string型attributeひとつと、そのイミュータブルなstring型のブランチによるシンプルなベンチマークは以下のとおり。
同PRより大意
# 同PRより
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :name
t.string :fast_name
end
end
class User < ActiveRecord::Base
attribute :fast_name, :immutable_string
end
user = User.new
Benchmark.ips do |x|
x.report("user.name") do
user.name = "foo"
user.name_changed?
end
x.report("user.fast_name") do
user.fast_name = "foo"
user.fast_name_changed?
end
end
「これが既存のコードに影響を与えることはまずなさそう: Active Recordで取ってきたstringのインスタンス自体を破壊的に変更することは普通ないだろうなと思うのでこれでいいでしょうし」「速い方がいいですよね😋」
⚓Springをboot.rbに移動
つっつきボイス:「bundle exec spring binstub
に--all
が要らなくなった」「#39622でspringがコケてた問題を修正したそうです」
1. `gem 'spring', group: :development`を`Gemfile`に追加
2. `bundle install`でspringをインストール
-3. `bundle exec spring binstub --all`でbinstubをspring化する
+3. `bundle exec spring binstub`でspring binstubを生成する
「DHHの元のプルリク↓も少し変えてるみたい」
「spring gemはいつの間にか動いててよくわからない😅」「マイグレーションがspringでつっかかったことはときどきあります😢」「割と謎だけど、Dockerコンテナで動かしている分には関係なくなることも多いのであまり気にならないかな」「springがあれば起動速くなりますし😋」
⚓Rails 5より前の古いYAMLの読み込みを非推奨化
古いYAMLを使いたい人はLegacyYamlAdapter
でやって欲しいそうです。
つっつきボイス:「古いyamlでdeprecation warningが出ると」「そろそろやっていい時期でしょうね」
# activerecord/lib/active_record/legacy_yaml_adapter.rb#L4
- module LegacyYamlAdapter
+ module LegacyYamlAdapter # :nodoc:
def self.convert(klass, coder)
return coder unless coder.is_a?(Psych::Coder)
case coder["active_record_yaml_version"]
when 1, 2 then coder
else
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ YAML loading from legacy format older than Rails 5.0 is deprecated
+ and will be removed in Rails 6.2.
+ MSG
if coder["attributes"].is_a?(ActiveModel::AttributeSet)
Rails420.convert(klass, coder)
else
Rails41.convert(klass, coder)
end
end
end
「その名もlegacy_yaml_adapter.rbなんてのがある↑」「Rails420とかRails41みたいに、書式が変わるたびにここに追加してたのね」「yamlのコンフィグファイルを使うテストで後方互換性をこうやって維持しているのか」「こういう実装は参考になる👍」「なるほど〜」「古い書式が必要ならLegacyYamlAdapterを使えばやれますし😋」
⚓Arelで使われていないEquality#operator
を削除
# activerecord/lib/arel/nodes/equality.rb
module Arel # :nodoc: all
module Nodes
class Equality < Arel::Nodes::Binary
- def operator; :== end
-
def equality?; true; end
operator
メソッドは87b6856で追加され、Active Recordの12b3ecaでこのメソッドを参照している。
既にHomogeneousIn
が追加されているが、これにはIn
ノードのようなoperator
はないので、operator
は以前のような互換性目的では動作しなくなっている。
これをやりたいときは代わりにequality?
メソッドを使うべき。
同PRより大意
つっつきボイス:「operator
っていうメソッドがArelにあったんですね」「不要になったら消されるのが常」「homogeneousって何でしたっけ?」「『同じ』的な意味っぽいけど🤔」「辞書見ると『同種の』という堅苦しい意味でした」
⚓各種高速化
- PR: PERF: 45% faster attributes for readonly usage by kamipo · Pull Request #39612 · rails/rails
- PR: Avoid to use slower `define_method` for `AcceptsMultiparameterTime` by kamipo · Pull Request #39645 · rails/rails
つっつきボイス:「#39612によるとModel.find(1)
はいいけど、Model.find(1).attr_name
はLazyAttributeHash
があっても遅かったのね」「find
はlazyになってたけどattributeがlazyになってなかったので修正したとかそういう感じでしょうね☺️」「なるほど!」「今ちょうどこういう感じのベンチマーク書いてるので参考になる↓🥰」
「ところでプルリクに貼られてるこのgist↑、折り畳めるようになってるんですけどmarkdownでどう書くんだろう?」「GitHubのgistだからGitHubだと書くだけで折りたたみ表示になるとか?🤔」「それっぽいですね」
生のデータベース値を元にattributes hashをインスタンス化するのはattribute周りで遅い部分のひとつだった。
これはattributeの改変検出で必要だったためで、言い換えれば改変が発生するまでは不要ということになる。
0f29c21で導入されたLazyAttributeHash
はattributeに最初にアクセスするまでインスタンス化をlazyにする(つまりModel.find(1)
は遅くならなくなったがModel.find(1).attribute名
が遅かった)
このPRはLazyAttributeSet
によるattributeのインスタンス化をよりlazyにし、attributeに最初に代入またはdirtyチェックするときまでattributeをインスタンス化しないようになる(つまりModel.find(1).attribute名
は遅くなくなる)。
これによってreaonly(改変なし)のattributeアクセスがおよそ35%改善される。
同PR冒頭より
「#39645はdefine_method
を置き換えて速くしたそうです」「実はdefine_method
でメソッドを動的生成する必要がなかった感😆」「呼び出すメソッドが決まってれば動的にやらなくてもよかったと」「なるほど」
# activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb#L4
module Type
module Helpers # :nodoc: all
class AcceptsMultiparameterTime < Module
- def initialize(defaults: {})
+ define_method(:serialize) do |value|
module InstanceMethods
def serialize(value)
super(cast(value))
end
- define_method(:cast) do |value|
+ def cast(value)
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
else
super(value)
end
end
- define_method(:assert_valid_value) do |value|
+ def assert_valid_value(value)
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
else
super(value)
end
end
- define_method(:value_constructed_by_mass_assignment?) do |value|
+ def value_constructed_by_mass_assignment?(value)
value.is_a?(Hash)
end
...
Before:
Warming up --------------------------------------
type.serialize(time) 12.899k i/100ms
Calculating -------------------------------------
type.serialize(time) 131.293k (± 1.6%) i/s - 657.849k in 5.011870s
After:
Warming up --------------------------------------
type.serialize(time) 14.603k i/100ms
Calculating -------------------------------------
type.serialize(time) 145.941k (± 1.1%) i/s - 730.150k in 5.003639s
「Active Record、着々と速くなってますね💪」「Rubyのdefine_method
って便利なのでときどき使いたくなりますけどね😋」
⚓セキュリティ修正: show_detailed_exceptions
をオンにしないとActionableErrors
を表示しないようにした
つっつきボイス:「こちらは今日(6/18)出した記事↓で扱ったセキュリティ修正です」「ああ、pendingしたマイグレーションに絡むヤツでしたっけ」「う、そうでしたか😅」
「このリンク先↓ちょっとだけ眺めたんですけど」「お、リンク切れ直ってますね」「pendingしたマイグレーションを実行できてしまうらしいんですけど、なぜexceptionを見ることで実行できたのかが今のところ謎なんですよね🤔」「あら〜」「productionで本当に実行できるとしたらエグいといえばエグいけど、実行できるのはpending中の定義済みマイグレーションだけみたいなので、ただちに大事になったりはしないんじゃないかな」「問題につながるのは、pendingマイグレーションが残った形でデプロイしたときなんでしょうし🤔」「pendingマイグレーションを実行できちゃうのはたしかにイケてませんし」「後で読んでみます👀」
信頼されていないユーザーがproduction環境でpendingマイグレーションを実行できる問題
これはRails 6.0.3.2より前のバージョンにおける脆弱性で、信頼されていないユーザーが、production環境で任意のpendingマイグレーションを実行できてしまう。
(中略)
影響
攻撃者はこのissueを用いて、productionモードで動くRailsアプリで任意のpending中マイグレーションを実行できるようになる可能性がある。ただし攻撃者が実行できるマイグレーションは、アプリケーションで定義済みであり、かつ未実行のものに限られる点が重要である。
groups.google.comより抜粋・大意
追記(2020/06/23)
y-yagiさんブログでこのissueがActionableErrorに関連していることを解説しているそうです。ありがとうございます!
pending migrationのやつは、ActionableError ですね。参考: https://t.co/uA7C1paaJY / “週刊Railsウォッチ(20200622前編)AR attributes周りの高速化進む、Active RecordでUNIONクエリを書く、Cable Ready gemほか|TechRacho(テックラッチョ)〜エンジニアの「?」…” https://t.co/MQEfqfB4RO
— rochefort (@rochefort8) June 22, 2020
⚓Rails
⚓Active RecordでUNIONクエリを書く(Hacklinesより)
つっつきボイス:「Active RecordでUNIONクエリですか」「生SQL使ったりいろいろ苦心してますね」「まあBase.connection.execute()
でやれますけど」「ActiveRecordExtendedというgemも使ってみてる👀」「同じモデルをUNIONするのは比較的わかりやすいかな☺️」
# 同記事より
result = ActiveRecord::Base.connection.execute("SELECT users.* FROM users WHERE users.active = 't' UNION SELECT users.* FROM users WHERE users.manager = 't'")
result.to_a #=> Collection of users from the union query
# 同記事より
User.where.not(updated_at: nil).merge(
User.union(
User.where(name: "Jon")
).union(
User.where(active: true)
)
).order(start_date: :desc)
「結局union
ヘルパーを自力で書いてる↓」
# 同記事より
module ActiveRecordUnion
extend ActiveSupport::Concern
class_methods do
def union(*relations)
mapped_sql = relations
.map(&:to_sql)
.join(") UNION (")
unionized_sql = "((#{mapped_sql})) #{table_name}"
from(unionized_sql)
end
end
end
ActiveRecord::Base.send(:include, ActiveRecordUnion)
「.join(") UNION (")
という無理やりな書き方↓がちょっと微笑ましい」「あるある😆」
# 同記事より
mapped_sql = relations
.map(&:to_sql)
.join(") UNION (")
参考: PostgreSQL12 7.4. 問い合わせの結合
「普段からActive Recordを使っていると、SQLのINTERSECTとかUNIONってそれほど使う機会なさそうな気はしますけどね」「pluck
でフェッチして差分取って検索し直すぐらいだったらSQLのINTERSECTで一発でキメたいときはあるといえばありますし」「まあこんなに苦労してやるぐらいだったら、記事冒頭みたいにBase.connection.execute()
使うか(データベース)ビューを実行したくなりますね☺️」「わかります」
「ただBase.connection.execute()
は返ってくる値が使いにくいので、できればビューを使いたいかな: でもビューだと今度は動的なことがやれなくなるのが痛し痒し...」「そうなんですよね😅」
⚓parallel_tests: RSpecやTest::Unitをパラレル化(Ruby Weeklyより)
# 同記事より
rake parallel:test # Test::Unit
rake parallel:spec # RSpec
rake parallel:features # Cucumber
rake parallel:features-spinach # Spinach
rake parallel:test[1] --> force 1 CPU --> 86 seconds
rake parallel:test --> got 2 CPUs? --> 47 seconds
rake parallel:test --> got 4 CPUs? --> 26 seconds
...
つっつきボイス:「parallel_tests、見たことある気がする👁」「ウォッチでまだ扱ったことがなかったので入れてみました」「使ったことないけど歴史長そう🏯」「RSpec用に似たようなgemって他にもあったと思いますし」
後で調べるとinitial commitは2009年でした。
「今は更新されなくなって無料になったRailscasts↓へのリンクがREADMEに貼ってあるぐらいなので相当昔からあるのはたしか」「ほんとだ」「これも今は使われなくなったzeus gemとかがあった時代ですし」「なつかしい」「間違いなく老舗ですね☺️」
- Railscasts: #413 Fast Tests (pro) - RailsCasts
「このparallel_tests gemとか、クックパッドさんが出してるrrrspecというRSpec並列化gem↓あたりが老舗かな」「なるほど」
⚓RaiilsのActiveModel::Error
で各バリデーションエラーをカプセル化
つっつきボイス:「この記事ではRails自体の機能でやってるんですが、これも知らなかったので😅」「どれどれ、errors
の構造がこういう配列になったのね↓」
user.errors.where(:contact_number)
=> [<#ActiveModel::Error attribute=contact_number, type=not_a_number, options={:value=>"abcdefghijk}>]
「どっかで見たな〜これ」「記事によるとこの#32313は昨年入ってたそうです↓」「ですよね、最近こうなった覚えがありますし😋」「ActiveModel::Errors
オブジェクトからActiveModel::Error
オブジェクトの配列に変わった」
# 同記事より
user.errors.add(:contact_number, :too_short, count: 10)
=> <#ActiveModel::Error attribute=contact_number, type=too_short, options={:count=>10}>
user.errors.where(:contact_number)
=> [<#ActiveModel::Error attribute=contact_number, type=not_a_number, options={:value=>nil}>, <#ActiveModel::Error
attribute=contact_number, type=too_short, options={:count=>10}>]
user.errors.added?(:contact_number, :too_short, count: 10)
=> true
user.errors.added?(:contact_number, :too_short)
=> false
user.errors.delete(:contact_number, :too_short, count: 10)
user.errors.where(:contact_number)
=> [<#ActiveModel::Error attribute=contact_number, type=not_a_number, options={:value=>nil}>]
user.errors.match?(:contact_number, :not_a_number)
=> true
user.errors.match?(:contact_number, :too_long)
=> false
「データモデルとしてはこの方が本来望ましい形ですね👍」「おぉ😍」
⚓delete_all
の挙動にびっくりした話(Hacklinesより)
つっつきボイス:「delete_all
してみたらUPDATEクエリが走ってびっくりしたそうです↓」「ああ、has_many
のときは参照のidがNULLでUPDATEされるみたいなヤツですね」「記事のAPIドキュメントにもそう書かれてますね😳」
:dependent
オプションで指定した戦略に応じてコレクションのレコードをすべて削除する。:dependent
が指定されていない場合はデフォルトの戦略に従う。
has_many
関連付けの場合、デフォルトの削除戦略は:nullify
になる。これは外部キーをNULLに設定する。同記事で引用した
delete_all
のAPIドキュメントより
「そういえばdelete_all
とdestroy_all
だと、前者はコールバックしないんだったっけ?」「そうそう、destroy_all
はコールバックを実行するんだった↓」
「destroy_all
はコールバックがある分、削除するレコード数が増えると当然めちゃ遅くなる可能性がある」「コールバック動かすにはeach
することになりますし」「ですよね」「delete_all
は1つのSQLクエリでやるからコールバックはできない分、当然速い🚅」「delete_all
とdestroy_all
の2つのメソッドがあるのには理由があるということで☺️」
メモ: レコードごとのインスタンス化、コールバック実行、削除は、多数のレコードを一気に削除しようとすると時間がかかることがある。1レコードにつき少なくともSQLのDELETEクエリがひとつ発行される(コールバックでさらに発行される可能性もある)。多数の行を短時間で削除したい場合、関連付けやコールバック関連の懸念がないのであれば
delete_all
を使う。同記事で引用した
destroy_all
のAPIドキュメントより
⚓Cable Readyでブラウザをリアルタイム更新(Ruby Weeklyより)
つっつきボイス:「Screencastsによる手順解説ですけど、最近Cable Readyを割と見かけるなと思って」「Action Cableといい感じに連携するヤツでしたっけ」
「cable_ready
っていうメソッド名なのね」「こういう書き方するのか〜」「include CableReady::Broadcaster
してるし」「チラ見した限りでは、Action CableがJSで出してきたブロードキャストを受けていい感じにリアルタイム更新してくれそう😋」
# サンプルコードより
class Card < ApplicationRecord
include CableReady::Broadcaster
after_update do
cable_ready["cards"].morph(
selector: "#" + ActionView::RecordIdentifier.dom_id(self),
html: ApplicationController.render(self)
)
cable_ready.broadcast
end
end
「JSのmorph
処理をモデルに書くとか、フロントエンジニアに睨まれそうな書き方😆」「morph?」「具体的には知りませんけど、名前からしていわゆるモーフィング的な動きをやれるヤツでしょうね」「あ、モーフィングですか」「引数がselector
とhtml
ですし、おそらく当該セレクタの内容をこの内容でモーフィング的に書き換えるんだろうな〜と推測しました」「なるほど!」
「まあApplicationRecord
を継承したモデルにこういう処理を書くのはちょっと考えちゃいますけど😅」
前編は以上です。
おたより発掘
Rails6でkamipoさん無双してそうなので、手元の古い5系Railsアプリもさっさと Rails 6 に上げたくなってきた // 週刊Railsウォッチ(20200622前編)AR attributes周りの高速化進む、Active RecordでUNIONクエリを書く、Cable Ready gemほか https://t.co/MofQuHvAky
— toshimaru (@toshimaru_e) June 23, 2020
バックナンバー(2020年度第2四半期)
週刊Railsウォッチ(20200616後編)本番環境をFullstaq Rubyに換えた理由、CSRF発生フローチャート、DBのトランザクション分離レベル比較ほか
- 20200615前編
rails new
にminimal
がマージ、ARの共通集合を取るand
、RubyMine+Dockerチュートリアル動画ほか - 20200609後編 Rubyにカスタマイズ可能な軽量fiberスケジューラを実験導入、RailsとGraphQL、DBについて知って欲しいことほか
- 20200608前編 RubyKaigi 2020が開催中止に、ネステッドSTIを避けるべき理由、rails newがインタラクティブになるかもほか
- 20200602後編 JSONストリームパーサーyajl-ruby、ruby-buildとopenssl、GoogleのCloud SQL、Rubyと機械学習ほか
- 20200601前編 Active Recordに新機能「delegated typing」追加、RuboCopのデフォルト設定アンケートほか
- 20200526後編 Rubyでよくやるスレッドバグ、Kubernetesでよくあるミス10、CSS/SVG/Canvasの使い分けほか
- 20200525前編 2020年のRailsマストgem 19個、スライド『Fat Modelの倒し方』、AR mergeのrewhereオプションを変更ほか
- 20200519後編 Rails 5と6のセキュリティ修正、Ruby 3.0のGuildがRactorに名前変更、Node作者によるDeno登場ほか
- 20200518前編 スライド『令和時代のRails運用』、Ruby 3.0のキーワード引数変更リスケ、Action CableのCLIほか
- 20200512後編 RubyのPStoreライブラリ、Lambda StoreのサーバーレスRedisは有能、Amazon Linux 2のライブパッチほか
- 20200511前編 Rails 6.0.3リリース、rails newに–masterオプションが追加、system specとfeature specの違いほか
- 20200428後編 Rubyのバックトレース順序が戻る、KubernetesでRailsをスケール、セキュリティソフト入れますか?ほか
- 20200427前編 Railsで避けたい8つのミス、ridgepole導入の注意点、RDS ProxyのPostgreSQL対応ほか
- 20200421後編 Ruby 2.4サポート終了、Ruby 3の右代入演算子、GitHubコア機能無料化ほか
- 20200420前編 anyway_config gemでRails環境設定、ShopifyのLiquidテンプレートエンジン、書籍『Beyond the Twelve-Factor App』ほか
- 20200414後編: Ruby 3で”endレス”メソッド定義構文が追加、ECMAScript 2020の新機能、紛失防止デバイスほか
- 20200413前編: 最近macOSでRailsが遅い、トランザクションでのreturnやbreakなどが非推奨化、Rails監視ツールリスト2020年度版ほか
- 20200407後編: RubyのTracePointでデバッグ、Rubyとモナド、Gitノウハウ集、リモートワークほか
- 20200406前編: Ruby 2.7.1セキュリティ修正、RailsビューHTMLにテンプレート名を出力、Action Mailboxテスト用フォーム改良ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。