Rails 自引用属性继承
Rails Self Referencing Attribute inheritance
我有一个名为 Option 的模型,它有一个 self-referential 关联。一个选项可以有多个子选项,一个子选项可以有一个 parent 选项。我还使用 cocoon gem 的嵌套属性在 form_for 选项上创建多个子选项。我可以在表单上创建选项时动态创建子选项。
views/options/_form.html.erb:
<%= form_for @option do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<%= f.label :activity %><br>
<%= f.select :activity_id, options_for_select(activity_array, @option.activity_id)%><br>
</p>
<div>
<div id="suboptions">
<%= f.fields_for :suboptions do |suboption| %>
<%= render 'suboption_fields', f: suboption %>
<% end %>
<div class="links">
<%= link_to_add_association 'add suboption', f, :suboptions %>
</div>
</div>
</div>
<p>
<%= f.submit "Send" %>
</p>
<% end %>
model/option.rb:
class Option < ApplicationRecord
belongs_to :activity
has_many :option_students
has_many :students, through: :option_students
has_many :suboptions, class_name: "Option", foreign_key: "option_id"
belongs_to :parent, class_name: "Option", optional: true, foreign_key: "option_id"
accepts_nested_attributes_for :suboptions, allow_destroy: true,
reject_if: ->(attrs) { attrs['name'].blank? }
validates :name, presence: true
after_initialize :set_defaults
before_update :set_defaults
def set_defaults
self.suboptions.each do |sbp|
sbp.activity_id = self.activity_id
end
end
end
参数:
def option_params
params.require(:option).permit(:name, :activity_id, :students_ids => [], suboptions_attributes: [:id, :name, activity_id, :_destroy])
end
我希望每个子选项在创建和更新时都从 parent 继承 activity_id 属性。我通过在模型上使用 set_defaults 方法尝试了这种方式,它适用于具有新嵌套子选项的新选项,如果我更新 activity_id ,它还会更新子选项的 activity_id parent。但是,如果我在更新时创建另一个子选项,它不会将 parent 中的属性传递给新的子选项。
您可以使用before_validation回调。例如,
测试代码
class Location < ApplicationRecord
has_many :children, class_name: 'Location', foreign_key: 'parent_id'
accepts_nested_attributes_for :children
before_validation :initialize_children
attr_accessor :activity
def initialize_children
children.each { |c| c.activity_id = self.activity_id }
end
end
Rails 控制台
irb(main):002:0> Location.create({name: "L10", activity_id: 200, :children_attributes => [{name: "L12"}]})
(0.1ms) begin transaction
SQL (1.1ms) INSERT INTO "locations" ("name", "activity_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "L10"], ["activity_id", 200], ["created_at", 2017-01-11 04:07:26 UTC], ["updated_at", 2017-01-11 04:07:26 UTC]]
SQL (0.1ms) INSERT INTO "locations" ("name", "parent_id", "activity_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["name", "L12"], ["parent_id", "1"], ["activity_id", 200], ["created_at", 2017-01-11 04:07:26 UTC], ["updated_at", 2017-01-11 04:07:26 UTC]]
(3.7ms) commit transaction
=> #<Location id: 1, name: "L10", parent_id: nil, activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">
irb(main):003:0> Location.all
Location Load (0.2ms) SELECT "locations".* FROM "locations"
=> #<ActiveRecord::Relation [#<Location id: 1, name: "L10", parent_id: nil, activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">, #<Location id: 2, name: "L12", parent_id: "1", activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">]>
irb(main):004:0> Location.last
Location Load (0.2ms) SELECT "locations".* FROM "locations" ORDER BY "locations"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<Location id: 2, name: "L12", parent_id: "1", activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">
升级到 Rails 5 时,我遇到了同样的问题。Fossil 的回答很有帮助,但我还必须做一件事:
- 在主对象的
has_many
关系中添加inverse_of
(本例Activity
模型:has_many :options, inverse_of: "activity"
- 在自引用对象中
has_many
关系中添加inverse_of
(本例Option
模型:has_many :suboptions, inverse_of: "parent"
- 在自引用模型中(这里
Option
),使用before_validation
回调传递主对象:
class Option
belongs_to :parent, foreign_key: "option_id", class_name: "Option"
has_many :suboptions, class_name: "Option", foreign_key: "option_id", inverse_of: "parent"
before_validation :pass_activity_to_suboptions
private
define pass_activity_to_suboptions
suboptions.each { |o| o.activity = self.activity }
end
end
我有一个名为 Option 的模型,它有一个 self-referential 关联。一个选项可以有多个子选项,一个子选项可以有一个 parent 选项。我还使用 cocoon gem 的嵌套属性在 form_for 选项上创建多个子选项。我可以在表单上创建选项时动态创建子选项。
views/options/_form.html.erb:
<%= form_for @option do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<%= f.label :activity %><br>
<%= f.select :activity_id, options_for_select(activity_array, @option.activity_id)%><br>
</p>
<div>
<div id="suboptions">
<%= f.fields_for :suboptions do |suboption| %>
<%= render 'suboption_fields', f: suboption %>
<% end %>
<div class="links">
<%= link_to_add_association 'add suboption', f, :suboptions %>
</div>
</div>
</div>
<p>
<%= f.submit "Send" %>
</p>
<% end %>
model/option.rb:
class Option < ApplicationRecord
belongs_to :activity
has_many :option_students
has_many :students, through: :option_students
has_many :suboptions, class_name: "Option", foreign_key: "option_id"
belongs_to :parent, class_name: "Option", optional: true, foreign_key: "option_id"
accepts_nested_attributes_for :suboptions, allow_destroy: true,
reject_if: ->(attrs) { attrs['name'].blank? }
validates :name, presence: true
after_initialize :set_defaults
before_update :set_defaults
def set_defaults
self.suboptions.each do |sbp|
sbp.activity_id = self.activity_id
end
end
end
参数:
def option_params
params.require(:option).permit(:name, :activity_id, :students_ids => [], suboptions_attributes: [:id, :name, activity_id, :_destroy])
end
我希望每个子选项在创建和更新时都从 parent 继承 activity_id 属性。我通过在模型上使用 set_defaults 方法尝试了这种方式,它适用于具有新嵌套子选项的新选项,如果我更新 activity_id ,它还会更新子选项的 activity_id parent。但是,如果我在更新时创建另一个子选项,它不会将 parent 中的属性传递给新的子选项。
您可以使用before_validation回调。例如,
测试代码
class Location < ApplicationRecord
has_many :children, class_name: 'Location', foreign_key: 'parent_id'
accepts_nested_attributes_for :children
before_validation :initialize_children
attr_accessor :activity
def initialize_children
children.each { |c| c.activity_id = self.activity_id }
end
end
Rails 控制台
irb(main):002:0> Location.create({name: "L10", activity_id: 200, :children_attributes => [{name: "L12"}]})
(0.1ms) begin transaction
SQL (1.1ms) INSERT INTO "locations" ("name", "activity_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "L10"], ["activity_id", 200], ["created_at", 2017-01-11 04:07:26 UTC], ["updated_at", 2017-01-11 04:07:26 UTC]]
SQL (0.1ms) INSERT INTO "locations" ("name", "parent_id", "activity_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["name", "L12"], ["parent_id", "1"], ["activity_id", 200], ["created_at", 2017-01-11 04:07:26 UTC], ["updated_at", 2017-01-11 04:07:26 UTC]]
(3.7ms) commit transaction
=> #<Location id: 1, name: "L10", parent_id: nil, activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">
irb(main):003:0> Location.all
Location Load (0.2ms) SELECT "locations".* FROM "locations"
=> #<ActiveRecord::Relation [#<Location id: 1, name: "L10", parent_id: nil, activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">, #<Location id: 2, name: "L12", parent_id: "1", activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">]>
irb(main):004:0> Location.last
Location Load (0.2ms) SELECT "locations".* FROM "locations" ORDER BY "locations"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<Location id: 2, name: "L12", parent_id: "1", activity_id: 200, created_at: "2017-01-11 04:07:26", updated_at: "2017-01-11 04:07:26">
升级到 Rails 5 时,我遇到了同样的问题。Fossil 的回答很有帮助,但我还必须做一件事:
- 在主对象的
has_many
关系中添加inverse_of
(本例Activity
模型:has_many :options, inverse_of: "activity"
- 在自引用对象中
has_many
关系中添加inverse_of
(本例Option
模型:has_many :suboptions, inverse_of: "parent"
- 在自引用模型中(这里
Option
),使用before_validation
回调传递主对象:
class Option
belongs_to :parent, foreign_key: "option_id", class_name: "Option"
has_many :suboptions, class_name: "Option", foreign_key: "option_id", inverse_of: "parent"
before_validation :pass_activity_to_suboptions
private
define pass_activity_to_suboptions
suboptions.each { |o| o.activity = self.activity }
end
end