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
Architecture:
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
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?)
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.
Related
I am trying to come up with a query for a many-to-many relationship with filtering for a certain field in the many-to-many table while ordering by a field in an associated table.
How do I get all the active firm_emps of a specific firm and order the firm_emps by user's name?
user.rb
Class User < ApplicationRecord
has_many :firm_emps, :dependent => :destroy
has_many :firms, through: :firm_emps
end
user.rb migration file
...
t.string :name
t.boolean :active
...
firm_emp.rb
Class FirmEmp < ApplicationRecord
belongs_to :firm
belongs_to :user
end
firm_emp.rb's migration file
...
t.belongs_to :user, index: true
t.belongs_to :firm, index: true
t.boolean :admin, default: false
t.boolean :active, default: true
...
firm.rb
Class Firm < ApplicationRecord
has_many :firm_emps, dependent: :destroy
has_many :users, through: :firm_emps
end
Firm.rb's migration file
...
t.string :full_name
t.boolean :active
...
I've tried the following queries in rails console:
f = Firm.first
f.users.where(active: true).order('users.name asc')
# But this filters on User's table field active: true and not the FirmEmps table field active: true
f.users.joins(:firmemps).where(active: true).order('users.name asc')
# Just doesn't work
f.firm_emps.active.order('firm_emps.active')
# But i can't order by user's field 'name'
EDIT:
#PragyaSriharsh's and #ArunEapachen's answers worked.
Try it:
f.users.joins(:firm_emps).where('firm_emps.active=?', true).order('users.name asc')
If it doesn't work use sort_by method.
Try the following.
f = Firm.first
f.users.where('firm_epms.active = ?' , true).order('users.name asc')
I'm building a tech-specific pricegrabber-like web app, and I have a model that carries params that are common in all products. This model is called Product. Then I have one model for each type of product that I'm going to work with, for example, I'm now trying to build the first specific model, which is Videocard. So, the Product model always must have one Specific model, in this case Product-Videocard.
At this moment I'm stuck finding a way to make a product and a specific model always come tied together whenever I reach to them, be it in an index view, show view, form_for, a search, etc. But I can't picture in my head how a form will create an item and its specifications and insert a foreign key into another model with only one submit request.
Below are both models and the migrations for each:
class Product < ApplicationRecord
#belongs_to :productable, :polymorphic => true
has_one :videocard, dependent: :destroy
# Comment for this Stackoverflow question: the way I'm thinking I
# should have to make tons of has_one associations, for the other
# products. Is there a DRY way to do this?
has_many :prices, through: :stores
validates :platform, presence: { message: "should be specified." }
validates :name, presence: { message: "should be specified." }
validates_associated :videocard
end
class Videocard < ApplicationRecord
belongs_to :product
end
Migrations (shortened to make this question as clear as possible):
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.references :productable, polymorphic: true, index: true
t.string :name
t.string :image
t.string :partnum
t.string :manufacturer
t.string :platform #mobile, desktop, server, laptop
t.timestamps
end
end
end
class CreateVideocards < ActiveRecord::Migration[5.0]
def change
create_table :videocards do |t|
t.references :product, index: true
t.integer :memory
t.string :interface
# [...lots of attributes...]
t.integer :displayport
t.integer :minidisplayport
t.integer :tdp
t.timestamps
end
end
end
Also how can I make it so that Product only needs one has_one association, instead of using multiple ones. Remember that Videocard will have one type of specification, Memory will have other, and so on.
I am using rails 4.2, I just want to know if there would be any difference if I use the :foreign_key keyword in my migrations rather than just adding a user_id column to add relationship to my models ?
YES
The key difference is not on the application layer but on the database layer - foreign keys are used to make the database enforce referential integrity.
Lets look at an example:
class User < ActiveRecord::Base
has_many :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
If we declare things.user_id without a foreign key:
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.integer :user_id
t.timestamps null: false
end
end
end
ActiveRecord will happily allow us to orphan rows on the things table:
user = User.create(name: 'Max')
thing = user.things.create
user.destroy
thing.user.name # Boom! - 'undefined method :name for NilClass'
While if we had a foreign key the database would not allow us to destroy user since it leaves an orphaned record.
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.belongs_to :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
user = User.create(name: 'Max')
thing = user.things.create
user.destroy # DB says hell no
While you can simply regulate this with callbacks having the DB enforce referential integrity is usually a good idea.
# using a callback to remove associated records first
class User < ActiveRecord::Base
has_many :things, dependent: :destroy
end
I created a FriendRequest.rb model in my Rails app with the following table columns.
create_table "friend_requests", force: true do |t|
t.integer "requesting_user_id"
t.integer "requested_friend_id"
t.datetime "created_at"
t.datetime "updated_at"
end
With relationships defined as you see below, I added this code to a /views/users/show.html.erb page show the friend requests that have been made for each user. However, I'm getting this error
PG::UndefinedColumn: ERROR: column friend_requests.user_id does not exist
because, I obviously didn't create a user_id column. Is there a way that I can make this code work by adding more information to the relationships? or should I scrap my work and do it differently?
<% for user in #user.friend_requests %>
<li><%= h user.name %></li>
<%= link_to "Add Friend", friend_requests_path(:friend_id => user), :method => :post %>
<% end %>
User.rb
has_many :friend_requests
FriendRequest.rb
belongs_to :user
Just change your has_many association for:
has_many :friend_requests, foreign_key: 'requesting_user_id'
By default, Rails will look for [model_name]_id in the other table, this is why it is looking for user_id, but by adding the foerign_key: option, you can override this default behaviour and tell Rails what is the name of the foreign_key you want to use.
You can use this configuration:
class User < ActiveRecord::Base
has_many :friend_requests
has_many :requesters, through: friend_requests
end
class FriendRequest < ActiveRecord::Base
belongs_to :requester, foreign_key: 'requesting_user_id'
belongs_to :requested, foreign_key: 'requested_friend_id'
validates :requester_id, presence: true
validates :requested_id, presence: true
end
Take a look at:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Look especially for the options :primary_key and :foreign_key
So I have an app with a 2 different models, Comments and Replies, each of which you can either Agree or Disagree, so I have a polymorphic model called Emotion. Here is my code for these:
class Comment < ActiveRecord::Base
belongs_to :user
has_many :replies
has_many :emotions, :as => :emotionable
end
class Reply < ActiveRecord::Base
belongs_to :user
belongs_to :comment
has_many :emotions, :as => :emotionable
end
class Emotion < ActiveRecord::Base
belongs_to :emotionable, :polymorphic => :true
end
So this all works fine, but I'm going to need to add a counter cache for both Comment and Reply in order to get the size of the Agrees and Disagree for each Object. In all of the docs, it has examples for doing counter cache with normal polymorphic associations, not one with an extra condition in it. For reference, by schema for Emotion looks like this:
create_table "emotions", :force => true do |t|
t.integer "user_id"
t.string "emotion"
t.integer "emotionable_id"
t.string "emotionable_type"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
TL:DR - I need to be able to call #commet.agrees_count, #comment.disagrees_count, #reply.agrees_count and #reply.disagrees_count on a polymorphic association through a counter cache. So Comment and Reply will need 2 counter caches.
My suggestion would be to manually increment or decrement your counter cache in an after_commit callback so that you can test if the record was persisted and it updates outside of the transaction. This is because it will make your code more explicit, and less mysterious on how and when the cache is updated or invalidated.
Also manually updating the cache gives you extra flexibility if for example you wanted to give some users more authority when they agree or disagree with a comment (e.g. karma systems).
you may want to add the counter cache attribute to the attr_readonly list in the associated classes (e.g. class Post; attr_readonly :comments_count; end). http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/belongs_to
:polymorphic
Specify this association is a polymorphic association by passing true.
Note: If you’ve enabled the counter cache, then you may
want to add the counter cache attribute to the attr_readonly list in
the associated classes
(e.g. class Post; attr_readonly :comments_count; end).
It's none business of 'Polymorphic relationships and counter cache', it's about Multiple counter_cache in Rails model
By the way, for 'Polymorphic relationships and counter cache'
class Code < ActiveRecord::Base
has_many :notes, :as => :noteable
end
class Issue < ActiveRecord::Base
has_many :notes, :as => :noteable
end
class Note < ActiveRecord::Base
belongs_to :noteable, polymorphic: true, counter_cache: :noteable_count
end
in your table 'issues', you should have the column 'noteable_count', same as your table 'codes'