延迟创建Rails关联的最佳模式是什么?



我有一个模型,假设是Cat,我想创建一个新模型Owner。A Cat has_one :owner,但是当我创建Cat模型时,Owner模型还不存在。

不需要为每个Cat回填新的Owner模型,我想有一个简单的方法,所以如果我调用@cat.owner.something, @cat.owner还不存在,它将动态调用@cat.create_owner并返回它。

我已经看到了一些不同的方法,但我想知道解决这个问题的最rails风格的方法是什么,因为我需要经常这样做。

我以前没有见过这样做,但还是决定试一试。

我首先在Cat模型中将关联方法owner别名化,以保留原始方法的备份。如果原始方法返回nil,我将覆盖owner方法以调用build_owner方法(通过关联返回一个新的Owner对象)。否则,返回original_owner_method对象。

class Cat < ActiveRecord::Base
  has_one :owner
  alias :original_owner_method :owner
  def owner
    if original_owner_method.nil? 
      build_owner
    else
      original_owner_method
    end
  end

现在如果你调用:cat = Cat.first

假设它没有所有者,当你调用它时,它将建立一个新的owner对象:cat.owner.name

它将返回nil,但仍然在链的cat.owner部分上构建所有者对象,而不调用method_missing

我将使用回调来创建所有者,而不是在第一次访问它时创建所有者。这确保了所有者永远不会为nil,并且如果回调失败,将自动回滚Cat创建。

class Cat < ActiveRecord::Base
  before_create :create_owner
private
  def create_owner
    return true unless owner.nil?
    create_owner(:default => 'stuff')
  end
end

更新:虽然我仍然推荐上述方法用于新应用程序,但由于您已经有了现有的记录,您可能希望更像这样:

class Cat < ActiveRecord::Base
  def owner
    super || create_owner(:default => 'stuff')
  end
end

对于这类问题,我认为这是最"rubyish"的

def owner
  @owner ||= create_owner
end

在rails中,我这样做的方式是

def owner
  @owner ||= Owner.find_or_create(cat: self)
end

但一般来说,我会尝试找出一种方法来使用Cat#create_owner或Owner#create_cat,并尽量避免整个问题。

根据我的经验,重写来自ActiveRecord::Base的默认属性getter/setter是一种危险的做法-有龙。我将举一个使我出错的例子来解释。

我使用了这个答案中建议的super || create_association模式。你可能会得到一个像这样棘手的bug:

From: /Users/mec/Sites/zipmark/service/spec/models/vendor_application_spec.rb @ line 39 :
    34:         subject.read_attribute(:ledger_id).should be_blank
    35:       end
    36: 
    37:       it "lazily creates the association" do
    38:         subject.ledger
 => 39:         binding.pry
    40:         subject.reload.ledger_id.should be_present
    41:       end
    42:     end
    43:   end
    44: 
[1] pry(#<RSpec::Core::ExampleGroup>)> subject.ledger
#<Ledger:0x007fc3c30ad398> {
   :id => "cf0ac70e-ce23-4648-bf3f-85f56fdb123a",
   :created_at => Wed, 30 Sep 2015 17:56:18 UTC +00:00,
   :updated_at => Wed, 30 Sep 2015 17:56:18 UTC +00:00,
   :description => "Freshbooks Ledger"
}
[2] pry(#<RSpec::Core::ExampleGroup>)> subject.reload.ledger_id
nil

我错误地期望Rails魔术用其新创建的ledger记录更新手头的记录(self)。我最终重写了重载的#ledger方法如下:

def ledger
  super || begin
    ledger = create_ledger(description: "#{name} Ledger")
    update_column(:ledger_id, ledger.id)
    ledger
  end
end

最新更新