Counter Cache for a column with conditions? - ruby-on-rails

I am new to the concept of counter caching and with some astronomical load times on one of my app's main pages, I believe I need to get going on it.
Most of the counter caches I need to implement have certain (simple) conditions attached. For example, here is a common query:
#projects = employee.projects.where("complete = ?", true).count
I am stumbling into the N+1 query problem with the above when I display a form that lists the project counts for every employee the company has.
Approach
I don't really know what I'm doing so please correct me!
# new migration
add_column :employees, :projects_count, :integer, :default => 0, :null => false
# employee.rb
has_many :projects
# project.rb
belongs_to :employee, :counter_cache => true
After migrating... is that all I need to do?
How can I work in the conditions I mentioned so as to minimize load times?

With regards to the conditions with counter_cache, I would read this blog post.
The one thing you should do is add the following to the migration file:
add_column :employees, :projects_count, :integer, :default => 0, :null => false
Employee.reset_column_information
Employee.all.each do |e|
Employee.update_counters e.id, :projects_count => e.projects.length
end
So you current projects count can get migrated to the new projects_count that are associated with each Employee object. After that, you should be good to go.

Check counter_culture gem:
counter_culture :category, column_name: Proc.new {|project| project.complete? ? 'complete_count' : nil }

You should not use "counter_cache" but rather a custom column :
rails g migration AddCompletedProjectsCountToEmployees completed_projects_count:integer
(add , :default => 0 to the add_column line if you want)
rake db:migrate
then use callbacks
class Project < ActiveRecord::Base
belongs_to :employee
after_save :refresh_employee_completed_projects_count
after_destroy :refresh_employee_completed_projects_count
def refresh_employee_completed_projects_count
employee.refresh_completed_projects_count
end
end
class Employee
has_many :projects
def refresh_completed_projects_count
update(completed_projects_count:projects.where(completed:true).size)
end
end
After adding the column, you should initialize in the console or in the migration file (in def up) :
Employee.all.each &:refresh_completed_projects_count
Then in your code, you should call employee.completed_projects_count in order to access it

Instead of update_counters i use update_all
You don't need the Employee.reset_column_information line AND it's faster because you are doing a single database call
Employee.update_all("projects_count = (
SELECT COUNT(projects.id) FROM projects
WHERE projects.employee_id = employees.id AND projects.complete = 't')")

Related

ActiveRecord adding rating range in migration file

class AddRatingToBooks < ActiveRecord::Migration
def up
add_column :books, :rating, :integer
end
def down
remove_column :books, :rating
end
I have the following snippet of code in my db/migrate/, I'm trying to add ratings to my books table, where it would be in a range from 0-100, but I'm not sure how to add that here, all i could find was querying with ranges. I'm sure it's simple I'm just not there yet.
You don't need to specify the range of integer values in your migration file. The migration file is simply used to add the database column to store the rating. This is not the place to add validations.
You should use your Book model to specify a validation that ensures your ratings fall within a certain range. Something like this:
class Book < ActiveRecord::Base
validates :rating, :inclusion => { :in => 0..100 }
end
I would highly recommend reading the Rails guides on both migrations and validations.
Probably I'm too late with the answer. But it's possible to define validation on db level with Migration Validators project: https://github.com/vprokopchuk256/mv-core
As example, in your migration:
def change
change_table :books do |t|
t.integer :rating, inclusion: 0..100
end
end
and then in your model:
class Book < ActiveRecord::Base
enforce_migration_validations
end
As result your validation will be defined both in db ( as statement inside trigger or check constraint, depending on your db) and on your model
SQL ( PostgreSQL ):
=# insert into books(rating) values(10);
INSERT 0 1
=# insert into books(rating) values(200);
ERROR: new row for relation "books" violates check constraint "chk_mv_books_rating"
Rails console:
Book.new(title: 10).valid?
=> true
Book.new(title: 200).valid?
=> false

using :counter_cache with some conditions [duplicate]

