我与联接表有一个(相对)简单的has_many :through关系:

class User < ActiveRecord::Base
  has_many :user_following_thing_relationships
  has_many :things, :through => :user_following_thing_relationships
end

class Thing < ActiveRecord::Base
  has_many :user_following_thing_relationships
  has_many :followers, :through => :user_following_thing_relationships, :source => :user
end

class UserFollowingThingRelationship < ActiveRecord::Base
  belongs_to :thing
  belongs_to :user
end

这些rspec测试(我知道这些不一定是好的测试,这些只是为了说明正在发生的事情):
describe Thing do
  before(:each) do
    @user = User.create!(:name => "Fred")
    @thing = Thing.create!(:name => "Foo")
    @user.things << @thing
  end

  it "should have created a relationship" do
    UserFollowingThingRelationship.first.user.should == @user
    UserFollowingThingRelationship.first.thing.should == @thing
  end

  it "should have followers" do
    @thing.followers.should == [@user]
  end
end

直到我将after_save添加到引用其Thingfollowers模型中时,此方法才能正常工作。也就是说,如果我这样做
class Thing < ActiveRecord::Base
  after_save :do_stuff
  has_many :user_following_thing_relationships
  has_many :followers, :through => :user_following_thing_relationships, :source => :user

  def do_stuff
    followers.each { |f| puts "I'm followed by #{f.name}" }
  end
end

然后第二次测试失败-即该关系仍添加到联接表中,但是@thing.followers返回一个空数组。而且,回调的那部分永远不会被调用(就像followers在模型中为空)。如果我在puts "HI"行之前的回调中添加followers.each,则“stdout”上会显示“HI”,因此我知道该回调已被调用。如果我注释掉followers.each行,那么测试将再次通过。

如果我全部通过控制台进行操作,则效果很好。即,我可以
>> t = Thing.create!(:name => "Foo")
>> t.followers # []
>> u = User.create!(:name => "Bar")
>> u.things << t
>> t.followers  # [u]
>> t.save    # just to be super duper sure that the callback is triggered
>> t.followers  # still [u]

为什么这在rspec中失败了?我做错什么了吗?

更新

如果我手动将Thing#followers定义为
def followers
  user_following_thing_relationships.all.map{ |r| r.user }
end

这使我相信,也许我用has_many :through定义的:source不正确?

更新

我创建了一个最小的示例项目,并将其放在github上:https://github.com/dantswain/RspecHasMany

另一个更新

感谢@PeterNixey和@kikuchiyo在下面的建议。最终答案是两个答案的结合,我希望我能在两个答案之间分配荣誉。我已经用我认为是最干净的解决方案更新了github项目,并推送了更改:https://github.com/dantswain/RspecHasMany

如果有人可以给我一个真正扎实的解释,我仍然会喜欢它。对我来说最令人困扰的一点是,为什么在最初的问题陈述中,如果我注释掉对followers的引用,那么一切(回调本身的操作除外)都可以正常工作。

最佳答案

过去我遇到过类似的问题,这些问题已通过重新加载关联(而不是父对象)得以解决。

如果在RSpec中重新加载thing.followers,是否可以正常工作?

it "should have followers" do
  @thing.followers.reload
  @thing.followers.should == [@user]
end

编辑

如果(如您所提到的)您遇到了无法触发回调的问题,那么您可以在对象本身中进行以下重新加载:
class Thing < ActiveRecord::Base
  after_save { followers.reload}
  after_save :do_stuff
  ...
end

或者
class Thing < ActiveRecord::Base
  ...
  def do_stuff
    followers.reload
    ...
  end
end

我不知道为什么RSpec遇到不重载关联的问题,但是我自己遇到了相同类型的问题

编辑2

尽管@dantswain确认followers.reload有助于缓解某些问题,但仍无法解决所有问题。

为此,该解决方案需要来自@kikuchiyo的修复程序,该修复程序需要在save中执行回调后调用Thing:
describe Thing do
  before :each do
    ...
    @user.things << @thing
    @thing.run_callbacks(:save)
  end
  ...
end

最终建议

我相信这是由于在<<操作上使用了has_many_through而发生的。我看不到<<实际上应该完全触发您的after_save事件:

您当前的代码是这样的:
describe Thing do
  before(:each) do
    @user = User.create!(:name => "Fred")
    @thing = Thing.create!(:name => "Foo")
    @user.things << @thing
  end
end

class Thing < ActiveRecord::Base
  after_save :do_stuff
  ...

  def do_stuff
   followers.each { |f| puts "I'm followed by #{f.name}" }
  end
end

问题是没有调用do_stuff。我认为这是正确的行为。

让我们看一下RSpec:
describe Thing do
  before(:each) do
    @user = User.create!(:name => "Fred")
    # user is created and saved

    @thing = Thing.create!(:name => "Foo")
    # thing is created and saved

    @user.things << @thing
    # user_thing_relationship is created and saved
    # no call is made to @user.save since nothing is updated on the user
  end
end

问题在于,第三步实际上并不需要重新保存thing对象-只需在联接表中创建一个条目。

如果您想确保@user确实保存了调用,则可能会获得所需的效果,如下所示:
describe Thing do
  before(:each) do
    @thing = Thing.create!(:name => "Foo")
    # thing is created and saved

    @user = User.create!(:name => "Fred")
    # user is created BUT NOT SAVED

    @user.things << @thing
    # user_thing_relationship is created and saved
    # @user.save is also called as part of the addition
  end
end

您可能还会发现after_save回调实际上是在错误的对象上,并且您更希望将其放在关系对象上。最后,如果回调确实确实属于用户,并且您确实需要在创建关系后触发该回调,则可以在创建新关系时使用touch更新用户。

09-27 21:30