Active Record の基礎 2.1 命名規約
Railsでは、データベースのテーブル名を探索するときに、モデルのクラス名を複数形にした名前で探索します。たとえば、Bookというモデルクラスがある場合、これに対応するデータベースのテーブルは複数形の「books」になります。Railsの複数形化メカニズムは非常に強力で、不規則な語でも複数形/単数形に変換できます(person <-> peopleなど)。これにはActive Supportの pluralizeメソッドが使われています。
ここ気になっていたので、Active Supportの pluralizeメソッドを見ていく。
Active Support コア拡張機能 - Railsガイド
ここにあるようにレシーバを「複数形」にしたものを返す。
If the optional parameter +locale+ is specified, the word will be pluralized as a word of that language. By default, this parameter is set to :en. You must define your own inflection rules for languages other than English.
オプションのパラメータ +locale+ が指定された場合、 指定された言語の単語として複数形化されます。 デフォルトでは、このパラメータは :en に設定されています。 英語以外の言語については、独自の活用規則を定義する必要があります。
らしく、locale によって変えられるんだ。初めて知った。
def pluralize(count = nil, locale = :en) locale = count if count.is_a?(Symbol) if count == 1 dup else ActiveSupport::Inflector.pluralize(self, locale) end end
count if count.is_a?(Symbol) となっているけど、count って symbol が渡されるんだ。数値かと思った。
しかも count の結果を locale に入れている?
あー、count に :ja とかが入っている場合は、else' に落ちて ActiveSupport::Inflector.pluralize(self, locale) で locale を渡しているのかー。なるほど。
dup ってなんだっけ。「自身と同じクラスのオブジェクトを作成後、自身のインスタンス変数を全て新たに作成したオブジェクトにコピーします」らしい。なるほど。
Rake::Cloneable#dup (Ruby 4.0 リファレンスマニュアル)
ActiveSupport::Inflector.pluralize(self, locale) って何してるんだ?
def pluralize(word, locale = :en) apply_inflections(word, inflections(locale).plurals, locale) end
apply_inflections を呼んでいる。
inflections(locale).plurals って何を返すの?
def inflections(locale = :en) if block_given? yield Inflections.instance(locale) else Inflections.instance_or_fallback(locale) end end
なるほどー。ブロックが渡されることもあるんだね。
大体は Inflections.instance_or_fallback(locale) に落ちるのかな?
じゃあ Inflections.instance_or_fallback(locale) はなんぞや
def self.instance_or_fallback(locale) # :en であれば @__en_instance__ == :en, なければ インスタンス が入る return @__en_instance__ ||= new if locale == :en I18n.fallbacks[locale].each do |k| return @__en_instance__ if k == :en && @__en_instance__ # それ以外の言語だったら それを返す # @__instance__ は Concurrent::Map.new return @__instance__[k] if @__instance__.key?(k) end instance(locale) end
@__en_instance__ って何?
チャッピー に聞いたところ、普通のインスタンス変数とのこと。
Ruby コミュニティでよくある 慣習的な命名。らしく、メモ化用のインスタンス変数っぽい。へー。
instance というメソッドも定義されている
def self.instance(locale = :en) return @__en_instance__ ||= new if locale == :en @__instance__[locale] ||= new end
:en か それ以外の言語の ActiveSupport::Inflector::Inflections インスタンスを返すのか
それで inflections(locale).plurals
ここで空配列が代入され、
ここで書かれているように inflect.plural が実行されるみたい。
@plurals.prepend([rule, replacement]) で配列を返している
# 文字列, [[rule, replacement], [...]], locale def apply_inflections(word, rules, locale = :en) result = word.to_s.dup if word.empty? || inflections(locale).uncountables.uncountable?(result) # 不可算名詞ならそのまま 文字列を返す # information → information result else # word 一部分に最初にマッチした文字列を置換する # cat → cats # dog → dogs # cactus → cacti(irregular) rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) } result end end
sub! ってなんだっけ。
「文字列中で pattern にマッチした最初の部分を文字列 replace で置き換えた文字列を生成して返します。」か。
String#sub (Ruby 4.0 リファレンスマニュアル)
Uncountables.uncountable? って何?
def uncountable?(str) if @pattern.nil? # /(equipment|information|rice)/i みたいになるっぽい。わからん... members_pattern = Regexp.union(@members.map { |w| /#{Regexp.escape(w)}/i }) @pattern = /\b#{members_pattern}\Z/i end @pattern.match?(str) end
不可算名詞かどうか判定しているのか。なるほど。
pluralize がの呼び出し元がどこかざっと見てみたが、結構あったのでどこから見ていくか迷い中。
次はここから呼び出し元を追ってみる。
ActiveModel::Name が initialize されるタイミングっていつ?
というか ActiveModel::Name って何の責務を持っているクラス?
> Book.model_name => #<ActiveModel::Name:0x0000000122911040 @collection="books", @element="book", @human="Book", @i18n_key=:book, @klass=Book (call 'Book.load_schema' to load schema informations), @name="Book", @param_key="book", @plural="books", @route_key="books", @singular="book", @singular_route_key="book", @uncountable=false> > Book.model_name.plural => "books"
なるほどー。I18n、partial、ルーティングとかで使われているのか。
ちなみに ActiveModel::Naming.model_name が Book でも呼び出せるってことは、つまり Book インスタンスでは ActiveModel::Naming module が extend されているってことだろうな。
基本的に継承関係としては User < ApplicationRecord < ActiveRecord::Base になるから、ActiveRecord::Base の中で extend ActiveModel::Naming しているはず...!
見てみたけど、してないな。
include している中でさらに extend しているのかなー。
include ActiveModel::API が怪しいよね。
見つけた。
included しているから、ActiveRecord::Base で ActiveModel::API が include されたタイミングで ActiveModel::Naming を extend して特異メソッド(クラスメソッド)にしているのかー。
だから、ActiveRecord::Base クラス自体で model_name は呼び出せる状態になっているから、サブクラスの User でも呼び出せるってわけか。
流れは把握できた。
図にしてみる
Book.model_name.plural の流れ
flowchart TD
A["Book.model_name.plural"] --> B["Book.model_name"]
B --> C["ActiveModel::Naming#model_name"]
C --> D{"@_model_nameがキャッシュされているか"}
D -- "Yes" --> E["キャッシュされたActiveModel::Name を返す"]
D -- "No" --> F["ActiveModel::Name.new(Book, namespace=nil)"]
F --> G["ActiveModel::Name#initialize @singular, @plural ... を計算"]
G --> H["@_model_name に代入"]
H --> I["ActiveModel::Name を返す"]
E --> J[".plural を呼ぶ"]
I --> J
J --> K["'books'"]
ActiveSupport::Inflector.pluralize(@singular, locale) の流れ
flowchart TD
B["ActiveSupport::Inflector.pluralize('book', :en)"]
B --> C["apply_inflections(word, inflections(locale).plurals, locale)"]
C --> D{"uncountable? or empty?"}
D -- "Yes" --> E["'book'"]
D -- "No" --> F["rules を順に試して\nsub! で最初にマッチした変換を適用"]
F --> G["'books'"]




