- Ruby / Rails関連
週刊Railsウォッチ(20200720前編)10月開催「Kaigi on Rails」CFP募集中、enumにデフォルト値設定機能、RailsでBitemporal Data Modelほか
こんにちは、hachi8833です。情報処理技術者試験の秋の募集が始まりました。
つっつきボイス:「そうそう、春のは中止でしたね」「受験する方はどぞよろしく〜」
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
⚓臨時ニュース: Kaigi on RailsのCFPを7月末まで募集中
- サイト: Kaigi on Rails
10/3(土)に「Kaigi on Rails」という大きなイベントがオンライン開催されます🎉。お知らせいただきありがとうございました!🙇
公式サイトを更新しました!
Aboutページ(https://t.co/dQPHIm2UUD)には、Kaigi on Rails のコンセプトや来歴を。
CFPページ(https://t.co/FIwG15vmaa)には、テーマに沿ったCFPの具体例を掲載しました。
参加・応募の際に参考にしていただければ幸いです✨https://t.co/qSiZ0swHHL#kaigionrails— Kaigi on Rails (@kaigionrails) July 11, 2020
同イベントでは現在CFPを7/31まで募集中です。奮ってご応募ください!
また、本日からCFPを受け付けます。トークの募集は【7月31日(金)】までとなります。登壇歴や業務歴は問いませんので、たくさんのご応募をお待ちしております!詳細はCFPの募集ページをご確認ください。https://t.co/0dFLZ9IeJM
2/3— Kaigi on Rails (@kaigionrails) June 21, 2020
⚓Rails: 先週の改修(Rails公式ニュースより)
以下のコミットログのChangelogを中心に見繕いました。ところでGitHubのコミットログの表示が少し変わってタブがなくなりましたね。
⚓属性のデフォルトをコンフィグ可能にしつつ型を維持するようになった
class Post < ActiveRecord::Base
attribute :written_at, default: -> { Time.now.utc }
end
# Rails 6.0
Post.type_for_attribute(:written_at) # => #<Type::Value ... precision: nil, ...>
# Rails 6.1
Post.type_for_attribute(:written_at) # => #<Type::DateTime ... precision: 6, ...>
これは#39797の別案。
コンテキスト: #39797のコメント属性の既存の型をオーバーライドしようとする場合、オーバーライドに用いる型を明示的に指定するのが普通なので、現在の振る舞い(型の指定を省略すると既存の型情報を捨てる)が実用上かなり意味がないことについて同意する。また、これはほぼバグに近く、デフォルトを単にオーバーライドする方法がない。
そこで既存の型についての現在の振る舞いを変更する。修正なのでdeprecation期間は置かない。
#39797はcloseする。
kufu/activerecord-bitemporal/pull/57も参照。
同PRより大意
つっつきボイス:「そういえばいつの頃からかattribute
でdefault:
を指定できるようになってましたね」「それを指定したときに今までのようなType::Value
じゃなくてちゃんと中身の型を見るようになったということみたい」「type_for_attribute
で取るならこの方がありがたい🙏」
「変更点↓を見るとwhen Proc
ではもともとtype_for_attribute(name)
を返してたけど、それとシンボル以外はType::Value.new
を返してたのか」「type_for_attribute
で取り出し可能ならこの方がいいですね😋」「前は型の指定を省略すると既存の型情報がなくなってたんですね」「問答無用でValue.new
してたからそういうことでしょう」
# activerecord/lib/active_record/attributes.rb#L246
def load_schema! # :nodoc:
super
attributes_to_define_after_schema_loads.each do |name, (type, options)|
case type
when Symbol
adapter_name = ActiveRecord::Type.adapter_name_from(self)
type = ActiveRecord::Type.lookup(type, **options.except(:default), adapter: adapter_name)
when Proc
type = type[type_for_attribute(name)]
else
- type ||= Type::Value.new
+ type ||= type_for_attribute(name)
end
define_attribute(name, type, **options.slice(:default))
end
end
⚓Active Recordのenum
に_default:
でデフォルト値を設定できるようになった
# 同PRより
class Book < ActiveRecord::Base
enum status: [:proposed, :written, :published], _default: :published
end
Book.new.status # => "published"
つっつきボイス:「アンスコ付きの_default:
で書くのか」「ホントだ」「今までだとenumの初期化時に設定する感じだったのかな?まあ自分はPostgreSQLのenumはともかくRailsのenumってあんまり好きじゃないので使う機会少ないですけど」
# activerecord/lib/active_record/enum.rb#L160
def enum(definitions)
klass = self
enum_prefix = definitions.delete(:_prefix)
enum_suffix = definitions.delete(:_suffix)
enum_scopes = definitions.delete(:_scopes)
+
+ default = {}
+ default[:default] = definitions.delete(:_default) if definitions.key?(:_default)
+
definitions.each do |name, values|
assert_valid_enum_definition_values(values)
# statuses = { }
enum_values = ActiveSupport::HashWithIndifferentAccess.new
name = name.to_s
# def self.statuses() statuses end
detect_enum_conflict!(name, name.pluralize, true)
singleton_class.define_method(name.pluralize) { enum_values }
defined_enums[name] = enum_values
detect_enum_conflict!(name, name)
detect_enum_conflict!(name, "#{name}=")
attr = attribute_alias?(name) ? attribute_alias(name) : name
- decorate_attribute_type(attr, :enum) do |subtype|
+ attribute(attr, **default) do |subtype|
EnumType.new(attr, enum_values, subtype)
end
- API:
ActiveRecord::Enum
⚓受信メールのparamsをURLでRails Conductorフォームに渡せるようになった
- PR: Pass params to rails conductor form for inbound emails by excid3 · Pull Request #39842 · rails/rails
受信メールのテストは、Action Mailboxで使うためのfromやtoやsubjectやbodyといったフィールドをいつもRails Conductorフォームに入力しなければならないのがつらくなりがち。
このPRによって、以下のようなリンクを開けばフォームのフィールドに自動で入ってくれるようになる。
http://localhost:3000/rails/conductor/action_mailbox/inbound_emails/new?from=test@example.com
このURLを開くだけでいつでもRails Conductorで手動テストできるので時間を大いに節約できる。
同PRより大意
つっつきボイス:「Action Mailboxのテストというか動作確認が便利になるヤツですね: scaffoldでできるフォームにちまちま値を入れなくてもURL一発で確認できるようになった」「毎回入れるのはダルすぎますし」「地味だけどあると嬉しい機能😂」
⚓issue: 関連付けが2回オートセーブされて一意性違反になる
# 同issueより
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 has a unique index on player_id and team_id
belongs_to :player
belongs_to :team
end
player = Player.create!(teams: team])
player.update!(updated_at: Time.current)
ここからは以下のマイルストーンより見繕いました。
つっつきボイス:「今週はプルリクが少なめだったのでissueも掘ってみました」「ああ、関連付けのオートセーブが伝搬するヤツ」「after_create
のタイミングで起きるらしいとか書いてありました」「この辺はいろいろ微妙なところがあって、たとえば永続化してない同士を関連付けると明示的にsave
してないのにsave
されちゃったりするときがあるとか」「あ〜」「ActiveRecord::RecordNotUnique
で落ちるのはつらい」「これはバグでしょうね」「オートセーブ周りの挙動を把握するのは難しいので、どこで何が更新されるかがわかりにくいコードはなるべく書かないようにしてます」
後で見ると以下のプルリクがあることに気づきました。現時点ではopenですがそのうちマージされるかもですね。
#38166より前は
after_create
コールバックでレコードを1件保存すると、コールバック以後に定義されたコレクション関連付けはすべてオートセーブされないようになっていた。この修正はそれと似ているが別の問題を持ち込んだ。after_create
コールバックでレコードを1件保存するとその関連付けのレコードがjoinされて2回insertされるようになった。
コールバックでレコードが保存されるときに@new_record_before_save
の改変を回避する代わりに、直前の値をスタックに保存して後で戻す手が使える。これによって値は代入中や関連付けのオートセーブ時に改変されなくなり、かつ関連付けが複数回オートセーブされることも防げる。
同PRより
# activerecord/lib/active_record/autosave_association.rb#L365
- def before_save_collection_association
- @new_record_before_save ||= new_record?
- end
+ def around_save_collection_association
+ previously_new_record_before_save = (@new_record_before_save ||= false)
+ @new_record_before_save = !previously_new_record_before_save && new_record?
- def after_save_collection_association
- @new_record_before_save = false
+ yield
+ ensure
+ @new_record_before_save = previously_new_record_before_save
end
⚓issue: _rails6_session
cookieが近々rejectされる
つっつきボイス:「今年5月頃のissueですが、プルリクの下の方にChromeもついに変更を再開するとありました」「たしかFirefoxはもう変更されてたかな」「_rails6_session
cookieがリジェクトされたらびっくりしそう」「たぶん今回の変更で世の中にあるサイトもいくつか動かなくなるかも」
メモ: 安全ではないサイト (http:) は "secure" ディレクティブを付けてクッキーを設定することができなくなりました (Chrome 52 以降および Firefox 52 以降の新機能).
developer.mozilla.orgより
参考: 【一問一答】 Chrome の SameSite Cookie の変更とは? : Cookie への新たなアプローチ | DIGIDAY[日本版]
⚓Rails
⚓activerecord-bitemporal: ActiveRecordでBitemporal Data Modelを扱う
つっつきボイス:「珍しくREADMEが日本語で、@kamipoさんもコントリビュータに入ってますね」「bitemporal?」「履歴的なデータを持たせるということなのかな: 『レコードを読み込む場合は暗黙的に一番最新のレコードを参照します』とあるから現在の値が取れるということか」
employee = nil
Timecop.freeze("2019/1/10") {
employee = Employee.create(emp_code: "001", name: "Jane")
}
Timecop.freeze("2019/1/15") {
employee.update(name: "Tom")
}
Timecop.freeze("2019/1/20") {
employee.update(name: "Kevin")
}
Timecop.freeze("2019/1/25") {
# 現時点で有効なレコードのみを参照する
pp Employee.count
# => 1
# name = "Tom" は過去の履歴レコードとして扱われるので参照されない
pp Employee.find_by(name: "Tom")
# => nil
# 最新のみ参照する
pp Employee.all
# => #<Employee:0x000055ee191468a8
# id: 1,
# bitemporal_id: 1,
# emp_code: "001",
# name: "Kevin",
# valid_from: 2019-01-20 00:00:00 UTC,
# valid_to: 9999-12-31 00:00:00 UTC,
# deleted_at: nil>
}
「bitemporalという呼び方は知らなかったけど、これはいわゆるデータベースで時系列データを扱うときのベストプラクティスのひとつですね」「あ、そういうものなんですか」「データは時系列データなんですけど、参照するときは2種類の取り方があります: たとえば普通にJOINするときなんかだと時系列と無関係に現在有効な最新のものだけを取りたいなんてこともよくあるので、そのあたりのデータのルールを事前に決めておかないとうまく取り出せなかったりすることがあるわけです」「ふむふむ」「このデータベース設計はまったくRailsに限らない、普通によく使われる手法です」「それをやりやすくするgemということなんですね」
これにより DB 上に複数の履歴レコードや論理削除されているレコードがあっても『現時点で有効な』レコードが参照されます。
同リポジトリより
employee = nil
Timecop.freeze("2019/1/10") {
employee = Employee.create(name: "Jane")
}
Timecop.freeze("2019/1/15") {
employee.update(name: "Tom")
}
Timecop.freeze("2019/1/20") {
# DB 上では履歴レコードや論理削除済みレコードなどが複数存在するが、暗黙的にクエリが追加されているので
# 通常の ActiveRecord のモデルを操作した時と同じレコードを返す
pp Employee.count
# => 1
pp Employee.first
# => #<Employee:0x000056230b1ecdf8
# id: 1,
# bitemporal_id: 1,
# emp_code: nil,
# name: "Tom",
# valid_from: 2019-01-15 00:00:00 UTC,
# valid_to: 9999-12-31 00:00:00 UTC,
# deleted_at: nil>
# 更新前の名前で検索しても引っかからない
pp Employee.where(name: "Jane").first
# => nil
# なぜなら暗黙的に時間指定のクエリが追加されている為
puts Employee.where(name: "Jane").to_sql
# => SELECT "employees".* FROM "employees" WHERE "employees"."valid_from" <= '2019-01-20 00:00:00' AND "employees"."valid_to" > '2019-01-20 00:00:00' AND "employees"."deleted_at" IS NULL AND "employees"."name" = 'Jane'
}
「こういう設計にしておかないとうまくできないことがあります: たとえば商品情報として何月何日から販売して何月何日に販売終了するというように期間限定で販売するとか、逆にたとえば年末年始の期間だけ販売しないとか」「おぉ」「よくある適用開始日とか適用終了日とかを設定するもので、業務アプリでは非常によく使われますね」「ちなみに手元の串刺し辞書を引くと解剖用語しか出てきませんでした↓」
bitemporal: {形} 《解剖》: 両側頭骨の
つっつき後に以下のスライドを見つけました。
⚓Shouldaマッチャーのバリデーションマッチャーしか使っていない理由(Ruby Weeklyより)
- 元記事: Why validation matchers are the only Shoulda matchers I use - Code with Jason
- リポジトリ: thoughtbot/shoulda-matchers: Simple one-liner tests for common Rails functionality
つっつきボイス:「Shouldaにいろいろ機能はあるけど自分はバリデーションマッチャーで十分、というような記事みたい」「RSpecでいろんなマッチャーを使っているとだんだん訳わからなくなってくることはよくある」「『テストしたいのは振る舞いであって実装ではない』『バリデーションは機能だからバリデーションマッチャーでテストする意味がある』みたいなことを書いてますね」「記事にもうちょっとコード例が多ければ楽に読めたかな😆」
記事の要点:
テストでよく疑問を寄せられるのがShouldaマッチャー。Shouldaマッチャーを使っているかとか、はたしていいものなのかとか。残念ながらShouldaマッチャーの大半は自分が使うことのないものだ。
たとえばActive Recordの関連付けが存在しているかどうかのテストだけを書くことにはあまり意義を感じられない。関連付けの存在テストと関連付けがあることでの振る舞いテストが両方あるとしたら前者に価値はないし、Postの関連付けの存在テストがあってもPostの振る舞いテストがなければあまり意味がない。自分は実装(が存在するかどうか)よりも振る舞いをテストしたい。
しかしバリデーションマッチャーは別。バリデーションはそれ自体が目的であり機能なのでShouldaマッチャーでテストする意義があると思う。Shouldaを使わないとしたら、バリデーションテストをきっぱり書かないか延々とバリデーションテストを書くかになるけど、自分はどちらも良くないと思うので、Shouldaに意義があるとすればやはりバリデーションマッチャーの部分だと思う。
⚓discard: 論理削除gem(Ruby Weeklyより)
# 同リポジトリより
Post.all # => [#<Post id: 1, ...>]
Post.kept # => [#<Post id: 1, ...>]
Post.discarded # => []
post = Post.first # => #<Post id: 1, ...>
post.discard # => true
post.discard! # => Discard::RecordNotDiscarded: Failed to discard the record
post.discarded? # => true
post.undiscarded? # => false
post.kept? # => false
post.discarded_at # => 2017-04-18 18:49:49 -0700
Post.all # => [#<Post id: 1, ...>]
Post.kept # => []
Post.discarded # => [#<Post id: 1, ...>]
つっつきボイス:「論理削除のgem来た」「新しめの割に★も1000超えてる」「正しくやれるとあるけど」
「あ〜、このundiscard
↓を許すかどうかがめちゃ考えどころなんですよ...」「discardを取り消すってことですね」「二重否定っぽいところがちょっと嫌な予感😅」「だいたいundiscard
的なことをやるとおかしくなりがちで、論理削除はそのものはまだしも、論理削除を取り消すとデータの整合性を維持するのが難しくなってくることが多い」「う〜む」
# 同リポジトリより
post = Post.first # => #<Post id: 1, ...>
post.undiscard # => true
post.undiscard! # => Discard::RecordNotUndiscarded: Failed to undiscard the record
post.discarded_at # => nil
「まあ論理削除したい人はだいたい後で戻したがる気がしますけど😆」「戻したかったら履歴で持っておいてまた作り直すという考え方もありますし」「単なるログデータぐらいだったら戻してもいいかなという気はしますけど」「データがロジックとかコールバックと結びついているときにundiscard
すると割と地獄を見る👹」
「Railsはデータの制約をアプリケーション側に持たせようとする傾向があるので、論理削除はRailsとは特に相性悪そうな気がするんですよ」「自分もそう思います」「データベースだったらトリガーでやるようなことをRailsだとコールバックでやったりしますし」「データベースレベルで制約がかかっていれば、仮にdiscard
やundiscard
に失敗しても整合性は失われませんけど」「まあ論理削除を使うなら慎重にやらないと」「でないと思いもよらないところでぬるぽが出るかもですし」「参照が失われるとつらい...」
参考: ぬるぽ - Wikipedia
「READMEの下の方に、なぜacts_as_paranoidとか↓を使わないかみたいなこと書いてますね」「paranoid系の論理削除gemはActive Recordのメソッドをオーバーライドするのが怖いんですよ」「delete
とかdestory
のオーバーライドはちょっとね...😅」
- リポジトリ: rubysherpas/paranoia: acts_as_paranoid for Rails 3, 4 and 5
- リポジトリ: ActsAsParanoid/acts_as_paranoid: ActiveRecord plugin allowing you to hide and restore records without actually deleting them.
「一方このdiscard gemは論理削除メソッドを別に用意しているので、アプリケーションエンジニアが『今自分は論理削除しようとしているゾ』と意識できるようにするという発想でしょうね」「その方がお行儀がよろしい😋」「やるならdiscardのやり方の方がましかな〜」「少なくとも気が付かないうちに論理削除してしまうことは減るでしょうね」
⚓RubyやRailsでコードを共有する方法おさらい(Hacklinesより)
つっつきボイス:「RubyやRailsでコードを共有する方法をカタログ的に書いた記事です」「昔ながらのRuby継承とか」「STIとか」「MIXINとか」「Module#included
とか」「デリゲートとか」
「Rubyのdef_delegator
って知らなかった〜」「へぇ〜」「へぇ〜」「まああってもいいですよね😋」
参考: Forwardable#def_delegator
(Ruby 2.7.0 リファレンスマニュアル)
「まあRailsにはdelegate
が前からありますけど↓」「Active Supportにあるヤツ」「記事にもあった」
参考: Module#delegate
「ところでこの記事には不思議にActiveSupport::Concern
の話が載ってませんね」「あらホントだ」「モジュールインクルードの話があるならありそうなものですけど」
記事見出しより:
- 昔ながらのRuby継承
- STI
- クラスやモジュールに
include
する extend
でオブジェクトをミックスModule#included
- Rubyクラスレベルのデリゲート:
Forwardable
SimpleDelegator
- Railsの
delegate
(シンタックスシュガー)
前編は以上です。
バックナンバー(2020年度第3四半期)
週刊Railsウォッチ(20200714後編)ruby-warning gemでワーニングを手軽に抑制、rubocop -aの振る舞いが変わる、書籍『MySQL徹底入門 第4版』ほか
- 20200713前編 rspec-openapiでスキーマ自動生成、Rails Architect Conf動画、
where()
ハッシュキーに比較演算子条件を書ける機能ほか -
20200707後編 Rubyで無名structリテラル提案、書籍『AWS認定ソリューションアーキテクト』、21世紀のC言語ほか
- 20200706前編 Railsでのマルチテナンシー実装戦略を比較、Railsでサブクエリを使う、URI.parserが非推奨化ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
PRにあるサンプルコードの`default: => { Time.now.utc }`という書き方は、クラス読み込み時ではなく実行時に動的に現在時刻を設定するときに必要なlambdaの使い方で、`default:`以外のDSLでもよく使われる書き方です。
この書き方をしなかった場合、Rails server起動時の時刻が常に設定されてしまうというRails初心者あるあるなバグを作りこんでしまうことになりますので気を付けましょう。