こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
- 公式更新情報: Ruby on Rails — The Rails Foundation, Stimulus Outlets API, bug fixes and lots of improvements!
🔗 readonlyの属性に代入するとActiveRecord::ReadonlyAttributeError
を発生するようになった
- PR: Raise on assignment to readonly attributes by hmcguire-shopify · Pull Request #46105 · rails/rails
# (修正後の挙動) class Post < ActiveRecord::Base attr_readonly :content end Post.create!(content: "cannot be updated") post.content # "cannot be updated" post.content = "something else" # => ActiveRecord::ReadonlyAttributeError
従来は、こうした代入はエラーにならずに成功するがデータベースには書き込まれなかった。
この振る舞いを以下のコンフィグで制御可能になった。config.active_record.raise_on_assign_to_attr_readonly = true
また、この振る舞いは
load_defaults 7.1
でデフォルトで有効になる。
Alex Ghiculescu, Hartley McGuire
同Changelogより
つっつきボイス:「readonlyの属性にうっかり代入しちゃうの、あるある」「ちょうど最近そういうバグを踏んだので、これは嬉しい👍」「Rails 7.1からはエラーを出すのがデフォルトになるんですね」「プロジェクトによっては7.1でbreaking changeになる可能性があるので、この改修は知っておきたい」「プロジェクトのコードがreadonlyに代入していなくても、gemが代入する可能性はありそうですね」
🔗 関連付けのpreload
やeager_load
をunscope
できるようになった
概要
関連付けのpreload
やeager_load
をunscope
する機能を追加する。
これは、unscope(:includes)
(クエリ全体の複雑さに応じてpreload
またはeager_load
を選ぶ)と同じ機能である。
その他情報
関連付け読み込みの種別が明確な場合(eager_load
およびpreload
)は、その明示的な関連付けをunscope
できるようになる。これは、has_many
関連付けが明示的にpreload
またはeager_load
を要求する場合に既存のクエリを取得して集計を実行するのに便利である。以下の例では、2つのhas_many
関連付けをunscope
すると、余分なpreload
クエリとeager_load
のJOINが削除される。query.eager_load!(:has_many_association1) query.preload!(:has_many_association2) query.unscope(:eager_load, :preload).group(:id).select(:id)
select
がpreload
の関連付けキーを含まない場合は、集計とselect
次第で明示的なpreload
が失敗するケースがある。この問題がきっかけでこの修正を作成した。
同PRより
つっつきボイス:「関連付けのpreload
やeager_load
を今までunscope
できなかったのか」「そういうunscope
ってできないかと思ってたけど、言われてみれば最終的なArelを組み替える形でできそうではありますね」「unscope
はあまりやりたいと思ったことなかったけど、特定のeager loadingを外してクエリを軽くしたいときなんかに使いたいこともあるので、あると嬉しいのはわかる👍」
「unscope
そのものは昔からあるメソッドですね」「ORDER BYを外したいときとかに使いますね」
参考: Rails API unscope
-- ActiveRecord::QueryMethods
「unscope
というと、default_scope
をunscope
してハマった記事↓を思い出します」「複数のwhere
節が設定されているときにそのうちの1つをunscope
するつもりでunscope(:where)
したら、unscope
するつもりのなかったwhere
条件もunscope
されてしまったりするので注意が必要ですね」
🔗 #inspect
実行時に暗号化済み属性を自動的にフィルタで除外する機能が追加された
- PR: Add filtering of encrypted attributes in #inspect by skipkayhil · Pull Request #46453 · rails/rails
この機能はデフォルトで有効になるが、以下のコンフィグで無効にできる。
config.active_record.encryption.add_to_filter_parameters = false
Hartley McGuire
同Changelogより
つっつきボイス:「暗号化済み属性ってinspect
でフィルタされていなかったのか」「add_to_filter_parameters
というコンフィグが追加されて、デフォルトで有効になるんですね」「developmentモードでフィルタを外したくなることはありそう」
「これと直接関係はありませんが、前に取り上げたaudits1984やconsole1984のことをちょっと思い出しました(ウォッチ20211004)」「コンソールの保護と監査を行う37signals(旧Basecamp)のgemですね」
🔗 ActiveRecord::Relation
の#first_or_initialize
や#create_or_initialize
の実行時に暗号化済み属性を初期化するよう修正
修正対象: #46151(問題についてはこちらを参照)
ここでの問題は、where(...).first_or_create
のような書き方をするときに、first_or_create
で以下のようにwhere
の"等しさ"(equality)に関する述語(trueかfalseとして扱われるもの)に該当するものだけをActive RecordがSELECTするが、
# https://github.com/rails/rails/blob/90cba59ddd26a1fbbad85e9a9810583e2de85279/activerecord/lib/active_record/relation.rb#L754
hash = where_clause.to_h(klass.table_name, equality_only: true)
暗号化機能は、
where
でそれに対応する述語を、その属性で取りうる値の配列に変換する(キーやアルゴリズムのローテーションをサポートするのが目的)。
# https://github.com/rails/rails/blob/90cba59ddd26a1fbbad85e9a9810583e2de85279/activerecord/lib/active_record/encryption/extended_deterministic_queries.rb#L66-L74
when String, Array
list = Array(value)
list + list.flat_map do |each_value|
if check_for_additional_values && each_value.is_a?(AdditionalValue)
each_value
else
additional_values_for(each_value, type)
end
end
これは上述した"等しさ"の述語でない形になり、無視されてしまう。
そこで、受け取った暗号化済み属性を考慮するためにscope_for_create
を手動で拡張する必要がある。
この説明で多少なりとも明確になるとよいが。
cc @jorgemanrubia(この機能の最初の実装者)
同PRより
参考: Ruby用語集 (Ruby 3.1 リファレンスマニュアル)
述語メソッド
predicate method
返り値を真偽値として用いるためのメソッド。メソッド名の末尾に?
を付ける習慣がある。
true
/false
を返すとは限らず、真である場合に、true
以外のオブジェクトを返すことで、単なる真偽を越えた情報を与えるものもある。
docs.ruby-lang.orgより
つっつきボイス:「どちらもRails 3.2ぐらいの頃からあるメソッド」「属性が暗号化されている場合に整合性が取れていなかったバグを修正した感じですね」「#first_or_initialize
や#create_or_initialize
はAPIドキュメントがないのか↓」
# https://github.com/rails/rails/blob/656118bc17791291db7c10075b489dfc6f93fce3/activerecord/lib/active_record/relation.rb#L121
def first_or_create(attributes = nil, &block) # :nodoc:
first || create(attributes, &block)
end
def first_or_create!(attributes = nil, &block) # :nodoc:
first || create!(attributes, &block)
end
def first_or_initialize(attributes = nil, &block) # :nodoc:
first || new(attributes, &block)
end
参考: Ruby on Rails 3.2 リリースノート - Railsガイド -- first_or_create
、first_or_create!
、first_or_initialize
🔗Rails
🔗 Railsのルーティングの高度なconstraints
(RubyFlowより)
つっつきボイス:「ルーティングのconstraints
は普通に書くぶんにはいいけど、思い通りのルーティングにしようとすると割と大変」
参考: 3.8 セグメントを制限する -- Rails のルーティング - Railsガイド
「こういうふうにconstraints
にlambdaを渡してカスタマイズするのは割とよくある↓」「ここではリクエスト元IPアドレスを元に制限しているんですね」「この辺はググるといろいろ見つかりますよ」
# 同記事より
get '*path', to: 'restricted_list#index',
constraints: lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) }
「そうそう、こういうふうにカスタムクラスでmatches?
を定義することもよくあります↓」
# 同記事より
class ShortenerRouteConstraint
def matches?(request)
# if request.path is not in static routes it should be a short link
static_page_routes.exclude?(request.path)
end
private
def static_page_routes
# dynamically pull all static pages + admin, 404, and errors
static_page_paths = Dir.new("app/views/static").children.map do |f|
"/#{File.basename(f, ".html.erb")}"
end
static_page_paths << "/admin"
static_page_paths << "/404"
static_page_paths << "/500"
end
end
「動的に制約をかけたい場合は、定数やリテラルではなくlambda渡しや#matches?
を実装したオブジェクトを渡すことで実現できます」「なるほど」
🔗 RSpecがときどき失敗する問題(Ruby Weeklyより)
つっつきボイス:「ActiveSupport#descendants
が遅いからうんと速くしたらたまにテストが落ちるようになったらしい」「CachedDescendants
で子孫クラスをキャッシュから探索するようにしてたのか↓」
# 同記事より
module Mixins
module CachedDescendants
extend ActiveSupport::Concern
cattr_accessor :descendants_map
self.descendants_map = Concurrent::Hash.new
class << self
# Clears the descendants map cache - can be hooked to Rails reloader
def reload!
descendants_map.clear
end
end
included do
class << self
# @return [Array<Class>] array with descendants classes
def cached_descendants
::Mixins::CachedDescendants.descendants_map[self] ||= descendants
end
end
end
end
end
「ActiveSupport#descendants
を毎回たどらなくていいようにすれば確かに爆速になるけど、見るからにマルチスレッドで壊れそうなコード」「一応concurrent-rubyのConcurrent::Hash
を使っているとはいえ、こういう書き方ってドキドキしちゃいますね」
「起きたり起きなかったりする問題はつらい...」「原因を突き止めて再現までしたのはすごい」
# 同記事より: 再現コード
GC.disable
puts "Total classes before: #{ObjectSpace.count_objects[:T_CLASS]}"
puts "String subclasses count before: #{String.subclasses.count}"
100.times { Class.new(String) }
puts "Total classes after defining: #{ObjectSpace.count_objects[:T_CLASS]}"
puts "String subclasses count after defining: #{String.subclasses.count}"
GC.enable
GC.start
puts "Total classes after GC: #{ObjectSpace.count_objects[:T_CLASS]}"
puts "String subclasses count after GC: #{String.subclasses.count}"
# Total classes after defining: 1324
# String subclasses count after defining: 102
# Running GC...
# Total classes after GC: 1124
# String subclasses count after GC: 2
「記事の結論はこれですね↓」
無名クラスや無名モジュールは、他のオブジェクトと同様にガベージコレクションの対象になる。それらを参照していないと、
#descendants
などで探索して使いたいと思っても、その前に消えてしまう可能性がある。絶えず何らかの形で参照しておかないと思わぬことになる。
同記事より
🔗 Railsに関わる技術の体系化を目指した本
https://t.co/LB6OYyc7L1 なかなか面白かったです。体系化というよりは網羅的に技法・パターン・ポリシーを紹介してるもので、全部取り入れたいとは思わないけどそういうチームはありそう感が強くて比較的穏当な雰囲気でした
— Masayoshi Takahashi (@takahashim) November 17, 2022
編集部注: 上の記事はつっつき時点(2022/11/24 20:00頃)には読めましたが、その後Kindleストア配信化を目指して非公開になったことが以下の記事に追記されています。Kindleではタイトルが変わる可能性もありそうです。
参考: Rails初・中級者向けの技術の体系化を目指した本を書きました
無料で公開していましたが、現在Kindleストアでの配信を検討しており非公開にしました。
いいねしていただいた方ありがとうございます。
おそらく、近いうちにkindle unlimited(amazonの読み放題サブスクサービス)で読めるようになると思いますが、自分としては本を出すのは初めてでかなり実験的な施策なので今後どのようになるかは今ひとつ予測不能です。
低評価レビューで心が折れるような事態にならないといいなと願ってます。
Rails初・中級者向けの技術の体系化を目指した本を書きましたより
つっつきボイス:「TechRacho記事も引用されていたことで気づきました」「ざっと見た感じでは、@takahashimさんも書いているように、Railsにはどんな手法やパターンがあるかを一通り網羅することを目指している本みたい」「書かれていることを全部使うような本ではないということですね」「こういうのをまとめる過程が勉強になる」「初めて参加するプロジェクトで見慣れない手法やパターンが登場したときに調べるのに便利そう👍」
「ところでRailsを徹底的に学ぼうとすると、RailsチュートリアルやRailsガイドより先の部分を詳しく解説した本や教材も必要になりますよね」「Railsの深淵に触れるためにはどうしても必要になりますね」
参考: Ruby on Rails チュートリアル:プロダクト開発の0→1を学ぼう
参考: Ruby on Rails ガイド:体系的に Rails を学ぼう
「Railsのconcernsディレクトリひとつとっても、初めて見たときに"これは何だろう?"という気持ちになったりする」「concernsをいいと言う人もよくないと言う人もいて最初考え込んでしまいました」「concernsは使いようで良くも悪くもなる子ですね」
「concernsはRailsに限らず、RubyでModule#include
を使うベストプラクティスのひとつとして存在していますね: gemでも同じような方法を使っているのをよく見かけます」「concernsはRubyの方言というかRuby特有のパターンなのかも」
参考: Module#include
(Ruby 3.1 リファレンスマニュアル)
「個人的には、1個のクラスでしかinclude
されないコードならconcernsでモジュール化しなくてもいい気がすることもあるんですが、Rubyはそういうふうに書く文化なんだと考えるようになりました」「Rubyはオープンクラスなんだから、それで書けばいいのではと思ったりすることもありますけど、モジュールのinclude
やprepend
などによってクラスの継承チェインに配置される方が便利だからそう書くんだろうなと思うようになりました」「そうそう、だから気軽にsuper
を使える」「フックもはさめるようになりますよね」
オープンクラス
open class
組込みのクラスが再定義可能であること。 Ruby は String や Integer といった基本的なクラスも自由に改変できる。
しかし、既存のクラスやモジュールをむやみに改変することは思わぬバグを生みやすい。そのため、改変の効果を局所化する refinement という機構がある。
Ruby用語集 (Ruby 3.1 リファレンスマニュアル)より
🔗 その他Rails
つっつきボイス:「RubyConf Mini 2022というイベントが11/15〜17に開催されていたそうで、そのまとめ記事です」「ロードアイランド州プロビデンス開催か」「Miniと銘打たれているにしては3日連続でスピーチも盛りだくさんですね」「あ、たしかに」
参考: RubyConf Mini
参考: プロビデンス (ロードアイランド州) - Wikipedia
「よく見ると、それとは別にRubyConf 2022が11/29〜12/1にテキサス州ヒューストンで開催される予定になっていました↓」「こちらが本番ということなのかな?」「並列のセッションが最大4つもありますね」「Miniとは会場も別なんですね」「同じ会場でやる方が会場や宿泊施設などを押さえやすそうだけど、何らかの意図か事情があるのかも🤔」
参考: Home | RubyConf 2022
参考: ヒューストン - Wikipedia
🔗 Hanami
🔗 Hanami 2.0がリリース
つっつきボイス:「Hanamiもついに2.0がリリース🎉」「alpha2のリリースが昨年でしたね(ウォッチ20210517)」「"Better, Faster, Stronger"」「Hanamiの人たち頑張ってますね」「Hanamiはメンテしている人たちも実際に使い続けていますし、Railsよりスリムなものが欲しい人には人気があるんじゃないかな」「今のRailsもAPIモードでrails new
するとスリムになりますけど、それでも色々入ってきますね」「Railsで使わない機能を後から外そうとすると割と大変」「使わないつもりの機能でも、入っていると使われちゃったりしますよね」
「hanami.gemspecを覗いてみたらzeitwerkも入ってました↓」「お〜、こういうところもキャッチアップしているんですね」
...
spec.add_dependency "bundler", ">= 1.16", "< 3"
spec.add_dependency "dry-configurable", "~> 1.0", "< 2"
spec.add_dependency "dry-core", "~> 1.0", "< 2"
spec.add_dependency "dry-inflector", "~> 1.0", "< 2"
spec.add_dependency "dry-monitor", "~> 1.0", ">= 1.0.1", "< 2"
spec.add_dependency "dry-system", "~> 1.0", "< 2"
spec.add_dependency "dry-logger", "~> 1.0", "< 2"
spec.add_dependency "hanami-cli", "~> 2.0"
spec.add_dependency "hanami-utils", "~> 2.0"
spec.add_dependency "zeitwerk", "~> 2.6"
...
前編は以上です。
バックナンバー(2022年度第4四半期)
週刊Railsウォッチ: The Rails Foundation発足、Ruby 3.2.0 Preview 3リリース、Ruby演算子クイズほか(20221122)
- 20221116後編 Rubyを使っている企業の時価総額リスト、irbのshow_source、GitHub Codespacesほか
- 20221115前編 RailsチュートリアルがRails 7対応版をリリース、ViewComponentで使えるLookbookほか
- 20221102後編 書籍『Programming Ruby 3.2 (5th Edition)』、ReDoSチェックサイトほか
- 20221101前編 Packwerkの詳しい解説書『Gradual Modularization for Ruby and Rails』ほか
- 20221026後編 Ruby 3.2のData.define、RubyPrize 2022最終ノミネート、Puma-dev gemほか
- 20221025前編 rodauth-rails gem作者の解説記事、turbo-railsの有料チュートリアルほか
- 20221019後編 Ruby技術者認定試験再受験無料キャンペーン、Starlink日本で販売開始ほか
- 20221018前編 Rails向けLanguage Server “refreshing”開発中、JetBrains Fleetほか
- 20221012後編 RailsとPostgreSQLで列挙型を作成する6つの方法、Ubuntu Proほか
- 20221011前編 Turbo 7.2.0リリース、GitLabのDevSecOpsサーベイ結果ほか
- 20221004後編 ヒアドキュメント拡張の提案、『組織に自動テストを根付かせる戦略』ほか
- 20221003前編 Kaigi on Rails 2022のタイムテーブル発表、書籍『Practicing Rails』ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)