Polymorphic relationships and counter cache - ruby-on-rails

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'

Related

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
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.

Associating two models in a way both models' attributes are always pulled together in any view

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.

Trouble changing to has_many through from has_many and belongs_to

I've done about a billion searches and tried a number of things here but I'm still getting errors. I've recently changed to a has many through via a model called joinable(maybe thats the problem) and I can't seem to get things working straight. Part of me thinks it something small as I get the idea of it all but I'm not sure I've done it correctly. I'm also using devise.
Here is what I think are all the relevant portions
User
class User < ActiveRecord::Base
acts_as_voter
has_many :joinables
has_many :pits, through: :joinables
has_many :comments
enum role: [:user, :vip, :admin]
after_initialize :set_default_role, :if => :new_record?
class Pit < ActiveRecord::Base
validates :topic, :author, :summary, presence: true
acts_as_taggable
acts_as_votable
has_many :comments
has_many :joinables
has_many :users, through: :joinables
mount_uploader :image, ImageUploader
I created a separate table called "joinable" and now I'm stuck figuring out how to populate it. I can create a user, but can't create a pit. Do I need to revamp my controllers or is their something small I may be missing? I get the idea but some of the little details are fuzzy based on all that I've read so far. I even tried a HABTM with a join table called Pit_Users.
I'm currently getting "Could not find table 'joinables"
coming from here in my controller
def create
#pit = current_user.pits.create(pit_params)
recent migration
class Joinable < ActiveRecord::Migration
create_table :joinable do |t|
t.integer :pit_id, :user_id
t.timestamps
end
end
I've tried a number of combinations all with similar errors. Many of the tutorials/guides are good with the basics but then seem to be leaving out a few details. That or I'm just missing them. Anyways. Would love it if someone more knowledgeable could point out what are probably obvious mistakes. Thanks.
In the migration file, it should be:
class Joinables < ActiveRecord::Migration
create_table :joinables do |t|
t.integer :pit_id
t.integer :user_id
end
end
And in the app/models/joinable.rb, there should be:
class Joinable < ActiveRecord::Base
belongs_to :user
belongs_to :pit
end
You can verify if it is working at the Rails console. Try this to get a Pit record with the association:
user_1 = User.create( ... )
pit_1 = user_1.pits.create!( ... )
pit_1.users.first # should give you user as user_1
Solution is to run rails generator for model
Run from console
rails generate model Joinable pit:references user:references
And delete your migration file for
class Joinable < ActiveRecord::Migration
create_table :joinable do |t|
t.integer :pit_id, :user_id
t.timestamps
end
end
After running rails generator you will get model named Joinable that is required for relations when using through and it will create appropriate migration for you.

Storage of different data formats in Rails dynamically defined by admin

I'm facing a problem where I cannot permanently decide which columns one of my models will have.
A use case will be this:
An admin creates a new dataset, he wants users to answer. In the dataset the admin defines several data points of different format and units.
I could imagine the classes to look similar to this:
class Dataset < ActiveRecord::Base
has_many :measurements
has_many :profiles, :through => :measurements
has_many :datapoints, :through => :dataset_datapoint
end
# Join table
class Dataset_datapoint < ActiveRecord::Base
belongs_to :dataset
belongs_to :datapoint
end
class Datapoint < ActiveRecord::Base
has_many :dataset, :through => :dataset_datapoint
has_many :data
# create_table "datapoints" do |t|
# t.string :name
# t.string :format # e.g. string, decimal etc.
# t.string :unit # e.g. CM, pounds etc.
end
class Data < ActiveRecord::Base
belongs_to :datapoint
# create_table "data" do |t|
# t.integer :datapoint_id
# t.string :value # This column could be anything from string to decimal
end
In my head, this seems pretty dynamic, but still quite easy to implement.
What I'm worried about, is how to do the validation on every Data model that is created? Since I cannot hardcode the validation in the model?
And to make it even more complicated, what if some datapoints require extra validations, such as minimum and maximum value?
Thanks in advance,
Jonas
You'll have to enumerate the list of available validations.
Then you can create a validation model and table (and maybe a join table if you want users to be able to reuse their validations - depends on your use cases):
class Validation < ActiveRecord::Base
belongs_to :dataset
# create_table 'validations' do |t|
# t.references :dataset
# t.string :type
# ... and columns for each restriction you could apply, ie:
# t.integer :max_value
# t.integer :min_value
# t.string :regexp
# ...etc...
end
Then, in your data model, add a before_save filter to call your custom validation method:
class Data < ActiveRecord::Base
belongs_to :datapoint
has_many :validations, :through => :datapoint
before_save :custom_validation
private
def custom_validation
validations.each do |validation|
if validation.type == 'integer_range'
unless value < validation.max_value and value > validation.min_value
# return false, or add an error on the value attribute, or whatever
end
# More validations here - use a case statement probably
end
end
end
Not sure if I've got your relationships exactly figured out, but something like that should give you a starting point.

how do I get foreign_key to work in this simple has_many, belongs_to relationship?

I'm pulling data from Harvest. Here are my two models and schema:
# schema
create_table "clients", :force => true do |t|
t.string "name"
t.integer "harvest_id"
end
create_table "projects", :force => true do |t|
t.string "name"
t.integer "client_id"
t.integer "harvest_id"
end
# Client.rb
has_many :projects, :foreign_key => 'client_id' # not needed, I know
# Project.rb
belongs_to :client, :foreign_key => 'harvest_id'
I'm trying to get the Projects to find their client by matching Project.client_id to a Client.harvest_id. Here is what I'm getting instead.
> Project.first.client_id
=> 187259
Project.first.client
=> nil
Client.find(187259).projects
=> []
Is this possible? Thanks!
Might not seem intuitive, but the foreign_key for both relations has to be the same. Let's say you decide to use harvest_id as the foreign key. It should be set up like this:
# Client.rb
has_many :projects, :foreign_key => 'harvest_id'
# Project.rb
belongs_to :client, :foreign_key => 'harvest_id'
You would also only have the harvest_id field in the projects table, since the client has_many projects.
Since your belongs_to relationship in Project model is on harvest_id, you have to ensure the harvest_id attribute is set in the project object.
> Project.first.harvest_id
=> ??
Your problem can occur if the harvest_id is not set.
Projects to find their client by matching Project.client_id to a Client.harvest_id
This does not seem to make sense as client and harvest are supposed to be different objects/records and you cannot match them.
It is like "Find apples where there are Orange-like seeds".
So we probably need more context.
You defined your relations the following way:
On the Project side you say "it is related to client via client_id", but on the Client you say that "it is related to Project via harvest_id"
There you have discrepancy.
So it seems you just have incorrect mappings defined.
Not sure how harvest_id is supposed to be used, so will make the assumption it is just association:
# Client.rb
has_many :projects
belongs_to :harvest
# Project.rb
belongs_to :client
belongs_to :harvest
# Harvest
has_one :client
has_one :project

Resources