using :counter_cache with some conditions [duplicate] - 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

Rails: Adding migration to add an array (default empty)

I'm trying to add a column called share to one of my resources.
The idea is that users can upload documents and share them with other (specific) users, and the array contains the emails of those that the user wants to share with.
I tried adding a migration with the code
class AddShareToDocuments < ActiveRecord::Migration
def change
add_column :documents, :share, :array, :default => []
end
end
But when I open up rails console in the command prompt, it says that share:nil and user.document.share.class is NilClass.
Creating a new array in the rails console sandbox by typing
newarray = []
says that newarray.class is Array.
Can anyone spot what I'm doing wrong?
Rails 4 the PostgreSQL Array data type
In terminal
$ rails generate migration AddTagsToProduct tags:string
Migration file:
class AddTagsToProduct < ActiveRecord::Migration
def change
add_column :products, :tags, :string, array: true, default: []
end
end
https://coderwall.com/p/sud9ja/rails-4-the-postgresql-array-data-type
if you want support all databases you must serialize the array in a String
class Documents < ActiveRecord::Base
serialize :share
end
class AddShareToDocuments < ActiveRecord::Migration
def change
add_column :documents, :share, :string, :default => []
end
end
In case of Postgresql and array datatype I found https://coderwall.com/p/sud9ja
Arrays are not normally a type to be stored in a database. As michelemina points out, you can serialize them into a string and store them, if the type of the data in the array is simple (strings, int, etc). For your case of emails, you could do this.
If, on the other hand, you want to be able to find all of the User objects that a document was shared with, there are better ways of doing this. You will want a "join table". In your case, the join-table object may be called a Share, and have the following attributes:
class Share
belongs_to :user
belongs_to :document
end
Then, in your Document class,
has_many :shares
has_many :users, :through => :shares
As far as generating the migration, this may be hacky, but you could create a new migration that changes the type to "string" (Edit: correct code):
class AddShareToDocuments < ActiveRecord::Migration
def up
change_column :documents, :share, :string
end
def down
change_column :documents, :share, :array, :default => []
end
end

Counter Cache for a column with conditions?

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')")

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