- Ruby / Rails関連
週刊Railsウォッチ: Active Modelで属性のパターンマッチをサポート、猫でもわかるHotwire入門ほか(20220516前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Active Modelでパターンマッチングを利用可能に
Active Modelで(ひいてはActive Recordでも透過的に)パターンマッチングを利用できるようになるとよい。
複数の属性で条件をチェックしたいときに、以下のようにパターンマッチングの構文が使えるとよい。
case Current.user
in { superuser: true }
"Thanks for logging in. You are a superuser."
in { admin: true, name: }
"Thanks for logging in, admin #{name}!"
in { name: }
"Welcome, #{name}!"
end
同PRより
つっつきボイス:「お〜、Rubyのパターンマッチング構文をActive Modelで直接使えるようにインターフェイスを用意したのか」「何と!」「"いいね"👍がたくさん付いてる」
「なるほど、一般にパターンマッチを実装するには、ハッシュを返すdeconstruct_keys
を定義すればいいのね↓」「こうすれば自分のクラスでもパターンマッチが使えるようになるのか」「わかりやすくて実用的ないい機能👍」
# activemodel/lib/active_model/attribute_methods.rb518
def deconstruct_keys(keys)
deconstructed = {}
keys.each do |key|
string_key = key.to_s
deconstructed[key] = _read_attribute(string_key) if attribute_method?(string_key)
end
deconstructed
end
参考: Ruby2.7の(実験的)新機能「パターンマッチ」で遊ぶ - メドピア開発者ブログ
後でこちらの記事も目に止まりました↓。
#deconstruct_keys
を誤って実装すると非効率的な結果となることがあるので、たくさんのキーを取りたい場合は愚直に書いた方が早いし見やすくなるとので注意が必要とのことでした😮
Ruby 2.7 で導入予定のパターンマッチングを試したら無限大の可能性を感じた話 - Feedforce Developer Blogより
🔗 長い文字列をDate・Time・DateTimeにキャストしたときにエラーにならないよう修正
- PR: Fix casting long strings to Date, Time or DateTime by fatkodima · Pull Request #45005 · rails/rails
つっつきボイス:「issueを見ると、" " * 129
をDateに変換したときにnil
にならずにエラーになっていたのか↓」「いかにも無効な日付文字列」「よくぞ見つけましたね」
- issue: ActiveModel::Type::Date raises for strings longer than 128 characters · Issue #45003 · rails/rails
# #45003より
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
gem "activemodel", "~> 7.0.0"
end
require "active_model"
require "minitest/autorun"
class BugTest < Minitest::Test
def test_stuff
assert_nil ActiveModel::Type::Date.new.cast(" " * 129)
end
end
「Dateなどの_parse
メソッドに関連しているみたい」「ドキュメントを見るとlimit: 128
というデフォルト値があるので、これを踏んだんでしょうね↓」「たしかにパーサーに無限長の文字列を渡せるのはあまりよくないので、Rubyでデフォルトの上限値を設けているのはわかる」
参考: Class: Date
(Ruby 3.1.2) -- _parse
# ruby-doc.orgより
_parse(string[, comp=true], limit: 128) → hash
「ここではDate._parse
を呼ぶときにArgumentErrorをrescue
したうえで握りつぶす形で回避してますね↓」「128文字を超える文字列を渡したかったというより、意味のわかりにくいArgumentErrorを回避したかったということなのかも」
# activemodel/lib/active_model/type/date.rb#L55
def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ parts = begin
+ ::Date._parse(string, false)
+ rescue ArgumentError
+ end
+
+ new_date(*parts.values_at(:year, :mon, :mday)) if parts
end
「ところで、このプルリクには珍しく👎も付いてますね」「エラーを握りつぶしていいかどうかは議論が分かれるところではあるけど、ArgumentErrorが直感的でないのもわかる」
🔗 矛盾するリレーション上の計算でクエリを発行しないようにする
従来は、
count
などのリレーション計算で、User.where(id: []).count
のような矛盾があってもクエリが発行されてしまっていた。
既に@jhawthornが以下のようなプルリクで、矛盾時のクエリ発行を回避するようにしてくれていた。
- Avoid making query when using
where(attr: [])
by jhawthorn · Pull Request #37266 · rails/rails -- レコード読み込みを修正- Avoid query from exists? on contradictory relation by jhawthorn · Pull Request #40387 · rails/rails --
exists?
メソッドを修正@composerinteraliaと@jhawthornと私は、リレーションでの計算になかった最適化にスポットを当ててこれまでのプルリクでガイドとして利用していた。
このプルリクは、count
、average
、minimum
、maximum
のリレーションが矛盾している場合のクエリ発行を回避する。
同PRより
つっつきボイス:「contradictionは"矛盾"」「User.where(id: []).count
はidが空配列なので意味がないことを指してるみたい」「そういうときに無駄なクエリを投げないようにしたんですね」「ActiveRecord::WhereClause
(ドキュメントなし)にcontradiction?
というメソッドがあるのか」「こういうふうに早い段階で別の方法に切り替えるのは最適化でよく行われますね: 自明なケースなら除外しやすい👍」
# activerecord/lib/active_record/relation/calculations.rb#L340
def execute_simple_calculation(operation, column_name, distinct) # :nodoc:
if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
# Shortcut when limit is zero.
return 0 if limit_value == 0
+ relation = self
query_builder = build_count_subquery(spawn, column_name, distinct)
else
# PostgreSQL doesn't like ORDER BY when there are no GROUP BY
relation = unscope(:order).distinct!(false)
column = aggregate_column(column_name)
select_value = operation_over_aggregate_column(column, operation, distinct)
select_value.distinct = true if operation == "sum" && distinct
relation.select_values = [select_value]
query_builder = relation.arel
end
- query_result = skip_query_cache_if_necessary do
- @klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: @async)
+ query_result = if relation.where_clause.contradiction?
+ ActiveRecord::Result.empty
+ else
+ skip_query_cache_if_necessary do
+ @klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: @async)
+ end
end
query_result.then do |result|
if operation != "count"
type = column.try(:type_caster) ||
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
type = type.subtype if Enum::EnumType === type
end
type_cast_calculated_value(result.cast_values.first, operation, type)
end
end
「こういう1件もヒットしないクエリってものすごく重くなることがあるんですよ」「1件もないことを証明するには全件チェックしないといけない、たしかに」「さんざん調べた結果"ありませんでした"だと悲しいですね」
🔗 insert_all
やupsert_all
でalias_attributes
もサポート
# 同PRより
class Book < ApplicationRecord
alias_attribute :title, :name
end
Book.insert_all [{ title: "Remote", author_id: 1 }], returning: :title
ちょっと便利になる。現在はBookの'title'属性で
ActiveModel::UnknownAttributeError
エラーになる。
同PRより
つっつきボイス: 「お、insert_all
やupsert_all
でalias_attributes
を解決してクエリを投げてくれるようになった」「今までエイリアスできなかったんですね」
「alias_attributes
は、Railsの管理下にない外部のデータベースにアクセスするときなんかによく使ってます」「あぁ、カラム名がすごくわかりにくいときとかですね」「古いシステムでカラム名やテーブル名が8文字までみたいな謎のローカルルールがあったりとか」「8文字はつらそう...」
🔗 デフォルト値付きのカラムで暗号化属性をサポート
デフォルト値を持つカラムで定義される暗号化属性のサポートを追加する。
従来はコンテンツが暗号化されていなかったためにこうしたカラムを読み出せなかった(config.active_record.encryption.support_unencrypted_data
を有効にした場合を除く)。これで、レコード作成時にこれらの値が暗号化されるようになる。
#44993でこの問題を調べてくれた@fatkodimaに感謝し、ここで共著者に追加した。
つっつきボイス:「暗号化した属性に暗号化されていないデフォルト値があるとどうなるかを考えてみれば、これが欲しくなるのもわかる気がする」
参考: Rails API encrypt_attribute
-- ActiveRecord::Encryption::EncryptableRecord
🔗 MemoryStore#write(name, val, unless_exist: true)
に失効したエントリを渡したときのチェック方法を修正
キーが失効しているときの
ActiveSupport::Cache::MemoryStore#write(key, val, unless_exist: true)
を修正。
#write_serialized_entry
でキーが存在するかどうかをチェックするときに、exist?
ではなく@data.key?
が使われていたために失効がバイパスされていた。
同PRより
# activesupport/lib/active_support/cache/memory_store.rb#L166
def write_entry(key, entry, **options)
payload = serialize_entry(entry, **options)
synchronize do
- return false if options[:unless_exist] && @data.key?(key)
+ return false if options[:unless_exist] && exist?(key)
old_payload = @data[key]
if old_payload
@cache_size -= (old_payload.bytesize - payload.bytesize)
else
@cache_size += cached_size(key, payload)
end
@data[key] = payload
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
true
end
end
つっつきボイス:「これはバグ修正」「MemoryStore
を本番で使うことがほとんどないので気づかれなかったのかも」
参考: §2.3 ActiveSupport::Cache::MemoryStore
-- Rails のキャッシュ機構 - Railsガイド
「MemoryStore
ってテストで使うヤツでしたっけ?」「Redisのような外部キャッシュストアなしで使えるキャッシュストアです」「Rubyプロセスのメモリ上にキャッシュストアを構築するからセットアップが楽なんですね」「その代わりプロセスを再起動すると全部消えるので、本番では普通使わないと思います」「なるほど」「MemoryStore
はないよりあった方が多少速くなると思いますが、今どきはマシンも高速になっていますし、OSのキャッシュも効いたりすると思うので、昔ほどは使ってないかも」
🔗Rails
🔗『猫でもわかるHotwire入門 Turbo編』
Hotwireの入門書を書きました!
Rails7からフロントエンドのデフォルトに採用されたので、この機会にぜひ〜。Railsと相性が良くて、今までのRailsアプリ開発の延長線上でモダンなWebアプリが作れます。
無料なので、読んでみてください〜。https://t.co/g2mB9yfKrk
— shita (@shita1112) April 15, 2022
つっつきボイス:「お〜これは力作」「ボリュームも内容も充実してそう」「無料ありがたい🙏」「"〜でもわかる"みたいなタイトルは久しぶりかも」
そういえば英語圏だと「〜 for dummies」シリーズや「〜 101」シリーズが、日本で言う「〜でもわかる」的な入門向けタイトルですね。「〜 for Zombies」(ゾンビでもわかる)みたいなタイトルも見かけます。
参考: dummies - Learning Made Easy
参考: Adams 101 Book Series | World of Books
「自分がこういう本を書くとしたら、5年ぐらい通用する内容にしたい気持ち」「わかります」「自分も大学の授業で教えるときにで使う教材も改定を重ねながら5年ぐらい使ってますね: 教材ではあまり特定の技術に依存しないようにしているから長持ちするという面もあると思いますが」
🔗 ブランチカバレッジでSpecの品質を改善する(Ruby Weeklyより)
つっつきボイス:「カバレッジ計測用のsimplecov gemでbranch
を指定するとブランチカバレッジが取れるようになる↓」
# 同記事より
require 'simplecov'
SimpleCov.start do
enable_coverage :branch
end
require_relative './example2.rb'
RSpec.describe 'number_to_dollar' do
subject { number_to_dollar(2.56) }
it 'returns dollar and cents' do
expect(subject).to eq('2 dollar(s) 56 cent(s)')
end
end
「ブランチカバレッジはよく登場する概念」「ブランチカバレッジって初めて聞いたかも」「ここで言うブランチは分岐の意味ですね」「なるほど、コードの分岐をどれだけテストで押さえているかというカバレッジの指標の方ですか」
参考: ブランチカバレッジ(ぶらんちかばれっじ):情報システム用語事典 - ITmedia エンタープライズ
「たとえばif
のあるコードがテストで1度も実行されなければブランチカバレッジで発見可能ということになりますね」「お〜」「ただ、変数に事前にどんな値が設定されるかで実行条件が変わったりするようなコードだと、ブランチカバレッジですべての変数のパターンを網羅できるとは限らないことも考えられる」「たしかに」「ちなみに今のRubyはソースコードの各部分について読み取りと実行が行われたかどうかという情報を取れます」
🔗 書籍『Modern Front-End Development for Rails, Second Edition』
Rails使いとしては気になる本。これを読むと、Rails 7 + Hotwire, Stimulus, Turbo, and React でフロントエンドを作る方法がわかるのかな?
Modern Front-End Development for Rails, Second Edition: Hotwire, Stimulus, Turbo, and React https://t.co/gSKAgcnefQ
— Junichi Ito (伊藤淳一) (@jnchito) May 10, 2022
つっつきボイス:「さっき見かけて気になってた本です」「HotwireとStimulusとTurboというRails 7から標準で使えるフロントエンド系の機能に加えて、Reactとの組み合わせについても解説されているということなのかな」「今年9月発売だからまだ先か」「ベータ版ならもう買えるようですね: The Pragmatic Bookshelfシリーズではこういうふうに書けた部分から本にすることもよくやっている」
「Pragmatic Bookshelfの本は、理論的に高度な本よりはテーマに沿った網羅的な内容の本が多いという印象: 個人的には、自分が初めて取り組む分野について流し読みするのに向いている感じで割と読みやすい」「なるほど」「もちろん著者にもよりますけどね」
「Pragmatic Bookshelfの本は、たとえばだいぶ昔のですがRSpec 2の本が有名かな↓」「あ、これもそうなんですか」「日本語版も出てましたね」
🔗 その他Rails
なお、いずれの修正点もフィヨルドブートキャンプ内の「Everyday Rails輪読会」で報告されたものです。僕も気付いてなかった問題点を見つけててすごい!みなさんどうもありがとうございました〜。 #fjordbootcamp
— Junichi Ito (伊藤淳一) (@jnchito) May 10, 2022
つっつきボイス:「Everyday Railsの改訂版がダウンロードできる❤️」「輪読会のような場が修正の機会になるというのはたしかにありますね: 自分も大学の授業で使う教材を2回ぐらい授業で使って学生からフィードバックをもらうといろいろ改善される」「最初の授業はベンチマークの場でもありますよね」
🔗 教材で大事なバージョン指定
「割と前ですが、教材を2回目に使う授業の直前にRailsがメジャーバージョンアップされたときは、教材のとおりに動かなくなって大変だったことがありましたね: _6.0.4_
のようにバージョン指定すれば回避できますが」「そういえばこのテクニックはRailsチュートリアル™でも使われています↓」
# railstutorial.jpより
rails _6.0.4_ new hello_app
「そうそう、ビギナー向けの教材は書いてあるとおりに動くことがとても大事なので、こうやってRailsやBundlerのバージョンをがっちり指定するのは教材づくりの定番ですね」「学習意欲に水を差さないために大事」「バージョン違いでつまづいて苦手意識を持ってしまったら残念」「なお、こういうバージョン指定のインストールは、教材以外に実際の業務で使うこともあります」
工学院大学や AIIT など、大学/大学院での導入事例も公開しているので、もし興味ある教育関係者の方いらっしゃいましたら気軽にお声がけください 😂✨ #Railsチュートリアル
教育機関・スクール向け https://t.co/gG8LPaygJR
最近は企業の研修事例が増えてきています! 🎓💖 https://t.co/vvG21rpuFB https://t.co/4VjUPA4sHS
— 安川要平/Yohei Yasukawa (@yasulab) April 10, 2022
前編は以上です。
バックナンバー(2022年度第2四半期)
週刊Railsウォッチ: Ruby 3.2.0devにRust版YJITがマージ、Docker Compose V2ほか(20220511後編)
- 20220510前編 Active RecordにPromiseと非同期集計メソッドがマージ、climate_control gemほか
- 20220419後編 RubyのGCコンパクション改修、jemalloc、ReDoSの自動検出修正ほか
- 20220418前編 RailsConf 2022が5月17〜19日開催、認可機能解説記事ほか
- 20220412後編 HashieでRubyのハッシュを強化、最近のRubyコア解説記事ほ
- 20220411前編 Turbo Railsチュートリアル、Active Recordの「Leaky Abstraction」を削減ほか
- 20220406後編 RBS関連記事、Ruby formatterプロジェクト、Google Cloud Runほか
- 20220404前編 Ruby 3.2.0 Preview 1リリース、Rails向けDocker環境ジェネレータ、scientist gemほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)