I am new to the concept of counter caching and with some astronomical load times on one of my app's main pages, I believe I need to get going on it.
Most of the counter caches I need to implement have certain (simple) conditions attached. For example, here is a common query:
#projects = employee.projects.where("complete = ?", true).count
I am stumbling into the N+1 query problem with the above when I display a form that lists the project counts for every employee the company has.
Approach
I don't really know what I'm doing so please correct me!
# new migration
add_column :employees, :projects_count, :integer, :default => 0, :null => false
# employee.rb
has_many :projects
# project.rb
belongs_to :employee, :counter_cache => true
After migrating... is that all I need to do?
How can I work in the conditions I mentioned so as to minimize load times?
With regards to the conditions with counter_cache, I would read this blog post.
The one thing you should do is add the following to the migration file:
add_column :employees, :projects_count, :integer, :default => 0, :null => false
Employee.reset_column_information
Employee.all.each do |e|
Employee.update_counters e.id, :projects_count => e.projects.length
end
So you current projects count can get migrated to the new projects_count that are associated with each Employee object. After that, you should be good to go.
Check counter_culture gem:
counter_culture :category, column_name: Proc.new {|project| project.complete? ? 'complete_count' : nil }
You should not use "counter_cache" but rather a custom column :
rails g migration AddCompletedProjectsCountToEmployees completed_projects_count:integer
(add , :default => 0 to the add_column line if you want)
rake db:migrate
then use callbacks
class Project < ActiveRecord::Base
belongs_to :employee
after_save :refresh_employee_completed_projects_count
after_destroy :refresh_employee_completed_projects_count
def refresh_employee_completed_projects_count
employee.refresh_completed_projects_count
end
end
class Employee
has_many :projects
def refresh_completed_projects_count
update(completed_projects_count:projects.where(completed:true).size)
end
end
After adding the column, you should initialize in the console or in the migration file (in def up) :
Employee.all.each &:refresh_completed_projects_count
Then in your code, you should call employee.completed_projects_count in order to access it
Instead of update_counters i use update_all
You don't need the Employee.reset_column_information line AND it's faster because you are doing a single database call
Employee.update_all("projects_count = (
SELECT COUNT(projects.id) FROM projects
WHERE projects.employee_id = employees.id AND projects.complete = 't')")

Problem with counter_cache implementation

I'm getting 'rake aborted! ... posts_count is marked readonly' errors.
I have two models: user and post.
users has_many posts.
posts belongs_to :user, :counter_cache => true
I have a migration which adds the posts_count column to the users table and then calculates and records the current number of posts per user.
self.up
add_column :users, :posts_count, :integer, :default => 0
User.reset_column_information
User.all.each do |u|
u.update_attribute( :posts_count, u.posts.count)
end
end
when I run the migration I get the error. This is pretty clear-cut, of course and if I remove the :counter_cache declaration from the posts model, e.g.
belongs_to :user
the migration runs fine. This obviously, does not make sense because you couldn't really implement it this way. What am I missing?
You should be using User.reset_counters to do this. Additionally, I would recommend using find_each instead of each because it will iterate the collection in batches instead of all at once.
self.up
add_column :users, :posts_count, :integer, :default => 0
User.reset_column_information
User.find_each do |u|
User.reset_counters u.id, :posts
end
end
OK, the documentation states:
Counter cache columns are added to the
containing model’s list of read-only
attributes through attr_readonly.
I think this is what happens: you declare the counter in the model's definition, thus rendering the "posts_count" attribute read-only. Then, in the migration, you attempt to update it directly, resulting in the error you mention.
The quick-and-dirty solution is to remove the counter_cache declaration from the model, run the migration (in order to add the required column to the database AND populate it with the current post counts), and then re-add the counter_cache declaration to the model. Should work but is nasty and requires manual intervention during the migration - not a good idea.
I found this blog post which suggests altering the model's list of read-only attributes during the migration, it's a bit oudated but you might want to give it a try.

Ruby on Rails: Seeding data for a user upon signup?

