@willnet
- 6.0.0 (2019/08/06)
- 5.2.0 (2018/04/09)
- 5.1.0 (2017/04/27)
- 5.0.0 (2016/06/30)
だいたい1年くらいでマイナー、メジャーバージョンが上がる
RailsConf(毎年だいたい4末くらいに開催)がターゲットになっていそう
- https://github.com/rails/rails/pulls?q=is%3Aopen+is%3Apr+milestone%3A6.1.0
- 一応6.1.beta1用のブログエントリの準備用PRはある(けどいまアクティブではない)
- basecampの面々が hey.com の開発で忙しかったという噂。今年中に出るといいな…。
6.1のリリースはまだだけど、この1年ですでにmasterにマージされた機能はたくさんあるので、今日はそのうちの一部を紹介します ✋(6.0の機能を知りたい人はパーフェクトRuby on Railsという素敵な本があるのでそちらを御覧ください)
- .annotate_template_file_names annotates HTML output with template names by joelhawksley · Pull Request #38848 · rails/rails
- Add the configuration option for annotating templates with file names to the generated app by prathamesh-sonpatki · Pull Request #39204 · rails/rails
config.action_view.annotate_rendered_view_with_filenames = true
- とすると有効になる(デフォルトだと config/environments/developmrnt.rb でコメントアウトされた状態になっている)
- べんり!
- ただし現時点ではERBのみ対応
- PRチャンスかも
- config/routes.rb の内容を別ファイルに外だしできるようになった
- 昔(Rails 4.0がリリースされる前)入った内容なんだけどDHHのお気に召さなかったようでrevert→6年たってDHHの気が変わった模様
- これまでもゴニョゴニョすればできたけど、公式のやり方が提供されたので安心して使えるようになったのがよいですね
↓のように書く
# config/routes.rb
Rails.application.routes.draw do
draw(:admin)
end
# config/routes/admin.rb
get :foo, to: 'foo#bar'
routes.rbが大量にあって、かつ分けやすい箇所がある(ex: 管理画面、 API)のであれば試してみるとよいのでは
- idを暗号化、かつ改ざん検知ができるトークンを付与したものを生成するsigned_idメソッドと、それを使ってfindできるfind_signedメソッドが追加されました
- トークンに有効期限を含めることもできます
signed_id = User.first.signed_id(
expires_in: 15.minutes, purpose: :password_reset
)
User.find_signed(signed_id) # => nil (purposeがない)
User.find_signed(signed_id, purpose: :password_reset) # => User.first
# 16分後...
User.find_signed(signed_id, purpose: :password_reset) # => nil (expireした)
User.find_signed!("bad data") # => エラー
コード例のように、パスワードリセット機能などを作るときに便利では
strict_loadingメソッド経由で取得したオブジェクトは、以後SQLの発行を伴う関連を呼び出せない(呼び出すとエラー)。これによってincludesなどのeager loadを強制させることができる。
user = User.strict_loading.first
user.posts.to_a
#=> ActiveRecord::StrictLoadingViolationError
user = User.strict_loading.includes(:posts).first
user.posts.to_a #=> OK
関連先にもstrict_loadingは伝播する。
user = User.strict_loading.includes(:posts).first
user.posts.first.comments
#=> ActiveRecord::StrictLoadingViolationError
関連のオプションとして設定することもできる。↓のようにすると、posts関連はincludesやpreloadなど経由でしか呼び出せない。
class User < ApplicationRecord
has_many :posts, strict_loading: true
end
user = User.first
user.posts.first #=> ActiveRecord::StrictLoadingViolationError
class User < ApplicationRecord
self.strict_loading_by_default = true
has_many :posts
end
user = User.first
user.posts.first
# => ActiveRecord::StrictLoadingViolationError Exception
↓のどちらかで、AR全体にstrict_loading_by_defaultを設定することもできる
ActiveRecord::Base.strict_loading_by_default = true
Rails.application.config.active_record.strict_loading_by_default = true
これはあえてstrict_loadingにしたくないんですよ、という場合は次のようにできる
user = User.strict_loading(false).first
user.posts.to_a #=> ok
strict_loadingを使うとpreloadが強制されて、N+1を防ぐことができるぞ!というのは便利そうだけど、代わりに↓のように無駄にオブジェクトを作ってしまうケースが出てきそうですね。なにも考えずにコードを書けるようになるわけではない。
user = User.includes(:posts).first
user.posts.last
#=> 一つだけPostがあればいいのに全件分のPostオブジェクトを生成してしまう!
- Rails6.0でプライマリ/レプリカの複数DBに対応した
- Rails6.1ではそれを拡張して、shardingにも対応
6.0では次のように、プライマリ/レプリカを指定していた
class ApplicationRecord < ApplicationRecord
connects_to database: { writing: :primary, reading: :secondary }
end
connects_to
メソッドに、次のようにshardingの定義を書けるようになった。- 見てわかるようにshardingとprimary/replicaを併用できる
- 6.0の記法からから1階層増えた感じ
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to shards: {
default: { writing: :primary, reading: :primary_replica },
shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica }
}
end
6.0のときと同じようにconnected_toでDBを切り替える
ActiveRecord::Base.connected_to(shard: :default) do
@id = Record.create! # デフォルトのshardのDBでレコードを作成する
end
ActiveRecord::Base.connected_to(shard: :shard_one) do
Record.find(@id) # もう一つのshardからfindしているのでレコードは見つからない
end
roleとshardを一度に両方切り替えることもできる
ActiveRecord::Base.connected_to(role: :reading, shard: :shard_one) do
Record.first
end
- Railsがバージョンアップしたときにdeprecatedになったメソッドを実行すると、deprecation warningなメッセージが表示される
- メッセージを表示する代わりに、例外を発生させることができるようになった
次のように、対象となるdeprecation warningsを指定できる
- warningの文章の一部を文字列もしくはシンボルで指定する
- warningの文章にマッチする正規表現を指定する
ActiveSupport::Deprecation.disallowed_warnings = [
"bad_method",
:worse_method,
/(horrible|unsafe)_method/,
]
全部対象にしたい場合は次のようにする
ActiveSupport::Deprecation.disallowed_warnings = :all
対象になったらどうするか、を次のように指定できる。production以外は例外を発生させて、productionはログに残すだけの例。
if Rails.env.production?
ActiveSupport::Deprecation.disallowed_behavior = [:log]
else
ActiveSupport::Deprecation.disallowed_behavior = [:raise]
end
deprecatedで例外を発生させる設定をデフォルトにしたんだけど、ここはまだdeprecatedなメソッドを使いたい、というようなときには明示的にそれを許可することもできる。
ActiveSupport::Deprecation.allow do
User.do_thing_that_calls_bad_and_worse_method
end
引数で許可するものを絞り込むこともできる
ActiveSupport::Deprecation.allow [:bad_method, "worse_method"] do
User.do_thing_that_calls_bad_and_worse_method
end
条件を満たすときだけ許可することもできる
ActiveSupport::Deprecation.allow [:bad_method], if: Rails.env.production? do
User.do_thing_that_calls_bad_method
end
deprecation warningに頑張って対応したPRをマージしたあとに、すぐまた別のPRでdeprecation warningが発生する、ということを防ぐことができて便利!
Active Storageでアップロードしたファイルへのアクセスは次のような挙動だった
- まずクライアントがRailsサーバへリクエストを送る
- 時間制限(デフォルト5分)つきのファイルへのURLを生成してリダイレクトする
- ファイルをダウンロードする
この手法は制限付きのリンクがいい感じに生成されてべんりなのだけど、広く公開して問題ないようなファイルだと無駄が多い。
↓のようにするとファイルの中身を直接返すURLを生成する(設定でデフォルトの挙動を変更することもできる)。
<%= image_tag rails_storage_proxy_path(@user.avatar) %>
これまで、Active Storageでサムネイルを表示するときには次のような挙動になっていた(ストレージがS3だと仮定)
- サムネイルがS3に存在するかどうかをチェック
- 存在しなければ元画像をダウンロードしてサムネイルを作りS3にアップロードする
- サムネイルの画像用のURLを作成する
- この方式は次の問題があった
- 毎回S3にサムネイルが存在するか確認しにいくので、その分レスポンスを返すまでに時間がかかる
- S3の仕様的に、存在確認後にすぐアップロードすると、しばらくの間アップロードしたのにファイルがない、ということになる
- たぶん存在確認の結果をしばらくキャッシュしてるんじゃないかな…
- これを解決するために、サムネイルが存在しているかどうかを保持しておくテーブルが新設された
Rails.application.config.active_storage.track_variants = true
とすると新しいやりかたが有効になる
ActiveModel::Errorsとは、user = User.new; user.errors
のようにすると返ってくるオブジェクト
- ActiveModel::Errorsは実質的に2つのHashオブジェクトから構成されている
- messages
- ex:
{:email=>["を入力してください"]}
- ex:
- details
- ex:
{:email=>[{:error=>:blank}]}
- ex:
- messages
- Hashをゴニョゴニョするのがめんどくさいケースが有る
- ex: 特定のメッセージに紐づくdetailを取得したいときに、messagesのindexを取得して
details[:email][index]
としなければいけない
- ex: 特定のメッセージに紐づくdetailを取得したいときに、messagesのindexを取得して
- 今回の修正により、ActiveModel::Errorsは実質的にActiveModel::Errorオブジェクト(新設)の集合となった
- これによりエラー内容に対しての細かい操作が、よりオブジェクト指向っぽい書き方ができるようになった(Hashもオブジェクトではあるけどね)
- 関連先のエラーもいい感じに表現できている(ActiveModel::Errorsのネストができる)
- Errorsにwhereメソッドが生えた
model.errors.where(:name, :foo, bar: 3).first
- messageに紐づくdetailが簡単に取得できるようになった
- 非互換な変更なので対応が必要なものもある
book.errors[:title] << 'is not interesting enough.'
のような、直接Hashをいじるような操作はdepricatedになったbook.errors.add(:title, 'is not interesting enough.')
のように書く
book.errors.each do |attribute, error_message| ...
は depreatedbook.errors.each do |error|
のように変更する
- 関連モデルを大量に保存するようなアプリケーションだと、エラー内容の解析やらなんやらが楽になってよいのでは
関連先が存在しないレコードを取得したいとき、次のようなコードをかくと思います。
Post.left_joins(:author).where(authors: { id: nil })
これを次のように短縮して書けるようになりました
Post.where.missing(:author)
次のように引数に複数の関連先を書くこともできる(authorとcomments両方とも存在しないレコードが返る)
Post.where.missing(:author, :comments)
add_check_constraint :products, "price > 0", name: "price_check"
remove_check_constraint :products, name: "price_check"
create_table :distributors do |t|
t.string :zipcode
t.check_constraint "zipchk", "char_length(zipcode) = 5"
end
- MySQLの場合は8.0.16以降サポート
- そもそもMySQLは8.0.16までcheck制約に対応していなかった
- これまでも直接SQLを発行すればcheck制約を追加できたけど、その場合はschema.rbをやめてstructure.sqlにする必要があった
class Book < ActiveRecord::Base
enum status: [:proposed, :written, :published], _default: :published
end
Book.new.status # => "published"
キーが_default
なのは「既存のカラムにdefaultがあったときに壊れるから」とのこと
- CSRFトークンのエンコード方式がbase64からurlsafe版のbase64に変更されました
- base64はa-z,A-Z,0-9,+,/でエンコードする方式
+
と/
はURLエンコードしないといけない→それぞれ-
と_
に変更した- Rubyにはurlsafe版のbase64エンコード用メソッドがある
Base64.urlsafe_encode64
- CSRFトークンをcookieとしてそのまま送りたいようなケースで、クライアント側でエンコード・デコードが必要になるのがだるいのでこうなったとのこと
僕らの生活的にはあまり影響はないのだけど、「6.1アップグレード時にCSRFトークンのエンコード方式が変わる」というのは影響がある
- ローリングアップデートなどで6.0と6.1のサーバが混在しているタイミングがあると、エンコード形式とデコード形式が変わってCSRFトークンのチェックがうまくいかずにエラーになる
- 6.1にアップグレードしたあとに障害があり、6.0にロールバックしたらエンコード形式とデコード形式が変わって(ry
- 6.0形式でエンコードされたものを6.1でデコードすることはできるようになっている
Rails.application.config.action_controller.urlsafe_csrf_tokens = true
で挙動を切り替えることができるので、いったんfalseの状態で6.1にアップグレードし、折を見て一気にtrueにすればローリングアップデートでも大丈夫
cookieのシリアライズの設定をmarshalからhybridに変更するとき、hybridはjsonでシリアライズするので、ローリングアップデートするとjsonでシリアライズしたものをmarshal設定でデシリアライズしてエラーになる可能性がある
- Rails5.1->5.2でcookiesの暗号化形式が変わった
- Rails.application.config.action_dispatch.use_authenticated_cookie_encryption で設定可能
- ↑を変更してローリングアップデートしようとすると、新しい暗号化形式で暗号化したものをで古い暗号化形式で復号化しようとしてエラーになる
- 6.1では古い設定のサーバに新しい設定のリクエストがきてもよしなにしてくれるようになり、深く考えずにローリングアップデートできるようになった
- Rails6.1までのmergeは同じカラムがmergeされるときに、Hashのように後勝ち(mergeの引数側が採用される)になるケースと、ならないケース(両方のクエリが統合される)がある
- 後勝ちになったりならなかったりするのは意図的なものではないので後勝ちになるように修正する
- 6.1では後勝ちにならないケースでdeprecateメッセージが表示され、6.2でデフォルト後勝ちになる
- 6.1で6.2の挙動(後勝ちにしたければrewhereオプションを指定する)
# Rails 6.1 で後勝ちになるパターン(IN句)
Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]
# Rails 6.1 で両方の指定が採用されるパターン
# where id between "davidのid" and "maryのid" and id = bobのid
Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => []
# 6.1で6.2相当の挙動を使うにはrewhereを使う
Author.where(id: david.id..mary.id).merge(Author.where(id: bob), rewhere: true) # => [bob]
# Rails 6.2ではどのようなケースでも後勝ち
Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]
Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => [bob]
david_and_mary = Author.where(id: [david, mary])
mary_and_bob = Author.where(id: [mary, bob]) # => [bob]
david_and_mary.merge(mary_and_bob) # => [mary, bob]
david_and_mary.and(mary_and_bob) # => [mary]
david_and_mary.or(mary_and_bob) # => [david, mary, bob]