How do I count within nested resourced and display the results? - ruby-on-rails

I am building a job board in rails based on PostgreSQL. I want to count and display the amount of job offers per employer on the index of the employer page. What is the code for this kind of count?
I created a nested resource, and associated my employer and offer model, by:
class Employer < ActiveRecord::Base
has_many :offers, dependent: :delete_all
end

You should use counter_cache i.e. adding an extra column(offer_count) in employer table & update the counter while making entry in offer table. For more details , check counter_cache in http://guides.rubyonrails.org/association_basics.html
Your migration should like
def change
add_column :employers, :offers_count, :integer, default: 0
Employer.reset_column_information
Employer.all.each do |p|
Employer.update_counters p.id, :offers_count => p.offers.length
end
end

Related

How to fetch an associated model's last record to prevent N+1 queries?

I have a model Invoice which has_many Payments and a model Payment that belongs_to Invoice.
We export Invoice data monthly in batches, and we need each Invoice's last Payment.
In our view we are currently doing Invoice.payments.last once for each Invoice that we are exporting, and I was asked to prevent N+1 queries.
I don't understand if I should add this query in the controller or in the Invoice model, or if it should be a has_one :last_payment association or a scope.
Any help would be appreciated.
If the number of payments per invoice is relatively small you can just include/eager_load/preload the association:
invoices = Invoice.includes(:payments)
invoices.each do |i|
puts i.payments.last.amount # no n+1 query
end
However this will load all the associated records into memory at once. This can cause performance problems.
One a very performant read optimization would be to add foreign key column to the invoices table and a belongs_to association which you can use when eager loading:
class AddLatestPaymentToInvoices < ActiveRecord::Migration[6.0]
def change
add_reference :invoices, :latest_payment, null: false, foreign_key: { to_table: :payments }
end
end
class Invoice < ApplicationRecord
has_many :payments, after_add: :set_latest_invoice!
belongs_to :latest_payment,
class_name: 'Payment'
private
def set_latest_payment(payment)
update_columns(latest_payment_id: payment.id)
end
end
invoices = Invoice.includes(:latest_payment)
invoices.each do |i|
puts i.latest_payment.amount # no n+1 query
end
The cost is an additional UPDATE query per record inserted. It can be optimized by using a DB trigger instead of an association callback.

Finding the most in a has_many relationship with an extra condition

I have a Users model that has many Polls. The Polls can be the same amongst users. The Poll model has many votes. The User Model also has many votes. The Vote model has User ID, Poll ID, and option chosen. I want to, for a given user, find the poll with the most votes.
I was looking counter_cache, but I'm not sure how to apply it to this problem. Is there a better way to solve this than iterating through each poll a user has and seeing the largest count where used_id = user_Id?
Yes, you could do this with the counter_cache. On the polls table you'd have a column votes_count. Your migration could look like this:
add_column :polls, :votes_count, :integer, default: 0
and the approach for finding the record would be:
class User
has_many :polls
has_many :votes
end
class Poll
has_many :votes
end
class Vote
belongs_to :user
belongs_to :poll, counter_cache :true
##attr :option_chosen
end
If you had some records before adding the table you need to reset the counters.
Now, to fetch the poll with the most votes, you could easily do:
User.first.polls.order(votes_count: :desc).first
Hope that helps.

Rails Converting a has_many relationship into a has and belongs to many

