ActiveRecordでデフォルト値を設定するにはどうすればよいですか?
-
11-07-2019 - |
質問
ActiveRecordでデフォルト値を設定するにはどうすればよいですか
Pratikの投稿で、complicatedい複雑なコードの塊について説明しています: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model
class Item < ActiveRecord::Base
def initialize_with_defaults(attrs = nil, &block)
initialize_without_defaults(attrs) do
setter = lambda { |key, value| self.send("#{key.to_s}=", value) unless
!attrs.nil? && attrs.keys.map(&:to_s).include?(key.to_s) }
setter.call('scheduler_type', 'hotseat')
yield self if block_given?
end
end
alias_method_chain :initialize, :defaults
end
次の例がうろついているのを見ました:
def initialize
super
self.status = ACTIVE unless self.status
end
and
def after_initialize
return unless new_record?
self.status = ACTIVE
end
また、人々が移行に使用するのを見たことがありますが、モデルコードで定義されていることを望んでいます。
ActiveRecordモデルのフィールドにデフォルト値を設定する標準的な方法はありますか?
解決
使用可能な各メソッドにはいくつかの問題がありますが、 after_initialize
コールバックを定義することは、次の理由で進むべき方法だと思います:
-
default_scope
は新しいモデルの値を初期化しますが、それがモデルを見つけるスコープになります。いくつかの数字を0に初期化するだけの場合、これはあなたが望むものではありません。 - 移行のデフォルトを定義することも一部の場合に機能します...既に述べたように、Model.newを呼び出しただけでは、これは機能しません 。
-
initialize
のオーバーライドは機能しますが、super
を呼び出すことを忘れないでください! - phusionのようなプラグインを使用するのは少しばかげています。これはルビーです、いくつかのデフォルト値を初期化するためだけにプラグインが本当に必要ですか?
-
after_initialize
をオーバーライドすると、廃止されます。Rails3.0.3でafter_initialize
をオーバーライドすると、コンソールに次の警告が表示されます。 :
非推奨の警告:Base#after_initializeは非推奨になりました。代わりにBase.after_initialize:methodを使用してください。 (/ Users / me / myapp / app / models / my_model:15から呼び出されます)
したがって、 after_initialize
コールバックを記述します。これにより、デフォルトの属性に加えて、関連付けのデフォルトを次のように設定できます。
class Person < ActiveRecord::Base
has_one :address
after_initialize :init
def init
self.number ||= 0.0 #will set the default value only if it's nil
self.address ||= build_address #let's you set a default association
end
end
これで、モデルの初期化を検索する場所がたった1つになりました。誰かがより良い方法を思いつくまで、私はこの方法を使用しています。
警告:
-
ブール値フィールドの場合:
self.bool_field = self.bool_field.nilの場合はtrue?
詳細については、この回答に関するPaul Russellのコメントを参照してください
-
モデルの列のサブセットのみを選択する場合(つまり、
Person.select(:firstname、:lastname).allなどのクエリで
)select
を使用する場合)init
メソッドがselect
句に含まれていない列にアクセスすると、MissingAttributeError
を受け取ります。次のようにして、このケースを防ぐことができます。self.number || = 0.0 self.has_attribute?の場合:number
およびブール列の場合...
self.bool_field = true if(self.has_attribute?:bool_value)&amp;&amp; self.bool_field.nil?
また、Rails 3.2より前の構文は異なることに注意してください(以下のCliff Darlingのコメントを参照してください)
他のヒント
(各列定義で:default
オプションを指定することにより)移行によりデータベースにデフォルト値を設定し、Active Recordがこれらの値を使用して各属性のデフォルトを設定するようにします。
私見、このアプローチはARの原則に沿っています:構成より規約、DRY、テーブル定義がモデルを駆動します。逆ではありません。
デフォルトはアプリケーション(Ruby)コードにありますが、モデルではなく移行にあります。
レール5 +
属性を使用できますモデル内のメソッド。例:
class Account < ApplicationRecord
attribute :locale, :string, default: 'en'
end
ラムダを default
パラメータに渡すこともできます。例:
attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }
いくつかの単純なケースは、データベーススキーマでデフォルトを定義することで処理できますが、計算値や他のモデルのキーを含む多くのトリッキーなケースは処理しません。これらの場合、私はこれを行います:
after_initialize :defaults
def defaults
unless persisted?
self.extras||={}
self.other_stuff||="This stuff"
self.assoc = [OtherModel.find_by_name('special')]
end
end
after_initializeを使用することにしましたが、新規または作成されたオブジェクトのみが見つかったオブジェクトに適用したくありません。 after_newコールバックがこの明らかなユースケースに提供されていないことは、ほとんど衝撃的だと思いますが、オブジェクトが既に保持されているかどうかを確認して、新しいものではないことを確認しました。
Brad Murrayの回答を見た場合、条件がコールバックリクエストに移動した場合、これはさらにクリーンになります。
after_initialize :defaults, unless: :persisted?
# ":if => :new_record?" is equivalent in this context
def defaults
self.extras||={}
self.other_stuff||="This stuff"
self.assoc = [OtherModel.find_by_name('special')]
end
after_initializeコールバックパターンは、次のことを行うだけで改善できます
after_initialize :some_method_goes_here, :if => :new_record?
initコードで関連付けを処理する必要がある場合、これは重要です。関連付けを含めずに初期レコードを読み取ると、次のコードが微妙なn + 1をトリガーします。
class Account
has_one :config
after_initialize :init_config
def init_config
self.config ||= build_config
end
end
Phusionのメンバーには、このためのプラグインがあります。
>提案された回答よりも優れた/クリーンな潜在的な方法は、次のようにアクセサーを上書きすることです:
def status
self['status'] || ACTIVE
end
「デフォルトアクセサの上書き」を参照してください。 ActiveRecord :: Baseドキュメントおよびselfの使用に関するStackOverflowの詳細。
ドキュメントから:
sudo gem install attribute-defaults
を実行し、 require 'attribute_defaults'
をアプリに追加します。
class Foo < ActiveRecord::Base
attr_default :age, 18
attr_default :last_seen do
Time.now
end
end
Foo.new() # => age: 18, last_seen => "2014-10-17 09:44:27"
Foo.new(:age => 25) # => age: 25, last_seen => "2014-10-17 09:44:28"
同様の質問ですが、すべてコンテキストが少し異なります -作成方法Rails activerecordのモデルの属性のデフォルト値
ベストアンサー:必要なものに依存します
すべてのオブジェクトに 値で開始する場合: after_initialize:init
ページを開いたときに new.html
フォームにデフォルト値を設定しますか? https://stackoverflow.com/a/5127684/1536309
class Person < ActiveRecord::Base
has_one :address
after_initialize :init
def init
self.number ||= 0.0 #will set the default value only if it's nil
self.address ||= build_address #let's you set a default association
end
...
end
すべてのオブジェクトに ユーザー入力から計算された値を持たせる場合: before_save:default_values
を使用します
ユーザーに X
を入力してから、 Y = X + 'foo'
を入力しますか?使用:
class Task < ActiveRecord::Base
before_save :default_values
def default_values
self.status ||= 'P'
end
end
これがコンストラクタの目的です!モデルの initialize
メソッドをオーバーライドします。
after_initialize
メソッドを使用します。
すみません、私は次のことをしました:
def after_initialize
self.extras||={}
self.other_stuff||="This stuff"
end
魅力のように動作します!
まず最初に:ジェフの答えには同意しません。アプリが小さく、ロジックがシンプルな場合に意味があります。ここでは、より大きなアプリケーションを構築および保守する際に、それがどのように問題になるかについての洞察を提供しようとしています。小さなものを構築するときは、最初にこのアプローチを使用することはお勧めしませんが、代替アプローチとしてそれを念頭に置いてください:
ここでの質問は、このレコードのデフォルトがビジネスロジックであるかどうかです。もしそうなら、ORMモデルに入れるのは慎重です。 rywが言及するフィールドはアクティブであるため、これはビジネスロジックのように聞こえます。例えば。ユーザーはアクティブです。
ビジネス上の懸念をORMモデルに入れることに慎重になるのはなぜですか?
-
SRP に違反します。 ActiveRecord :: Baseを継承するクラスは、すでにさまざまなことを多く行っています。その主なものは、データの一貫性(検証)と永続性(保存)です。 AR :: Baseを使用してビジネスロジックを小さくしても、SRPが壊れます。
-
テストに時間がかかります。 ORMモデルで発生する何らかのロジックをテストする場合、テストを実行するにはRailsを初期化する必要があります。これはアプリケーションの最初の段階ではあまり問題になりませんが、単体テストの実行に時間がかかるまで蓄積されます。
-
これは、SRPをさらに具体的に破壊します。アイテムがアクティブになったときにユーザーにメールを送信する必要があるようになったとしますか?次に、アイテムORMモデルにメールロジックを追加します。このモデルの主な役割はアイテムのモデリングです。電子メールロジックを気にする必要はありません。これは、ビジネスの副作用の場合です。これらはORMモデルに属していません。
-
多様化することは困難です。初期化ロジックを制御することのみを目的とする、init_type:stringフィールドに裏付けされたデータベースのようなものを持つ成熟したRailsアプリを見てきました。これは、構造的な問題を修正するためにデータベースを汚染しています。もっと良い方法があると思います。
POROの方法:これはもう少しコードですが、ORMモデルとビジネスロジックを別々に保つことができます。ここのコードは単純化されていますが、アイデアを示す必要があります。
class SellableItemFactory
def self.new(attributes = {})
record = Item.new(attributes)
record.active = true if record.active.nil?
record
end
end
次に、これを配置して、新しいアイテムを作成する方法は次のようになります
SellableItemFactory.new
そして、私のテストでは、ItemFactoryに値がない場合、ItemFactoryがItemでアクティブに設定されていることを確認するだけでした。 Railsの初期化は不要で、SRPの破損もありません。アイテムの初期化がより高度になると(たとえば、ステータスフィールド、デフォルトタイプなどを設定する)、ItemFactoryはこれを追加できます。 2種類のデフォルトになった場合、これを行うために新しいBusinesCaseItemFactoryを作成できます。
注:ファクトリが多くのアクティブなものを構築できるように、ここで依存性注入を使用することも有益かもしれませんが、簡単にするために省略しました。ここにあります: self.new(klass =アイテム、属性= {})
これは長い間回答されてきましたが、デフォルト値が頻繁に必要であり、データベースに入れないことを好みます。 DefaultValues
の懸念事項を作成します:
module DefaultValues
extend ActiveSupport::Concern
class_methods do
def defaults(attr, to: nil, on: :initialize)
method_name = "set_default_#{attr}"
send "after_#{on}", method_name.to_sym
define_method(method_name) do
if send(attr)
send(attr)
else
value = to.is_a?(Proc) ? to.call : to
send("#{attr}=", value)
end
end
private method_name
end
end
end
そして、私のモデルで次のように使用します:
class Widget < ApplicationRecord
include DefaultValues
defaults :category, to: 'uncategorized'
defaults :token, to: -> { SecureRandom.uuid }
end
私は人々が彼らの移行にそれを見たのを見たことがありますが、私はむしろそれを見たいです モデルコードで定義されています。
のフィールドにデフォルト値を設定する標準的な方法はありますか ActiveRecordモデル?
Rails 5以前の標準的なRailsの方法は、実際に移行時に設定することであり、デフォルト値が設定されているのを確認したいときはいつでも db / schema.rb
を見るだけでした。任意のモデルのDB。
@Jeff Perrinの答えが示すものとは異なり(少し古い)、移行アプローチでは、Railsの魔法により、 Model.new
の使用時にデフォルトが適用されます。 Rails 4.1.16での動作確認済み。
多くの場合、最も単純なものが最適です。コードベースでの知識の負債と潜在的な混乱点の減少。そして、「うまくいく」。
class AddStatusToItem < ActiveRecord::Migration
def change
add_column :items, :scheduler_type, :string, { null: false, default: "hotseat" }
end
end
null:false
は、DBでNULL値を許可しません。また、追加の利点として、既存のすべてのDBレコードもこのフィールドのデフォルト値で更新されます。必要に応じて、移行でこのパラメーターを除外できますが、非常に便利です!
Rails 5+の標準的な方法は、@ Lucas Catonが言ったように:
class Item < ActiveRecord::Base
attribute :scheduler_type, :string, default: 'hotseat'
end
after_initializeソリューションの問題は、この属性にアクセスするかどうかに関係なく、DBからルックアップするすべてのオブジェクトにafter_initializeを追加する必要があることです。遅延読み込みアプローチをお勧めします。
もちろん、属性メソッド(getter)はメソッド自体なので、それらをオーバーライドしてデフォルトを提供できます。次のようなもの:
Class Foo < ActiveRecord::Base
# has a DB column/field atttribute called 'status'
def status
(val = read_attribute(:status)).nil? ? 'ACTIVE' : val
end
end
誰かが指摘したように、Foo.find_by_status( 'ACTIVE')を実行する必要があります。その場合、DBがサポートしていれば、データベースの制約にデフォルトを設定する必要があると思います。
複雑な検索を実行すると ActiveModel :: MissingAttributeError
エラーが発生する after_initialize
で問題が発生しました:
eg:
@bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)
&quot;検索&quot; .where
は条件のハッシュです
だから私はこの方法で初期化をオーバーライドすることでそれをやった:
def initialize
super
default_values
end
private
def default_values
self.date_received ||= Date.current
end
super
呼び出しは、カスタマイズコードを実行する前に、 ActiveRecord :: Base
からオブジェクトが正しく初期化されていることを確認するために必要です。つまり、default_values
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
&quot; default_value_for&quot;を使用することを強くお勧めします。 gem: https://github.com/FooBarWidget/default_value_for
初期化メソッドをオーバーライドすることを必要とするトリッキーなシナリオがいくつかありますが、それはgemが行います。
例:
データベースのデフォルトはNULL、モデル/ルビ定義のデフォルトは&quot; some string&quot;ですが、実際には を使用して、何らかの理由で値をnilに設定します: MyModel.new (my_attr:nil)
ほとんどのソリューションでは、値をnilに設定できず、代わりにデフォルトに設定します。
OK、そのため || =
アプローチをとる代わりに、 my_attr_changed?
...
BUT は、データベースのデフォルトが&quot; some string&quot;であり、モデル/ルビー定義のデフォルトが&quot; some other string&quot;であると想像しますが、特定のシナリオでは want を使用して値を&quot; some string&quot;に設定します(dbのデフォルト): MyModel.new(my_attr: 'some_string')
この結果、 my_attr_changed?
は false になります。これは、値がdbのデフォルトと一致するためです。これにより、ルビー定義のデフォルトコードが起動され、値が&quotに設定されます;その他の文字列&quot; -繰り返しますが、あなたが望むものではありません。
これらの理由から、これはafter_initializeフックだけでは適切に達成できるとは思いません。
繰り返しますが、&quot; default_value_for&quot; gemは正しいアプローチを取っています: https://github.com/FooBarWidget/default_value_for
デフォルト値を設定するためにこれを行うと、ほとんどの場合、混乱を招き、扱いにくくなりますが、:default_scope
も使用できます。 squilのコメントはこちらをご覧ください。
after_initializeメソッドは廃止されました。代わりにコールバックを使用してください。
after_initialize :defaults
def defaults
self.extras||={}
self.other_stuff||="This stuff"
end
ただし、移行で:default を使用するのが最もクリーンな方法です。
検証メソッドを使用すると、デフォルト設定の多くの制御が提供されることがわかりました。更新のデフォルトを設定する(または検証に失敗する)こともできます。本当に必要な場合は、挿入と更新に異なるデフォルト値を設定します。 デフォルトは#validまで設定されないことに注意してください。が呼び出されます。
class MyModel
validate :init_defaults
private
def init_defaults
if new_record?
self.some_int ||= 1
elsif some_int.nil?
errors.add(:some_int, "can't be blank on update")
end
end
end
after_initializeメソッドの定義については、after_initializeも:findによって返される各オブジェクトによって呼び出されるため、パフォーマンスの問題が発生する可能性があります。 http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find
列が「ステータス」タイプの列であり、モデルがステートマシンの使用に適している場合は、 aasm gem 、その後は簡単に実行できます
aasm column: "status" do
state :available, initial: true
state :used
# transitions
end
未保存のレコードの値はまだ初期化されませんが、 init
などを使用して独自のロールを作成するよりも少しクリーンであり、すべてのステータス。
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end
ここに私が使用した解決策がありますが、まだ追加されていないことに少し驚いていました。
これには2つの部分があります。最初の部分は実際の移行でデフォルトを設定し、2番目の部分はモデルに検証を追加して、存在が正しいことを確認します。
add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'
ここで、デフォルトがすでに設定されていることがわかります。検証では、文字列の値が常にあることを確認したいので、
validates :new_team_signature, presence: true
これにより、デフォルト値が設定されます。 (私にとっては「チームへようこそ」とあります)、さらに一歩進んで、そのオブジェクトに常に値が存在することを確認します。
役立つことを願っています!
api docsから http://api.rubyonrails.org/classes/ActiveRecord/ Callbacks.html
モデルで before_validation
メソッドを使用します。これは、呼び出しを作成および更新するための特定の初期化を作成するオプションを提供します
例えばこの例(再びapi docsの例から取られたコード)では、クレジットカードの番号フィールドが初期化されます。これを簡単に調整して、必要な値を設定できます
class CreditCard < ActiveRecord::Base
# Strip everything but digits, so the user can specify "555 234 34" or
# "5552-3434" or both will mean "55523434"
before_validation(:on => :create) do
self.number = number.gsub(%r[^0-9]/, "") if attribute_present?("number")
end
end
class Subscription < ActiveRecord::Base
before_create :record_signup
private
def record_signup
self.signed_up_on = Date.today
end
end
class Firm < ActiveRecord::Base
# Destroys the associated clients and people when the firm is destroyed
before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
end
彼がここに提案されていないことに驚いた