ActiveRecord更改相关的模型属性

时间:2014-03-30 16:22:40

标签: ruby-on-rails ruby activerecord factory-bot rspec2

我有两个模型ArticleArticleVote。当我销毁一篇文章投票(用户取消他的投票)时,我想要改变文章的分数。所以我做了一个回调。这是我的ArticleVote模型的样子:

class ArticleVote < ActiveRecord::Base
  belongs_to :article
  belongs_to :user

  before_destroy :before_destroy

  validates :value, inclusion: {in: [1, -1]}

  def self.upvote(user, article)
    cast_vote(user, article, 1)
  end

  def self.downvote(user, article)
    cast_vote(user, article, -1)
  end

private

  def self.cast_vote(user, article, value)
    vote = ArticleVote.where(user_id: user.id, article_id: article.id).first_or_initialize
    vote.value = value
    vote.save!
    article.score += value
    article.save!
  end

  def before_destroy
    article.score -= value
    article.save
  end
end

我的ArticleVote#destroy测试失败:

context '#destroy' do
  let(:user) { FactoryGirl.create(:user) }
  let(:article) { FactoryGirl.create(:article) }

  it 'changes article score by negative vote value' do
    ArticleVote.upvote(user, article)

    expect{ ArticleVote.where(user: user, article: article).first.destroy }.to change{ article.score }.by -1
  end
end
  

故障:

     

1)ArticleVote投票#destroy应该通过创造性的投票价值来改变文章得分        失败/错误:期待{ArticleVote.where(user:user,article:article).first.destroy} .to change {article.score} .by -1          结果应该已经被-1更改,但被更改为0        './spec/models/article_vote_spec.rb:32:in'块(4级)in'

当我将测试更改为此时,它会通过:

context '#destroy' do
  let(:user) { FactoryGirl.create(:user) }
  let(:article) { FactoryGirl.create(:article) }

  it 'changes article score by nevative vote value' do
    ArticleVote.upvote(user, article)
    vote = ArticleVote.where(user: user, article: article).first

    expect{ vote.destroy }.to change{ vote.article.score }.by -1
  end
end

这两个不应该相同吗?我的articlevote.article不应该引用相同的实例吗?

1 个答案:

答案 0 :(得分:2)

在第一次测试中,您将在内存中创建新的Article对象。每次调用article.score时,Rails都不会检查db中的属性值,因为它会使一切都非常慢 - 这些值存储在内存中(它有点缓存结果)。因此article.score在任何时候都不会改变。您需要告诉rails重新加载数据库中的所有属性 - 在article.reload.score块中使用change

补充说明:

我们说过:

model_1 = Model.where(<condition>).first
model_2 = Model.where(<some condition>).first

model_1和model_2都是从数据库中的某一行创建的,但它们是内存中的不同对象。因此当你这样做时:

model_1.some_attribute = 'new value'
model_1.save

model_2.some_attribute #=> 'old_value'

原因是性能--Rails不会检查数据库中是否给定属性在数据库中已更改。 model_2在创建时执行了sql查询,并且在您告诉它之前不会重新检查。

但是在大​​多数情况下,在内存中创建两个重复对象没有意义,最好不要这样做。在创造这些对象的地方并不总是那么明显。如果是第一次测试,问题是ArticleVote.where(user: user, article: article).first.article与原始article对象重复,因此您的before_save回调遵循与model_1, model_2示例相同的模式。

避免此类问题的最佳方法是正确使用关联,包括inverse_of选项并使用model.associations.where(...)代替AssocatedClass.where(model: model, ...)model.association.create(...)代替'AssociationClass .create(model:model,...)