I have a Rails app with the following relationship:
region.rb
class Region < ActiveRecord::Base
has_many :facilities
end
facility.rb
class Facility < ActiveRecord::Base
belongs_to :region
end
I want to expand functionality a bit so that facilities can belong to more than one region at a time. I believe I can do this with a has_many_through relationship but I'm needing some guidance on converting the existing has_many into a has many through. I understand how to create and wire up the join table, but how would I take existing data and translate it?
So for instance. On a facility object there is region_id, since the facilities can belong to more than one region I'd probably need a region_ids field and shovel the collection of regions into that column which should then populate the other side of the association via the join table. I have this part pretty much figured out as far as moving forward and wiring up the association. But I'm unsure as to how to take existing data and translate it over so the app doesn't break when I change the model association.
Any advice would be greatly appreciated.
I suggest you to always use has_many :through instead of HBTM.
To establish this kind of relation you'll need the following set up:
# region.rb
class Region
has_many :facility_regions
has_many :facilities, through: :facility_regions
end
# facility.rb
class Facility
has_many :facility_regions
has_many :regions, through: :facility_regions
end
# facility_region.rb
class FacilityRegion
belongs_to :facility
belongs_to :region
end
Also, of course, you'll need to create a migration:
rails g migration create_facility_regions facility_id:integer region_id:integer
# in this migration create a uniq index:
add_index :facility_regions, %I(facility_id region_id), name: :facility_region
rake db:migrate
UPD
As to migration from one database state to another one.
I think it should not be a problem.
1) Do not delete the relations you had before (leave has_many :facilities and belongs_to :region in models).
2) When new table is created and new associations added to the classes (which I showed) create a new migration:
rails g migration migrate_database_state
3) Write the script, which will create new records in db (to reflect the current state of things):
ActiveRecord::Base.transaction do
Facility.where.not(region_id: nil).find_each do |facility|
next if FacilityRegion.find_by(falicity_id: facility.id, region_id: facility.region_id)
FacilityRegion.create!(facility_id: facility.id, region_id: facility.region_id)
end
end
4) Put this script into last created migration and run it (or in console without migration, effect would be the same).
5) After script is successfully run, create new migration in which you delete region_id from facilities table and remove these associations definitions (has_many :facilities and belongs_to :region) from models.
It must be it. I might have made some typos or so, make sure I did not miss anything and
You need to add another model, a "middle guy" called FacilityRegion.rb, like this:
facility.rb
class Facility < ActiveRecord::Base
has_many :falicity_regions
has_many :regions, through: falicity_regions
end
facility_region.rb
class FacilityRegion < ActiveRecord::Base
belongs_to :region
belongs_to :facility
end
region.rb
class Region < ActiveRecord::Base
has_many :falicity_regions
has_many :facilities, through: falicity_regions
end
If you want to use belongs_and_has_many relationship, you need to:
rails g migration CreateJoinTableRegionsFacilities regions facilities
Then,
rake db:migrate
Now, your relationships should be:
Region.rb:
class Region < ApplicationRecord
has_and_belongs_to_many :facilities
end
Facility.rb
class Facility < ApplicationRecord
has_and_belongs_to_many :regions
end
In order to populate the new join table, you will need to in your console:
Region.all.find_each do |r|
Facility.where(region_id: r.id).find_each do |f|
r.facilities << f
end
end
Now, you can either leave the columns region_id and facility_id in Facility and Region table, respectively, or you can create a migration to delete it.

rails 4 HABTM relation and extra fields on join table

What I have (pseudo code):
model Document
column :title
HABTM :users
model User
column :name
HABTM :documents
Document has users (being approvers for document, either approve or not), and in this context join table should have extra column approved for each user.
jointable
user_id, document_id, approved
1 , 1 , true
2 , 1 , false
What I want is basically:
contract.approvers => returns users but with possibility to =>
contract.approvers.first.approve(:true) => and it updates JOINtable approve column to TRUE.
Answer right for this situation is optional, will appreciate advises on schema too (or maybe i should use other type of relation?).
HABTM has been deprecated a while ago, I think it is just a reference to has many through now.
Either way
join table name = DocumentReview
Document
has_many :document_reviews
has_many :users, through: :document_reviews
User
has_many :document_reviews
has_many :documents, through: :document_reviews
I don't understand how contract fits into this, i think you are saying that a document is a contract?
I would put the approve method in a separate class
class DocumentSignOff
def initialize(user, document)
#document_review = DocumentReview.find_by(user: user,document: document)
end
def approve!
#maybe more logic and such
#document_review.udpate(approved: true)
end
end
end

Create if record does not exist

I have 3 models in my rails app
class Contact < ActiveRecord::Base
belongs_to :survey, counter_cache: :contact_count
belongs_to :voter
has_many :contact_attempts
end
class Survey < ActiveRecord::Base
has_many :questions
has_many :contacts
end
class Voter < ActiveRecord::Base
has_many :contacts
end
the Contact consists of the voter_id and a survey_id. The Logic of my app is that a there can only be one contact for a voter in any given survey.
right now I am using the following code to enforce this logic. I query the contacts table for records matching the given voter_id and survey_id. if does not exist then it is created. otherwise it does nothing.
if !Contact.exists?(:survey_id => survey, :voter_id => voter)
c = Contact.new
c.survey_id = survey
c.voter_id = voter
c.save
end
Obviously this requires a select and a insert query to create 1 potential contact. When I am adding potentially thousands of contacts at once.
Right now I'm using Resque to allow this run in the background and away from the ui thread. What can I do to speed this up, and make it more efficient?
You can do the following:
Contact.where(survey_id: survey,voter_id: voter).first_or_create
You should add first a database index to force this condition at the lowest level as possible:
add_index :contacts, [:voter_id, :survey_id], unique: true
Then you should add an uniqueness validation at an ActiveRecord level:
validates_uniqueness_of :voter_id, scope: [:survey_id]
Then contact.save will return false if a contact exists for a specified voter and survey.
UPDATE: If you create the index, then the uniqueness validation will run pretty fast.
See if those links can help you.
Those links are for rails 4.0.2, but you can change in the api docks
From the apidock: first_or_create, find_or_create_by
From the Rails Guide: find-or-create-by
It would be better if you let MySQL to handle it.
Create a migration and add a composite unique key to survey_id, voter_id
add_index :contact, [:survey_id, :voter_id], :unique=> true
Now
Contact.create(:survey_id=>survey, :voter_id=>voter_id)
Will create new record only if there is no duplicates.

Resources