I'm trying to create default seed records for every user that signs up to the app. I'm thinking I could use the after_create method in my users observer model:
def after_create(user)
user.recipes.create(:name => "Sample Recipe", :description => "This is a sample recipe.")
user.cuisines.create(:name => "Sample Cusine", :description => "This is a sample cuisine.")
...
end
Is that too resource-intensive if I have 10 models that need seed data upon signup? Is there a more efficient way?
You're doing this the correct way, and here's why:
As business logic (every user should start with a sample cuisine and recipe) it belongs in the model.
This is where it is most easily testable.
If they have to be created for each user anyway, there's no less "resource intensive" way to do it. Any kind of batch process would leave the user without these defaults for a time.
Personally, I'd probably skip the added abstraction and complexity of putting it in the observer, because I'd want it obvious upon reading through the model that this is happening. But that's personal preference, and there's nothing wrong with how you've set it up here.
Why not set those as the default values in your database? That way there's no extra resources being used code-wise, it is one less point of failure, and you don't need to manually build up the samples in the associations. You can give a default to a column like this:
class AddRecipesDefaults < ActiveRecord::Migration
def self.up
change_column :recipes, :name, :string, :default => "Sample Recipe"
change_column :recipes, :default, :string, :default => "This is a sample."
end
def self.down
change_column :recipes, :name, :string, :default => nil
change_column :recipes, :name, :string, :default => nil
end
end

How do you validate uniqueness of a pair of ids in Ruby on Rails?

Suppose the following DB migration in Ruby:
create_table :question_votes do |t|
t.integer :user_id
t.integer :question_id
t.integer :vote
t.timestamps
end
Suppose further that I wish the rows in the DB contain unique (user_id, question_id) pairs. What is the right dust to put in the model to accomplish that?
validates_uniqueness_of :user_id, :question_id seems to simply make rows unique by user id, and unique by question id, instead of unique by the pair.
validates_uniqueness_of :user_id, :scope => [:question_id]
if you needed to include another column (or more), you can add that to the scope as well. Example:
validates_uniqueness_of :user_id, :scope => [:question_id, :some_third_column]
If using mysql, you can do it in the database using a unique index. It's something like:
add_index :question_votes, [:question_id, :user_id], :unique => true
This is going to raise an exception when you try to save a doubled-up combination of question_id/user_id, so you'll have to experiment and figure out which exception to catch and handle.
The best way is to use both, since rails isn't 100% reliable when uniqueness validation come thru.
You can use:
validates :user_id, uniqueness: { scope: :question_id }
and to be 100% on the safe side, add this validation on your db (MySQL ex)
add_index :question_votes, [:user_id, :question_id], unique: true
and then you can handle in your controller using:
rescue ActiveRecord::RecordNotUnique
So now you are 100% secure that you won't have a duplicated value :)
From RailsGuides. validates works too:
class QuestionVote < ActiveRecord::Base
validates :user_id, :uniqueness => { :scope => :question_id }
end
Except for writing your own validate method, the best you could do with validates_uniqueness_of is this:
validates_uniqueness_of :user_id, :scope => "question_id"
This will check that the user_id is unique within all rows with the same question_id as the record you are attempting to insert.
But that's not what you want.
I believe you're looking for the combination of :user_id and :question_id to be unique across the database.
In that case you need to do two things:
Write your own validate method.
Create a constraint in the database
because there's still a chance that
your app will process two records at
the same time.
When you are creating a new record, that doesn't work because the id of your parent model doesn't exist still at moment of validations.
This should to work for you.
class B < ActiveRecord::Base
has_many :ab
has_many :a, :through => :ab
end
class AB < ActiveRecord::Base
belongs_to :b
belongs_to :a
end
class A < ActiveRecord::Base
has_many :ab
has_many :b, :through => :ab
after_validation :validate_uniqueness_b
private
def validate_uniqueness_b
b_ids = ab.map(&:b_id)
unless b_ids.uniq.length.eql? b_ids.length
errors.add(:db, message: "no repeat b's")
end
end
end
In the above code I get all b_id of collection of parameters, then compare if the length between the unique values and obtained b_id are equals.
If are equals means that there are not repeat b_id.
Note: don't forget to add unique in your database's columns.

Resources