2014-03-30 3 views
0

У меня есть две модели: Article и ArticleVote. Когда я уничтожаю голосование за статью (пользователь отменяет свой голос), я хочу, чтобы оценка статьи была изменена. Поэтому я сделал обратный вызов. Вот что моя ArticleVote модель выглядит следующим образом:ActiveRecord изменение связанных атрибутов модели

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 

Failures:

1) ArticleVote voting #destroy should change article score by nevative vote value Failure/Error: expect{ ArticleVote.where(user: user, article: article).first.destroy }.to change{ article.score }.by -1 result should have been changed by -1, but was changed by 0 # ./spec/models/article_vote_spec.rb:32:in `block (4 levels) 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 

не должен Эти два эквивалента? Должны ли мои article и vote.article ссылки на тот же пример ?

+0

Что является результатом первого теста, когда он терпит неудачу? – mralexlau

+0

Обновлено с ошибкой –

ответ

2

В вашем первом тесте вы создаете новый объект статьи в памяти. Rails не проверяет значения атрибутов в db каждый раз, когда вы вызываете article.score, так как это сделает все очень медленным - эти значения хранятся в памяти (это своего рода кеширование результатов). Следовательно, article.score не изменится ни в какой точке. Вам нужно указать рельсы для перезагрузки всех атрибутов из базы данных - используйте 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, когда он был создан, и не будет проверять его до тех пор, пока вы не сообщите ему об этом.

Однако в большинстве случаев нет смысла создавать два дублирующихся объекта в памяти, и лучше всего этого не делать. Это не всегда так очевидно, когда эти obejcts создаются. В случае вашего первого теста проблема заключается в том, что 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 (модель: модель, ...)

+1

Действительно, это так, но мне кажется, что я делаю то, что должно произойти автоматически. Кроме того, не будет ли этот дополнительный запрос дополнительной выборки? Если это кажется очень субоптимальным в производстве: я получаю статью из базы данных, меняя ее состояние, сохраняя ее, а затем снова получаю ее, чтобы загрузить что-то, что я только что сохранил. –

+1

Да, он делает дополнительный запрос sql. Основная причина - не идеальные ассоциации моделей. Для exmaple - более естественным было бы иметь метод 'upvote' для модели' Article', а не класса ArticleVote'. Если хотите, отправьте это на CodeReview, и я постараюсь помочь вам его перепроектировать. – BroiSatse

+0

Сделаем, спасибо за ваш ответ –

Смежные вопросы