rails-settings-cached gemについて、設定の読み出し方、値の更新の仕方などが興味があったので読んでみました。
github.com
なお、rails-settings-cachedのバージョンは2.9.4です。
簡単なgemの使い方
- ユーザーが以下のように設定ファイルを書くと
Setting.admin_emailのような形で値を設定できる
Setting.admin_email = 'abc'のようにすると値をDBに保存し、今後はその値が使われる
class Setting < RailsSettings::Base
cache_prefix { "v1" }
field :admin_email, default: "admin@example.com", type: :string
end
1.fieldの定義で読み出しと上書きのメソッドを提供している仕組み
対応するコード
rails-settings-cached-2.9.4/lib/rails-settings/base.rb
def field(key, **opts)
_define_field(key, **opts)
end
private
def _define_field(key, default: nil, type: :string, readonly: false, separator: nil, validates: nil, **opts)
key = key.to_s
raise ProtectedKeyError.new(key) if PROTECTED_KEYS.include?(key)
field = ::RailsSettings::Fields::Base.generate(
scope: @scope, key: key, default: default,
type: type, readonly: readonly, options: opts,
separator: separator, parent: self
)
@defined_fields ||= []
@defined_fields << field
define_singleton_method(key) { field.read }
unless readonly
define_singleton_method("#{key}=") { |value| field.save!(value: value) }
if validates
validates[:if] = proc { |item| item.var.to_s == key }
send(:validates, key, **validates)
define_method(:read_attribute_for_validation) { |_key| self.value }
end
end
if type == :boolean
define_singleton_method("#{key}?") do
send(key)
end
end
define_method(key) do
self.class.public_send(key)
end
end
コードリーディグしてわかったこと
field = ::RailsSettings::Fields::Base.generate(...)の定義は以下
typeの定義に応じて、RailsSettings::Fields::XXXのインスタンスを作っている
rails-settings-cached-2.9.4/lib/rails-settings/fields/base.rb
class << self
def generate(**args)
fetch_field_class(args[:type]).new(**args)
end
private
def fetch_field_class(type)
field_class_name = type.to_s.split("_").map(&:capitalize).join("")
begin
const_get("::RailsSettings::Fields::#{field_class_name}")
rescue StandardError
::RailsSettings::Fields::String
end
end
end
define_singleton_method(key) { field.read }の部分で、fieldに記載したものを読み出しメソッドにしている
define_singleton_method("#{key}=") { |value| field.save!(value: value) }の部分で、更新用のメソッドを定義している
- ちなみに、真偽値の場合は以下で
?メソッドも提供するようにしているのもわかる
2. 設定値をDB→デフォルト値という順番で読み出す箇所のコード
前述の通り、field.readを見れば良い
対応するコード
rails-settings-cached-2.9.4/lib/rails-settings/fields/base.rb
def read
return deserialize(default_value) if readonly || saved_value.nil?
deserialize(saved_value)
end
def deserialize(value)
raise NotImplementedError
end
def saved_value
return parent.send(:_all_settings)[key] if table_exists?
puts(
"WARNING: table: \"#{parent.table_name}\" does not exist or not database connection, `#{parent.name}.#{key}` fallback to returns the default value."
)
nil
end
コードリーディグしてわかったこと
- DBに値が保存されている場合(
saved_valueが存在する場合)はその値を引数にして、そうでない場合はデフォルト値を引数にしてdeserializeを呼んでいる
deserializeは、fieldの各クラスに実装されている
- 例えば、
RailsSettings::Fields::Hashでは、with_indifferent_accessでkeyに対して文字列でもシンボルでもアクセスできるようにされていることがわかる
rails-settings-cached-2.9.4/lib/rails-settings/fields/hash.rb
module RailsSettings
module Fields
class Hash < ::RailsSettings::Fields::Base
def deserialize(value)
return nil if value.nil?
return value unless value.is_a?(::String)
load_value(value).deep_stringify_keys.with_indifferent_access
end
end
end
end
- DB設定値(
_all_settings)からkeyで探し、一致すればそれを返すことになっている
3. 値のキャッシュの仕方
saved_valueのparent.send(:_all_settings)[key]の中身を見れば良い。
parentはユーザー定義のSettingクラス
対応するコード
rails-settings-cached-2.9.4/lib/rails-settings/base.rb
def _all_settings
RequestCache.all_settings ||= cache_storage.fetch(cache_key, expires_in: 1.week) do
vars = unscoped.select("var, value")
result = {}
vars.each { |record| result[record.var] = record.value }
result.with_indifferent_access
end
end
コードリーディングしてわかったこと
- Railsのキャッシュの仕組みを使って、キャッシュされたものがなければすべての値をselectで取得し、そこから値を探している
4. 「=」でDB保存をしているコード
前述の通り、=でfield.save!が呼ばれる仕組み
def save!(value:)
serialized_value = serialize(value)
parent_record = parent.find_by(var: key) || parent.new(var: key)
parent_record.value = serialized_value
parent_record.save!
parent_record.value
end
- 値を読み込んで、すでに存在するか判断した上でsaveしているというシンプルな作り