- Ruby / Rails関連
週刊Railsウォッチ(20200817前編)お盆も続くRails改修、Rails 6.1にManyモナドが入る?rails-auth gemでクライアント認証ほか
こんにちは、hachi8833です。皆さま熱中症にはお互い気をつけましょう。
つっつきボイス:「昨日急に体調つらくなって、自分でもびっくりするぐらい丸一日寝てたんですけど、もう一日の記憶がありませんし😇」「これだけ暑いと冷房の効いた部屋にいても体調悪くなりそうですよね...」「いやホントお大事に💊」「ウォッチも熱中症対策ということでエントリを減らし目にしました」
私も猛暑になるととりあえず梅干ししゃぶってクエン酸補給してます。
「そうそう、『室温28℃はエアコンの設定温度ではありません』ってよく注意喚起されてますよね」「自分はとりあえずエアコン27℃にしてますけど」「私も」「結局28℃は目安でしかなくて、西日が差すとか部屋の立地や構造などの条件でいくらでも変わってきますし」(以下エアコン工事や百葉箱の話題など延々)
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
⚓Rails: 先週の改修(Rails公式ニュースより)
以下のコミットリストのChangelogを中心に見繕いました。お盆休みのせいか少なめです。
- コミットリスト: Comparing master@{2020-08-07}...@{2020-08-14} · rails/rails
-
6.1.0マイルストーン: 6.1.0 Milestone
つっつきボイス:「お、6.1.0のマイルストーンですか」「少し前からできてたんですが、その中から1つをこの後ピックアップしました🍡」
なおつっつきの時点では6.1.0マイルストーンに余分なフィルタをかけてしまって残り2件だと思いこんでました😅。本日確認したところ、まだ25件がオープンです。
⚓joinスコープのthrough
関連付けが重複するとeager loadingの結果が正しくなくなる問題を修正
キリ番ゲットでした。
🎊4⃣0⃣0⃣0⃣0⃣🎊https://t.co/NXkuqcREzn
— Ryuta Kamizono (@kamipo) August 7, 2020
つっつきボイス:「ううめんどくさそう...」「こういう感じの現象前にも見た気がする」
# 同PRより
class Author < ActiveRecord::Base
has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization"
has_many :general_posts, through: :general_categorizations, source: :post
end
authors = Author.eager_load(:general_categorizations, :general_posts).to_a
「この辺がややこしくなってしまうのは、ActiveRecordのコードから導出されるSQLが、期待しているSQLと一致しているかどうかを考えるのが難しいからというのもありますね」「両方を一致させるのってえらく大変そう...」「お互い違う言語ですから」
「へ〜、general_categorizations_authors_join
っていう名前になってる↓」
-- 同PRより
SELECT "authors"."id" AS t0_r0, ... FROM "authors"
-- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?
-- `has_many :general_posts, through: :general_categorizations, source: :post`
---- duplicated `through: :general_categorizations` part
LEFT OUTER JOIN "categorizations" "general_categorizations_authors_join" ON "general_categorizations_authors_join"."author_id" = "authors"."id"
INNER JOIN "categories" "categories_categorizations" ON "categories_categorizations"."id" = "general_categorizations_authors_join"."category_id" AND "categories"."name" = ? -- <-- filtering `"categories"."name" = ?` won't work
---- `source: :post` part
LEFT OUTER JOIN "posts" ON "posts"."id" = "general_categorizations_authors_join"."post_id"
「上のクエリの1つ目のLEFT OUTER JOIN
にあるcategorizations
には別名が付いてなくて、その直後のINNER JOIN
にあるcategories
にも別名が付いてないんですけど、その次のLEFT OUTER JOIN
にはgeneral_categorizations_authors_join
という別名が付いている: ポイントは、この別名とJOIN句がどうやって導出されたのか?という点です」「な、長い😅」
「導出方法がわからないと、JOINしたときにcategorizations
がどう紐付けられるかを予測できないんですよ」「う〜む」「categories_categorizations
っていう名前も自動生成なんですね」「なんという名前😆」
「というふうに、ActiveRecordで書かれたコードから生成されるSQLを読み解くのって難しいんですよ」「難しい〜😭」「何だか空中ブランコ見ているみたいで大丈夫なの?って気がしてきます」「動くと言われても不安になってきますよね...」
「修正内容もどんなふうに修正されたのか、コードをちょっと見ただけだとわからない...」「とりあえず修正後を見るとJOINするテーブル数が減ってるから↓、プルリクに書いてあるとおりthrough
関連付けをdeduplicateして再利用したということなんでしょう」「デデュプリケート...」「さっきのgeneral_categorizations_authors_join
が消えてる🎉」「ホントだ」「これはたしかにグレートワークカミポ」「余人が手出しできなさそう...」
# 同PRより
SELECT "authors"."id" AS t0_r0, ... FROM "authors"
-- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?
-- `has_many :general_posts, through: :general_categorizations, source: :post`
---- `through: :general_categorizations` part is deduplicated / re-used
LEFT OUTER JOIN "posts" ON "posts"."id" = "categorizations"."post_id"
「たしかにActive Recordでスコープを使っていると無意識にこういうの書いちゃうことありますけど」「joinedスコープ的な書き方では気をつけないといけないでしょうね」「これに似た問題は過去にもちょくちょくあった気がします: joinのときとか、mergeのときとか」
長い名前というと、ついメリー・ポピンズを思い出します。
参考: スーパーカリフラジリスティックエクスピアリドーシャス - Wikipedia
⚓assert _recognizes
がマウントしたルーティングでも使えるように修正
- PR: Fix `assert_recognizes` on mounted root routes. by gmcgibbon · Pull Request #39981 · rails/rails
ルーティングの
assert_recognizes
アサーションをマウントしたrootルーティングでも使えるようにする。4a9d4c8と似ているが、テストのアサーションにあるパスを認識する点が異なる。
現状のルーティングのrecognize
では、ActionDispatch::Journey::Patch::Pattern::MatchData#post_match
を用いてrootルーティング(/
)を/\A\//
という正規表現でチェックすると空文字列のPATH_INFO
(""
)が生成される。これではマウントしたエンジンのルーティングのforward先が空文字になるのでルーティングを見つけられない。
同PRより大意
# actionpack/lib/action_dispatch/journey/router.rb#L65
def recognize(rails_req)
find_routes(rails_req).each do |match, parameters, route|
unless route.path.anchored
rails_req.script_name = match.to_s
- rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1')
+ rails_req.path_info = match.post_match
+ rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
end
parameters = route.defaults.merge parameters
yield(route, parameters)
end
end
つっつきボイス:「assert_recognizes
って使ったことないな〜」「Railsガイドにもあった↓」
「ガイドのassert_generates
アサーションはそのまんまでわかりやすい、つかこれがあればいい気がする」「でassert_recognizes
アサーションはその逆をテストするのね」「assert_routing
は使ったことあったかも」
# Railsガイドより
assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')
「ルーティング難しいから、複雑なルーティングを書いたときにはこういうアサーションを使うとよさそう」「普通のresoucesルーティングならわざわざテストは書かないかな〜」
「ところで今ググって出てきたrailsdoc.comって初めて見たんですけど何でしょう?」「はて?」「知らない〜」
後で調べました↓。昨年のツイートですが、今は動いています(運営主体が同じかどうかはわかりません)。参照するときは公式ではない点に留意しておこうと思います。
もうひとつの「それ、公式じゃないから気を付けような!サイト(でもちょっと便利w)」として https://t.co/84PyXfRAAD があったんだけど、今見たら接続できなくなってた。Twitterを見てるとどうも今年の8月あたりからつながらなくなってた様子。
で、結局このサイトは誰が管理してたんだろう??🤔 https://t.co/VZSIgPxFUN
— Junichi Ito (伊藤淳一) (@jnchito) November 17, 2019
⚓@controller
というインスタンス変数が初期化されない場合があったのを修正
#39937で指摘されたように、
@controller
というインスタンス変数がルーティングのテストで定義されないことがある。
rails/actionpack/lib/action_dispatch/testing/assertions/routing.rb#202
request = ActionController::TestRequest.create @controller.class
上が一部のインスタンスで
instance variable @controller not initialized
warningを出すことがある。
インスタンス変数にアクセスする前にdefined?(controller)
をチェックするといいだろう。
controller = @controller if defined?(@controller)
request = ActionController::TestRequest.create controller&.class
@ioquatixのレポートと解決に感謝🙂。
同PRより大意
つっつきボイス:「@controller
という変数名にちょっとドキッとした」「@controller
が初期化されないことがあったら結構困るし」「コントローラを直接参照したいことがどのぐらいあるかですけど、まあテストだし、本来はこのパスで触れないはずのものでも状態のテストはしたいこともあるでしょうし」
⚓establish_connection
のパラメータをリネームしてキーワードに変更
# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1034
- def establish_connection(config, pool_key = Base.default_pool_key, owner_name = Base.name)
+ def establish_connection(config, owner_name: Base.name, shard: Base.default_shard)
owner_name = config.to_s if config.is_a?(Symbol)
pool_config = resolve_pool_config(config, owner_name)
db_config = pool_config.db_config
# Protects the connection named `ActiveRecord::Base` from being removed
# if the user calls `establish_connection :primary`.
if owner_to_pool_manager.key?(pool_config.connection_specification_name)
- remove_connection_pool(pool_config.connection_specification_name, pool_key)
+ remove_connection_pool(pool_config.connection_specification_name, shard: shard)
end
message_bus = ActiveSupport::Notifications.instrumenter
payload = {}
if pool_config
payload[:spec_name] = pool_config.connection_specification_name
payload[:config] = db_config.configuration_hash
end
owner_to_pool_manager[pool_config.connection_specification_name] ||= PoolManager.new
pool_manager = get_pool_manager(pool_config.connection_specification_name)
- pool_manager.set_pool_config(pool_key, pool_config)
+ pool_manager.set_pool_config(shard, pool_config)
message_bus.instrument("!connection.active_record", payload) do
pool_config.pool
end
end
つっつきボイス:「poolという名前が変わった」「ついでにキーワード引数に変わってる」「publicなestablish_connection
で使う名前としては、これがふさわしいという判断なんでしょうね」
current_pool_key
->current_shard
default_pool_key
->default_shard
pool_key -> shard
「poolという言葉だと自分はほぼコネクションプールを連想しますし、poolだと排他制御されてるように見えてしまうかもしれませんね: コネクションプールは自分だけが使えるリソースをもらって、終わったら返却する印象がありますけど、public云々と言っているのはもしかすると同時に使われる可能性も加味してるんじゃないかと推測してみました🤔」「ははぁ、なるほど」
参考: コネクションプーリング(コネクションプール)とは - IT用語辞典 e-Words
pool_key
からshard
へのリネームもともとは
shard
をpool_key
という名前にするつもりだった(内部のprivate APIを実装するときにシャーディングに使われるかどうかがわからなかったのと、public APIにせずに振る舞いを実装したかったので)。シャーディングのpublic APIができることになったので、private向けの名前にするより同じ名前をpublic APIで使う方がよい。今後のコード改修やバグ追跡がやりやすくなるはず。このプルリクでdeprecationは不要(シャーディングAPIはまだリリースされておらず、内部コードはすべて
pool_key
を使うようになっているので)。
- 接続メソッドでキーワード引数を使うよう更新
この変更では位置引数ではなくキーワード引数を接続用メソッドで使うようになる。この変更は一見必要ではなさそうに見えるが、今自分たちは接続管理をより頑丈かつ柔軟にするためにリファクタリング中なので、この時点でメソッドシグネチャを変更しておけば今後の変更がやりやすくなる。
このコミットではリリース済みのpublic APIは変更されない(shard
もowner_name
引数も6.1に追加されている)。キーワード引数にすることで柔軟性が高まり、内部動作を変更しても見苦しくならなくなる。このキーワード引数ではデフォルトで複数の「非位置引数」をサポートする。
(@seejohnrunとの共作)
同PRより大意
⚓issue: Active SupportやActive RecordリレーションにManyモナドを導入したい
上のRails 6.1マイルストーンのissueの1つです。
@tomstuartのRubyでモナドを使う素敵なスピーチに刺激を受けたので、「Rails on Many monad」に足を踏み入れてみたいと思う。
Tomのmonads gemにある彼の成果を必要に応じてActive Supportにコピーすることについて快諾いただいた。このAPIの最もイケてる点は、以下のようにActive RecordリレーションでMany monadを使えるようになるという点だ。
Blogs.all.with_many.categories.posts.comments.body.split(/\s+/).values
同じことを以下のように
flat_map
でやるよりはるかにレベルアップできる。
Blogs.all.flat_map(:categories).flat_map(:posts).flat_map(:comments).flat_map(:body).split(/\s+/).values
これはずっと前からやりたかったのだが、これを正しく説明する方法について考えてみていいだろう。目指すはモナド!
これを動かすために、まずActive SupportにMaybeモナドを入れ、続いてcore extensionでwith_many
というEnumerableを追加し、それからActive Recordリレーションでもうまく動くようにすることを提案する(たぶんさくっとやれるはず)。
同issueより大意
つっつきボイス:「ちょうど昨日モノイド記事↓を公開したので、DHHがあげたマイルストーンissueを貼ってみました」
「モナドか〜、こういうwith_many
をやりたいということなのね↓」「あ、今気づいたんですけど、Manyが大文字なのはモナドの名前なのか😅: モナドってMaybeとかOptionみたいな名前付けられがち」
Blogs.all.with_many.categories.posts.comments.body.split(/\s+/).values
「まあ書き方が増える分にはいいんじゃないかな」「他のテストがコケたりしなければ😆」「まだ作り中みたいですけど、6.1.0のマイルストーンにこのissueが入っているということは入れるつもりなんでしょうね」
参考: Maybeモナド と Listモナド - Qiita
「モナドはまずActive Supportに入れるつもりみたい: Active Recordにいきなり入れるよりはいいかも」「モナド大好きなkazzさんに見せたいな〜」「kazzさん最近つっつきになかなか参加できないんですよね...」
⚓Rails
⚓RailsアプリをDocker化する(Ruby Weeklyより)
つっつきボイス:「Rails Docker化を割と基本的なところから説明してる感じでした」「RailsをDocker Compose化するのはそんなに大変じゃないんですけど、ECSやEKSに乗せようとするといろいろ難しくなりがち: コンテナオーケストレーションツールに乗せると条件も変わってくるし考えないといけないことも増えてくるので」
記事見出しより:
- RailsアプリケーションをDocker化するメリット
- development環境でののメリット
- production環境でのメリット
- チュートリアルの概要
- 前提条件
- Dockerの基本概念
- イメージ
- コンテナ
- DockerとDocker Composeの違い
- アプリのDocker化に関連するファイル
- Dockerfile
- docker-compose.yml
- アプリをDocker化する
- Dockerfile
- docker-compose.yml
- init.sql
- config/database.yml
- Docker化したアプリのビルドと実行
⚓RailsのログをRSpecでテストする方法2種(Ruby Weeklyより)
# 同記事より
# expect
it "logs a message" do
allow(Rails.logger).to receive(:info)
expect(Rails.logger).to receive(:info).with("Someone visited the site!")
visit root_path
expect(page).to have_content "Welcome to my site!"
end
つっつきボイス:「Everyday Railブログです」「spyを使う方法もあるか、なるほど」「ログの中身をテストすることってあるんですね」「当然あります、重要なシステムでログが正常に出力されてなくて復旧できなくなったら洒落にならないので」「なるほど」「そうなったのを見たことならあります😆」
⚓rails-auth: authenticationとauthorizationをミドルウェアベースで実現(Ruby Weeklyより)
- Railsの場合: Rails Usage · square/rails-auth Wiki
- 他のライブラリとの比較: Comparison With Other Libraries · square/rails-auth Wiki
つっつきボイス:「authentication(認証)とauthorization(認可)をどっちもやれるgemみたいです」
Rails::Auth
は、authentication(以下AuthN)やauthorization(以下AuthZ)でRackミドルウェアを用いるよう設計された柔軟なライブラリです。AuthNの手順とAuthZの手順を別々のミドルウェアクラスに分割し、最初にAuthNのミドルウェアでcredential(X.509証明書やcookieなど)を検証し、続いてcredential(アクセス制御リストなど)を使うリクエストを別のAuthZ用ミドルウェアで認可します。
Rails::Auth
による認証や認可は、ブラウザcookieを用いてエンドユーザーに対して使うことも、X.509クライアント証明書を用いてサービス間リクエストに対して使うことも、適切な認証ミドルウェアを持つcredentialを用いてその他のクライアントに対して使うこともできます。
名前とはうらはらに、SinatraなどのRackベースのフレームワークでも使えます。
同READMEより大意
# 同リポジトリより
module MyApp
class Application < Rails::Application
[...]
Rails::Auth::ConfigBuilder.application(config, matchers: { allow_x509_subject: Rails::Auth::X509::Matcher })
end
end
「X.509マッチャーとかあるんだ、やるな〜」「あれ、X.509って何でしたっけ?」「OpenSSLの証明書とかでよく使われているヤツですね」
「OpenSSLとかで証明書を表示するとこういうのがどひゃ〜っと出力されるんですよ↓」「あ〜これですか😳」「ちらっと見た感じでは、このgemはクライアント証明書も使えるという触れ込みなんでしょうね」
# Wikipediaより
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
10:e6:fc:62:b7:41:8a:d5:00:5e:45:b6
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=BE, O=GlobalSign nv-sa, CN=GlobalSign Organization Validation CA - SHA256 - G2
Validity
Not Before: Nov 21 08:00:00 2016 GMT
Not After : Nov 22 07:59:59 2017 GMT
Subject: C=US, ST=California, L=San Francisco, O=Wikimedia Foundation, Inc., CN=*.wikipedia.org
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
00:c9:22:69:31:8a:d6:6c:ea:da:c3:7f:2c:ac:a5:
af:c0:02:ea:81:cb:65:b9:fd:0c:6d:46:5b:c9:1e:
9d:3b:ef
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
(省略)
「READMEにX.509クライアント証明書を使うって書いてあるところからして、このgemはもしかするとRailsにSSLというかTLSを解釈させるものなんだろうか?」
参考: クライアント証明書とは - IT用語辞典 e-Words
「コード覗いてみると↓、このgemはX.509証明書を受け取るようになってる」「証明書を直接扱ってるような感じがありますね」「でないとSubject Alternate Names(SANs)とか取り出せないし」
# lib/rails/auth/x509/certificate.rb#10
def initialize(certificate)
unless certificate.is_a?(OpenSSL::X509::Certificate)
raise TypeError, "expecting OpenSSL::X509::Certificate, got #{certificate.class}"
end
@certificate = certificate.freeze
@subject = {}
@certificate.subject.to_a.each do |name, data, _type|
@subject[name.freeze] = data.freeze
end
@subject_alt_names = SubjectAltNameExtension.new(certificate)
@subject_alt_names.freeze
@subject.freeze
end
「Deviseとかとは使いみちが少し違う感じでしょうか?」「Wikiに書いてあるような、マイクロサービスでクライアント証明書を使うみたいな用途なら、接続先が限定されるし偽装がほぼ不可能になるという意味でわかるような気がするけど」
「gemspecを見ると↓、やっぱりこのgemはTLSをしゃべるのか」「opensslもrequire
してるし」「TLSをしゃべらせるのって大変そうだけど、よく作ったな〜」
lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "rails/auth/version"
Gem::Specification.new do |spec|
spec.name = "rails-auth"
spec.version = Rails::Auth::VERSION
spec.authors = ["Tony Arcieri"]
spec.email = ["tonyarcieri@squareup.com"]
spec.homepage = "https://github.com/square/rails-auth/"
spec.licenses = ["Apache-2.0"]
spec.summary = "Modular resource-oriented authentication and authorization for Rails/Rack"
spec.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ")
A plugin-based framework for supporting multiple authentication and
authorization systems in Rails/Rack apps. Supports resource-oriented
route-by-route access control lists with TLS authentication.
DESCRIPTION
# (省略)
「どんなふうに使うのかがまだピンとこない」「READMEにもうちょっと情報があるといいんですけど」
「ところでこのリポジトリのSquareってあの決済システムのSquareなのかな?」「最初違うかなと思ったけど、アイコンも見覚えあるし、どうやらそうみたい」
“We are resilient. We are tenacious. We have survived the Great Depression. We have survived the recession. We are survivors.” —Lauren Stovall, Hot Sam’s Detroit
This year, running a business is a different story. This is Lauren’s.
— Square (@Square) August 11, 2020
「iPhoneのヘッドフォン端子に挿して使うSquareの決済システムがコミケとかでも使われてたりしますね」
参考: サークル参加者と語る、「同人サークルとクレジットカード決済」
「rails-authはもしかするとSquareが主に内部で使っているのかも?🤔」「あ〜そうかも」「クライアント認証をRubyで書きたかったのかも」「Rackミドルウェアでやってるみたいだからまあいいけど、もし名前のとおりにRailsでクライアント認証とかやってたら速度とかつらそうですけどね」
前編は以上です。
バックナンバー(2020年度第3四半期)
週刊Railsウォッチ(20200811山の日短縮版)RSpec Queueでパラレルテスト、カロリーメイトとRubyのコラボ、Rubyのcoercionほか
- 20200804後編 「RubyKaigi Takeout 2020」9月オンライン開催、メールバリデータtruemail、Gitのmasterが変更可能にほか
- 20200803前編 書籍『パーフェクトRuby on Rails』増補改訂版、マルチDBで抽象クラスをscaffold生成、GitLabがPumaに乗り換えほか
- 20200721後編 『パーフェクトRuby on Rails』増補改訂版発売間近、scan_left gemでレイジーなinjectほか
- 20200720前編 10月開催「Kaigi on Rails」CFP募集中、enumにデフォルト値設定機能、RailsでBitemporal Data Modelほか
- 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など)です。