Dev
Dev

Reputation: 467

Model associations callbacks

I would like to add a review section to my app. To be more specific, a user can leave a review for a shop and the shop can then reply to that review. But I'm not sure if the model associations and review table migrations I have are correct.

class User < ActiveRecord::Base
  has_many :reviews
end

class Review < ActiveRecord::Base
  belongs_to :user
end

class ReviewReply < ApplicationRecord
  belongs_to :user, optional: true
  belongs_to :review, optional: true
end


class Shop < ActiveRecord::Base
  has_many :reviews
end

class CreateReviews < ActiveRecord::Migration[6.0]
  def change
    create_table :reviews do |t|
      t.text :body
      t.integer :rating
      t.references :shop, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

Upvotes: 0

Views: 125

Answers (3)

Yshmarov
Yshmarov

Reputation: 3729

Architecture:

enter image description here

1. CALCULATING RATING

console:

rails g migration add_rating

migration:

  def change
    add_column :shops, :average_rating, :integer, default: 0, null: false
    add_column :reviews, :rating, :integer, default: 0, null: false
  end

user.rb

has_many :reviews

shop.rb

  has_many :reviews

  def update_rating
    if reviews.any? && reviews.where.not(rating: nil).any?
      update_column :average_rating, reviews.average(:rating).round(2).to_f
    else
      update_column :average_rating, 0
    end
  end

review.rb

  belongs_to :user
  belongs_to :shop
  has_one :review_reply

  after_save do
    unless rating.nil? || rating.zero?
      shop.update_rating
    end
  end

  after_destroy do
    shop.update_rating
  end

review_reply.rb

  belongs_to :review

2. REPLY TO A REVIEW

views/reviews/show.html.erb:

<% unless @review.review_reply.present? %>
  <%= link_to "Write a Reply", new_review_reply_path(review_id: @review.id) %>
<% end %>

views/review_replies/form.html.erb

= f.input :review_id, input_html: {value: params[:review_id]}, as: :hidden

Upvotes: 1

Clemens Kofler
Clemens Kofler

Reputation: 1968

There are a couple of things things that I would change if it's a real application (= not just something you're toying around with):

  • I'd remove the optional: true from the two associations in ReviewReply since replies don't make sense (data-wise) if they don't have an author or aren't connected to a review.
  • I'd also set these two columns in review_replies to null: false.
  • You should think about adding some deletion cascades (either adding dependent: :some_action in models or using on_delete: :some_action on the database columns – I'd recommend the latter):
    • delete review replies when a review is deleted?
    • delete review replies when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)
    • delete reviews when a shop is deleted?
    • delete reviews when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)

Upvotes: 1

Vasfed
Vasfed

Reputation: 18444

In your migration polymorphic relation for reviewable is set up incorrectly - unique index on reviewable_type will prevent adding multiple records, better use

t.references :reviewable, polymorphic: true

in modern rails it adds non-unique index on both columns by default (you can be explicit with index: true, but in any way this is different from two separate indexes on each column alone)

Also most probably you want your unique index to include user id so that each user can review each reviewable once:

t.index [:reviewable_type, :reviewable_id, :user_id], unique: true, name: 'idx_unique_user_review'

Reason for including both shop and reviewable is not clear, but it depends on your application and goals. If the shop itself is the reviewable (as suggested by has_many :reviews, as: :reviewable) - then shop reference is useless. But in fact if you do not plan on extending reviews on something other than shops - it's easier to go with non-polymorphic reference for now.

In large app it's better to have common name prefixes for related things, so ReviewReply model name is better. belongs_to :review is most probably not optional, it's very strange to reply to nothing. Also it will most likely have belongs_to :shop (also not optional) not user, since the shop is the one replying.

Upvotes: 0

Related Questions