System I'm building has Users and Articles. Users can upvote/downvote Articles and cancel their votes if they want to. Each vote changes article's score. Score equals to sum of all vote values together.So I created three models: User, Article, ArticleVote. Even though the most natural way for me would be to be able to do user.upvote(article), for separation of concerns reasons I've put it in ArticleVote model: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)  endprivate  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  endendSo now I call upvote via ArticleVote.upvote(user, article) which doesn't look too bad, but because of Rails' behavior, this doesn't work very good.If I want to cancel an upvote for example:article = Article.find(528953)# article.score = 5 at this point (for example)vote = ArticleVote.where(user: current_user, article: article).first.destroy# article.score = 5 !!!!article.reload# article.score = 4So even though I already fetched an Article from database, and used instance of it to find ArticleVote I have to fetch it again to get the actual score value. I would expect ActiveRecord to automatically set vote.article to reference my article, so when I modify it's state, I wouldn't have to fetch it once again.Is there a way I can avoid this unnecessary article.reload call and still have valid article.score value?(Using Rails version 4)
Article voting schema
ruby;ruby on rails
I would start with creating two subclasses of your votes, using rails STI (you need to add one string 'type' column to your model - You can get rid of value though) and use rails counter_cache feature for both subclasses.class ArticleVote < ActiveRecord::Base  def initialize(*args)    raise 'This class cannot be initialized' if self.class == ArticleVote    super  end  validate :user, presence: trueendclass UpVote < ArticleVote  belong_to :article, counter_cache: trueendclass DownVote < ArticleVote  belongs_to: article, counter_cache: trueendWith this in place, I would create three associations on your Article model, and define score method:class Article < ActiveRecord::Base  has_many :votes, class_name: 'ArticleVotes'  has_many :up_votes  has_many: down_votes  def score    (up_votes_count || 0) - (down_votes_count || 0)    endendYou can create new votes using article.up_votes.create(user_id: user.id) (similar for down_votes).Counter cache will keep track of a number of down and up votes (including destroying existing votes) and this is extremely cheap to subtract those values. Note that you need two extra columns in you article model (up_votes_count and down_votes_count, no score column needed).Now the only thing left is to keep track of user changing their votes. To do this, I would add before_create filter on Vote model:class ArticleVote < ActiveRecord::Base  belongs_to :article  def initialize(*args)    raise 'This class cannot be initialized' if self.class == ArticleVote    super  end  validate :user, presence: true  before_create(prepend: true) do    existing_record = article.article_votes.find_by(user_id: user_id)    return false if existing_record && existing_record.class == self.class    existing_record && existing_record.destroy    true  endendThis filter will check whether given user already voted on given article. If he did and he is trying to vote again, do nothing (return false) otherwise remove previous vote and create new one.There is a potential problem here with condition race. The only way to ensure record uniqueness is to create database constraint like unique index.  Note that, while testing it in the console, counters columns behaves as normal column - it is stored in memory and will not checked whether the value changed in a database or not. Hence you need to reload the model to see the changes:article.score #=> 0article.up_vote.create(user_id: User.first.id)article.score #=> 0         # Counters not updatedarticle.reload.score #=> 1  # But column in a database has been updated