Take the following for example:
class Foo < AR::Base
has_many :bars, :as => :barable, :dependent=> :destroy
accepts_nested_attributes_for :bars, :allow_destroy => true
end
class Bar < AR::Base
belongs_to :barable, :polymorphic => true
end
class Baz < Bar
before_save do
raise "Hi"
end
end
In the form for 'Foo' - I have fields_for :bars_attributes where a hidden field sets type to 'Baz'. The 'Baz' is succesfully created but the callback never fires. (It does, however, fire when manually creating a 'Baz' in the console.)
Any advice appreciated!
Baz's callbacks will only be triggered if you create it as a Baz object, i.e Baz.new(...).
However, you're not creating a Baz record, but rather a Bar record: Bar.new(type: 'Baz').
This will only trigger Bar's callbacks, even though that later on it will be treated as a Baz.
you need to specify additonal association in your Foo.rb
has_many :bazs
# or
# has_many :bazs class_name: 'ModuleName::Baz' # if you scoped your child classed within some module
If you do that your
before_save do
raise "Hi"
end
will fire on for example #current_user.bazs.build
Related
How can I create a parent model only if its children models fit some validations alltogether like the sum of their attributes equal come value, etc?
I have an parent model as:
class Foo < ApplicationRecord
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars, allow_destroy: true
end
And the child model as:
class Bar < ApplicationRecord
belongs_to :foo
end
Is there a right way of doing it? Where should I validate the children models? Also how could I test it with rspec? Like this?
before do
#foo = create(:foo)
#bar = create(:bar, value: 30, foo_id: #foo.id)
end
Rails offers validates_associated, which will ensure the associated records are valid. If the associated records are invalid, the parent record will not be saved.
In Foo:
class Foo < ApplicationRecord
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars, allow_destroy: true
validates_associated :bars
end
In Bar:
class Bar < ApplicationRecord
belongs_to :foo
include ActiveModel::Validations
validates_with BarValidator
end
In BarValidator, which is a custom validator:
class BarValidator < ActiveModel::Validator
def validate(record)
record.errors.add :some_error, 'Some message' unless condition_met?
end
end
You've stated:
only if its children models fit some validations alltogether like the sum of their attributes equal come value, etc?
which is somewhat ambiguous. If you truly do need to calculate the sum of children then you can add a validator to the parent which maps through the children and appends an error on failure to meet conditional:
In Foo, (or preferably, a validator):
validate :children_condition
def children_condition
errors[:base] << "Some message" if bars.map(&:attribute).sum != expected_minimum_value
end
Important notes from the documentation on validates_associated:
WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion.
NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association is both present and guaranteed to be valid, you also need to use validates_presence_of.
As for:
Also how could I test it with rspec?
I would create a test suite of valid parent, valid child && valid parent, invalid child, etc and expect the Model.count to have increased by the expected amount (which would be zero in the latter example).
class MyTask < ApplicationRecord
has_many :jobs, as: :ownerable, dependent: :destroy
accepts_nested_attributes_for :jobs, allow_destroy: true
before_save :set_some_data
end
class Job < ApplicationRecord
belongs_to :ownerable, polymorphic: true, optional: true
end
In the :set_some_data method, we actually take the values from all the jobs belonging to the MyTask object and perform some calculations and save the result in a column (actually, just a self.column_name = calculated_value, not actually calling save).
The problem is that the UPDATE on the column happens before any jobs marked for destruction ie with "_destroy" => 1 in the params. And so obviously, it includes data from the deleted jobs, which is incorrect.
I am currently doing the following - change callback to:
after_save :set_some_data
def set_some_data
#Do stuff
# WARNING: Don't use any method that will trigger an after_save callback. Infinite loop otherwise.
self.update_columns(column_name: calculated_value)
end
It does what I want. But is this a good solution? Can you suggest some better alternatives?
you can do this with with after_destroy and put the method in job.rb this will make sure when child deleted(job) it will call the parent to update the value
class Job < ApplicationRecord
belongs_to :ownerable, polymorphic: true, optional: true
after_destroy :update_parent
def update_parent
# check your parent model
self.ownerable.update_columns(column_name: calculated_value)
end
end
for more detail callback you can check this and
As I understand Ruby inheritance and method lookup, when a child instance calls a parent's instance method, which in turn calls a method that's named in both parent and child, the scope is still at the child instance. So this will happen:
class Foo
def method1
"foo"
end
def method2
puts method1
end
end
class Bar < Foo
def method1
"bar"
end
end
Bar.new.method2
=> "bar"
However, when I do what I think is a similar thing with ActiveRecord associations, I don't get what I'd expect:
class Foo < ApplicationRecord
has_many :orders
has_many :order_items, through: :orders
end
class Bar < Foo
has_many :orders, -> { where(attribute1: 1) }
end
When I call bar.orders I get what I expect. But when I call bar.order_items I get the same result as if I had called foo.order_items (the query scope is not used). If I include has_many :orders_items, through: :orders in bar.rb it behaves as I expect. Why do ApplicationRecords behave this way? Am I comparing apples to oranges?
Like Max states in the comment, you're not defining methods, you're calling them, when you'r using the meta programming, so there is no inheritance involved.
We can use ActiveRelation like this:
MyModel.where(:field => "test").create => #<Message ... field:"test">
But it doesnt work for joins with polymorphic has_one associations:
class RelatedModel < AR::Base
# has :some_field
belongs_to :subject, :polymorphic => true
end
class MyModel < AR::Base
# need some dirty magic here
# to build default related_model with params from active_relation
has_one :related_model, :as => :subject, :dependent => :destroy
end
describe MyModel do
it "should auto-create has_one association with joins" do
test = MyModel.joins(:related_model).where("related_models.subject_type" => "MyModel", "related_models.some_field" => "chachacha").create
test.related_model.should_not be_nil
test.related_model.some_field.should == "chachacha"
test.related_model.subject_type.should == "MyModel"
test.related_model.subject_id.should == test.id
# fails =)
end
end
Is it possible to extract active_relation params, pass them to MyModel for use in before_create and build RelatedModel with them?
Diving into ActiveRecord sources i found that
ActiveRecord::Relation covers 'create' with 'scoping' method.
ActiveRecord::Persistance 'create' calls 'initialize' from ActiveRecord::Core.
ActiveRecord::Core 'initialize' calls 'populate_with_current_scope_attributes'
This method declared in ActiveRecord::Scoping uses 'scope_attributes' declared in ActiveRecord::Scoping::Named.
scope_attributes creating relation 'all' and calls 'scope_for_create' on it.
'ActiveRecord::Relation's 'scope_for_create' uses only 'where_values_hash' from current_scope that does not contain rules like 'related_models.subject_type' (this values are contained in where_clauses). So we need to have simple key-value wheres to be used with 'create' on ActiveRecord::Relation. But ActiveRecord not clever enough to know that 'some_field' in where clause should be used with join table.
I found it can be implemented only by accessing where options with self.class.current_scope.where_clauses in 'before_create' on MyModel, parsing them and setting up attributes.
class MyModel < AR::Base
before_create :create_default_node
def create_default_node
clause = self.class.current_scope.where_clauses.detect{|clause| clause =~ /\`related_models\`.\`some_field\`/}
value = clause.scan(/\=.+\`([[:word:]]+)\`/).flatten.first
self.create_node(:some_field => value)
end
end
But it is so dirty, then i decided to find simpler solution and inverted dependency as described in Railscast Pro #394, moved RelatedModel functionality to MyModel with STI. Actually i needed such complicated relation creation because RelatedModel had some functionality common for all models (acts as tree). I decided to delegate 'ancestors' and 'children' to RelatedModel. Inverting dependency solved this problem.
class MyModel < AR::Base
acts_as_tree
belongs_to :subject, :polymorphic => true
end
class MyModel2 < MyModel
end
class RelatedModel < AR::Base
# has :some_field
has_one :my_model, :as => :subject, :dependent => :destroy
end
MyModel.create{|m| m.subject = RelatedModel.create(:some_field => "chachacha")}
MyModel.ancestors # no need to proxy relations
I'm having a frustrating problem with a has_many through: namely the fact that the through models are not created until save. Unfortunately, I need to set data on these models prior to saving the parent.
Here's the loose setup:
class Wtf < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
class Bar < ActiveRecord::Base
has_many :wtfs
has_many :foos, :through => :wtfs
end
class Foo < ActiveRecord::Base
has_many :wtfs
has_many :bars, :through => :wtfs
def after_initialize
Bar.all.each do |bar|
bars << bar
end
end
end
Everything is fine except that I need to access the "wtf"'s prior to save:
f = Foo.new
=> #
f.bars
=> [list of bars]
empty list here
f.wtfs
=> []
f.save!
=> true
now I get stuff
f.wtfs
=> [list of stuff]
I even went so far as to explicitly create the wtfs doing this:
def after_initialize
Bar.all.each do |bar|
wtfs << Wtf.new( :foo => self, :bar => bar, :data_i_need_to_set => 10)
end
end
This causes the f.wtfs to be populated, but not the bars. When I save and retrieve, I get double the expected wtfs.
Anyone have any ideas?
I think you have the right idea with creating the Wtfs directly. I think it will turn out OK if you just set the bars at the same time:
def after_initialize
Bar.all.each do |bar|
wtfs << Wtf.new(:bar => bar, :data_i_need_to_set => 10) # Rails should auto-assign :foo => self
bars << bar
end
end
Rails should save the records correctly because they are the same collection of objects. The only drag might be that if rails doesn't have the smarts to check if a new Bar record in the bars collection already has a Wtf associated, it might create one anyway. Try it out.
Couldn't you write a before_save handler on Wtf that would set the data you need to set? It would have access to both the foo and bar, if needed.
You could set the method that populates bar to an after_create, like this:
class Foo < ActiveRecord::Base
has_many :wtfs
has_many :bars, :through => :wtfs
after_create :associate_bars
def associate_bars
Bar.all.each do |bar|
bars << bar
end
end
end
This would make the wtfs be already be created when this method is called.