Вопрос

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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top