mongodb/mongoid を読む - Associations 周辺の内部キャッシュ仕様
MongoDB Ruby の 公式ODM mongodb/mongoid をよく利用しています。
Associations has_many, has_one 等で定義したデータに一度アクセスすると、mongoid内部にキャッシュしてくれるが、最新のデータを取得したいケースのやり方・キャッシュ仕様を内部実装をソースを読んで調べたときのメモ。
読んだコミットID: 3c31c0f0adaa028c6c58a4fe09b81b332d5fcae9
TL;DR
-
Model#association
の第一引数に true 指定でインスタンス変数のキャッシュを利用せずに最新データを取得できる。
Model#association(true)
Associations(has_one, has_many等)の定義箇所
まず、Associations の has_one, has_many, belongs_to, has_and_belongs_to_many マクロの定義は、Mongoid::Document 内で以下モジュールを順にincludeで定義されている。
Mongoid::Composable
Mongoid::Association
Mongoid::Association::Macros
具体的な定義箇所
lib/mongoid/association/macro.rb
def belongs_to(name, options = {}, &block)
define_association!(__method__, name, options, &block)
end
def has_many(name, options = {}, &block)
define_association!(__method__, name, options, &block)
end
def has_and_belongs_to_many(name, options = {}, &block)
define_association!(__method__, name, options, &block)
end
def has_one(name, options = {}, &block)
define_association!(__method__, name, options, &block)
end
private
def define_association!(macro_name, name, options = {}, &block)
Association::MACRO_MAPPING[macro_name].new(self, name, options, &block).tap do |assoc|
assoc.setup!
self.relations = self.relations.merge(name => assoc)
end
end
define_association! で各Associationクラスを初期化して setup! にてgetter/setter 等の関数が定義されている。
例えば、has_many の setup! 関数の定義は以下のようになっている。
lib/mongoid/association/referenced/has_many.rb
def setup!
setup_instance_methods!
self
end
def setup_instance_methods!
define_getter!
define_ids_getter!
define_setter!
define_ids_setter!
define_existence_check!
define_autosaver!
polymorph!
define_dependency!
@owner_class.validates_associated(name) if validate?
self
end
(ざっくり)
各Association(has_one, has_many, belongs_to, has_and_belongs_to_many)に実装されている setup!
の内容は若干異なるが、内部でコールしている define_getter!
, define_setter!
は各Association共通でコールされている。
getterの定義
getterは以下内容で定義されている。
lib/mongoid/association/accessors.rb
# Defines the getter for the association. Nothing too special here: just
# return the instance variable for the association if it exists or build
# the thing.
#
# @example Set up the getter for the association.
# Person.define_getter!(association)
#
# @param [ Association ] association The association metadata for the association.
#
# @return [ Class ] The class being set up.
#
# @since 2.0.0.rc.1
def self.define_getter!(association)
name = association.name
association.inverse_class.tap do |klass|
klass.re_define_method(name) do |reload = false|
value = get_relation(name, association, nil, reload)
if value.nil? && association.autobuilding? && !without_autobuild?
value = send("build_#{name}")
end
value
end
end
end
getter内容であるklass.re_define_method(name)
で渡しているブロックの引数に reload = false
の定義があり、このブロック引数 reload
を利用することで、MongoDBへのデータリロードの有無を指定できる。
このreload
引数については get_relation
関数コメントの引数部分にもデータリロードの有無の振る舞いをする旨がコメントされている。
lib/mongoid/association/accessors.rb
# Get the association. Extracted out from the getter method to avoid
# infinite recursion when overriding the getter.
#
# @api private
#
# @example Get the association.
# document.get_relation(:name, association)
#
# @param [ Symbol ] name The name of the association.
# @param [ Association ] association The association metadata.
# @param [ Object ] object The object used to build the association.
# @param [ true, false ] reload If the association is to be reloaded.
#
# @return [ Proxy ] The association.
#
# @since 3.0.16
def get_relation(name, association, object, reload = false)
if !reload && (value = ivar(name)) != false
value
else
_building do
_loading do
if object && needs_no_database_query?(object, association)
__build__(name, object, association)
else
selected_fields = _mongoid_filter_selected_fields(association.key)
__build__(name, attributes[association.key], association, selected_fields)
end
end
end
end
end
(ざっくり)
get_relation
では、reload指定なしでインスタンス変数ivar(name)
にキャッシュしていたら、キャッシュ済みの値を返す。
キャッシュ使わなかった場合に実行される __build__
関数を追ってみると、create_relation
関数内で Association#build にて実際にクエリを発行し、set_relation
にてキャッシュとして発行したクエリをインスタンス変数に保存していた。
lib/mongoid/association/accessors.rb
def __build__(name, object, association, selected_fields = nil)
relation = create_relation(object, association, selected_fields)
set_relation(name, relation)
end
def create_relation(object, association, selected_fields = nil)
type = @attributes[association.inverse_type]
target = association.build(self, object, type, selected_fields)
target ? association.create_relation(self, target) : nil
end
def set_relation(name, relation)
instance_variable_set("@_#{name}", relation)
end
各Association(has_many, has_one, belongs_to, has_and_belongs_to_many) の#buildメソッドの内容は以下。
MongoDBへの問い合わせクエリが発行されていることがわかる。
has_many
lib/mongoid/association/referenced/has_many/buildable.rb
def build(base, object, type = nil, selected_fields = nil)
return (object || []) unless query?(object)
return [] if object.is_a?(Array)
query_criteria(object, base)
end
has_one
lib/mongoid/association/referenced/has_one/buildable.rb
def build(base, object, type = nil, selected_fields = nil)
if query?(object)
if !base.new_record?
execute_query(object, base)
end
else
clear_associated(object)
object
end
end
belongs_to
lib/mongoid/association/referenced/belongs_to/buildable.rb
def build(base, object, type = nil, selected_fields = nil)
return object unless query?(object)
execute_query(object, type)
end
has_and_belongs_to_many
lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb
def build(base, object, type = nil, selected_fields = nil)
if query?(object)
query_criteria(object)
else
object.try(:dup)
end
end
おわり
mongodb/mongoid の associations の getterを中心に内部キャッシュ仕様について調べました。association の第一引数にtrueするだけでMongoDBへのデータリロードを行うことができることがわかりました。他に良い方法があれば、コメントなりで教えてもらえると嬉しいです。
Discussion