我与联接表有一个(相对)简单的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
添加到引用其Thing
的followers
模型中时,此方法才能正常工作。也就是说,如果我这样做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
更新用户。