How to rewrite Rails associations without separate queries (looping through them again) - sql

Running into some performance issues with the following code (stripped out irrelevant parts).
This is the CardsController#index code:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
cards.each do |card|
if current_user
card.bumped = card.bump_by?(current_user)
card.bump = card.get_bump(current_user)
else
card.bumped = false
card.bump = nil
end
end
end
Card.rb:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
self.card_bumpers.include? user
end
def get_bump(user)
CardBump.find_by(user_id: user.id, card_id: self.id)
end
end
How can I avoid and optimize the second loop on each card where I do the associations of card.bumped and card.bump ?
Thanks in advance

In the model level, since the method bump_by? equals to bump existence, so
card.bumped = !card.bump.empty?
So the whole includes check in method bump_by? can be avoided, which in turn avoid fetching all associated bumps.

First of all, I would optimize your controller code a little and would update all cards with a single query if current_user is not present:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
if current_user
cards.each do |card|
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)
end
else
cards.update_all(bump: nil, bumped: false)
end
end
There is also a possibility to optimize model code:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
# use #exists? to check whether a record is present in the database.
# This will make a `SELECT 1 as count...` query and,
# therefore, perform a lookup on database level.
# #include? in opposite will load ALL associated items from DB,
# turn them into a ruby objects array and perform a lookup in the
# obtained array which is much slower than simple lookup performed by #exists?
get_bump(user) == user || self.card_bumpers.exists?(user.id)
end
def get_bump(user)
#_bump ||= self.card_bumpers.find_by(user_id: user.id)
end
end
Since Card#get_bump is also looking in card_bumpers association we can memorize its result and later use memorized value in Card#bump_by? without hitting database again. If there is no memorized value then fast check for record existence will be performed by a database.
Notice, that I changed lines order in controller to get benefit of memorizing:
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)

Related

Find cards with today date when user receive mails in RoR

Every day I need to send letters to users with today's tasks.
For do this I need to find all users who are allowed to send letters, and among these users to find all cards that have a deadline today. The result is three array elements with a nil value. How is this better done and right?
#users = User.all {|a| a.receive_emails true}
#user_cards = []
#users.each_with_index do |user, index|
#user_cards[index] = user.cards.where(start_date: Date.today).find_each do |card|
#user_cards[index] = card
end
end
My user model:
class Card < ApplicationRecord
belongs_to :user
# also has t.date "start_date"
end
My card model:
class User < ApplicationRecord
has_many :cards, dependent: :destroy
# also has t.boolean "receive_emails", default: false
end
Something like #cards_to_send = Card.joins(:users).where("users.receive_emails = true").where(start_date: Date.today)
Have a look at https://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-the-joined-tables for the docs on how to query on a joined table.
You could do this with a SQL join like this
User.joins(:cards).where(receive_emails: true, cards: { start_date: Date.today })
https://guides.rubyonrails.org/active_record_querying.html#joining-tables

Rails: Setting Model Attributes to Attributes from Another Model

I am a little unsure of how to ask this so I apologize for the clunky explanation.
I have three models, User, Waterusage and Goals
class Goal < ApplicationRecord
belongs_to :user
end
class Waterusage < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable,
has_one :waterusage, :dependent => :destroy
has_one :goals, :dependent => :destroy
end
Waterusage is filled out first by users and then goals. Goals is the exactly same schema as waterusage, but uses a portion of the waterusage form and copies the remaining attributes from waterusage.
class Goal < ApplicationRecord
belongs_to :user
# before_validation :get_from_waterusage
before_validation :calculate_totals
def get_from_waterusage
self.household_size = #waterusage.household_size
self.swimming_pool = #waterusage.swimming_pool
self.bathroom_sink_flow_rate = #waterusage.bathroom_sink_flow_rate
self.low_flow_toilet = #waterusage.low_flow_toilet
self.kitchen_sink_usage = #waterusage.kitchen_sink_usage
self.kitchen_sink_flow_rate = #waterusage.kitchen_sink_flow_rate
self.dishwasher_rate = #waterusage.dishwasher_rate
self.dishwasher_multiplier = #waterusage.dishwasher_multiplier
self.laundry_rate = #waterusage.laundry_rate
self.laundry_multiplier = #waterusage.laundry_multiplier
self.lawn_size = #waterusage.lawn_size
self.carwash_rate = #waterusage.carwash_rate
self.carwash_multiplier = #waterusage.carwash_multiplier
self.miles = #waterusage.miles
self.statewater = #waterusage.statewater
self.percent_statewater = #waterusage.percent_statewater
self.pet_cost = #waterusage.pet_cost
end
...
end
Here is the GoalsController
class GoalsController < ApplicationController
before_action :authenticate_user!
def new
#goal = goal.new
end
def create
##user = User.find(params[:user_id])
#goal = current_user.create_goal(goal_params)
redirect_to goal_result_path
end
def destroy
#user = User.find(params[:user_id])
#goal = #user.goal.find(params[:id])
#goal.destroy
redirect_to user_path(current_user)
end
def show
#goal = goal.find(params[:id])
end
def results
if current_user.goal.get_individual_total > 6000
#temp = 6000
else
#temp = current_user.goal.get_individual_total
end
#goal = current_user.goal
end
private
def goal_params
params.require(:goal).permit(:household_size, :average_shower,
:shower_flow_rate, :bath_rate, :bath_multiplier,
:bathroom_sink_usage,
:bathroom_sink_flow_rate, :mellow, :low_flow_toilet,
:kitchen_sink_usage,
:kitchen_sink_flow_rate, :dishwasher_rate,
:dishwasher_multiplier,
:dishwasher_method, :laundry_rate, :laundry_multiplier,
:laundry_method,
:greywater, :lawn_rate, :lawn_multiplier, :lawn_size,
:xeriscaping,
:swimming_pool, :swimming_months, :carwash_rate,
:carwash_multiplier,
:carwash_method, :miles, :statewater, :percent_statewater,
:shopping,
:paper_recycling, :plastic_recycling, :can_recycling,
:textile_recycling,
:diet, :pet_cost, :individual_total, :household_total,
:home_usage, :outdoor_usage,
:individualDifference, :householdDifference, :vehicle_usage,
:power_usage, :indirect_source_usage,
:individualDifference, :householdDifference)
end
end
I currently have the following error:
NameError in GoalsController#create
undefined local variable or method `current_user' for #
<Goal:0x007fbedde9a590>
It seems to be in the way I am retrieving the info from the waterusage model with
self.household_size = #waterusage.household_size
It there a join I could use?
The waterusage model works BTW.
Thanks
Don't know if it's the best way to do that, but I would use something like this:
In your goals model, you can check if its user have a waterusage already. If it has, you fill the values from that water usage
You can do it using after_initialize callback. In your goal model, would be something like
class Goal < ApplicationRecord
belongs_to :user
after_initialize :set_default_values
def set_default_values
waterusage = self.user.try(:waterusage)
if waterusage
self.attribute1 = waterusage.attribute1
self.attribute2 = waterusage.attribute2
self.attribute3 = waterusage.attribute3
#and it goes on...
end
end
end
so, like this when you do a Goal.new, it will check for a waterusage for that user and initialize those values on your goal. So you don't have to change anything on your controller and even if you do it on console, it will work. Guess it's a better practice to do that using models callbacks. Don't know if it solves your problem, but give it a try. Good luck!
Your error message is:
NameError in GoalsController#create
undefined local variable or methodcurrent_user' for #
Goal:0x007fbedde9a590`
The current_user object is automagically defined inside your controller by the Devise gem you're using. It will not be defined inside your models.
One of your comments includes the following snippet you say you're using from within your Goal model: current_user.waterusage.household_size. That is what your error message is referring to. (Note that this snippet from one of your comments disagrees with the code in your original post. This makes it harder to be certain about what is going wrong here.)

Rails ActiveRecord querying

So I have the following three models: Assignment.rb, Submission.rb, User.rb
And here are the relationships:
class Assignment
has_many :submissions
end
class Submission
belongs_to :assignment
belongs_to :user
# submission has a boolean column called submitted with val true or false
end
class User
has_many submissions
end
I want to know how can I query the assignments that a user has not submitted (in a clean way)? If a user submits an assignment, a new submission for that assignment and user will be created.
Not sure if I provided enough info for anyone to answer, so please comment if anything else is needed.Thx!
The logic that #Norly Canarias is using is correct, but I would alter it to use methods on the User class, and I would also modify it to make it database-agnostic (for example, using 'submissions.submitted = true' will not work at all in Postgres).
class User < ApplicationRecord
has_many :submissions
has_many :assignments, through: :submissions
def submitted_assignments
assignments.where(submissions: {submitted: true})
end
def unsubmitted_assignments
Assignment.where.not(id: submitted_assignments)
end
end
I have tested this and it works as expected. For a user who has a Submission for Assignment 1 with submitted == true, and who has a Submission for Assignment 2 with submitted == false, and assuming there are two more Assignments (3 and 4) for which no Submission exists, you will get:
>> user.submitted_assignments.ids
#=>[1]
>> user.unsubmitted_assignments.ids
#=>[2, 3, 4]
I think something like this could work (I haven't tested though):
class Assignment
has_many :submissions
end
class Submission
belongs_to :assignment
belongs_to :user
end
class User
has_many :submissions
has_many :assignments, through: :submissions
end
user = User.first
submitted = user.assignments.where('submissions.submitted = true')
not_submitted = Assignment.where.not(id: submitted)
You can also make it a scope
class Assignment
has_many :submissions
scope :not_submitted_by_user, ->(user) do
where.not(id: user.assignments.where('submissions.submitted = true'))
end
end
user = User.first
not_submitted = Assignment.not_submitted_by_user(user)
To get all the Assignments that are not from a specific user
#assignments = Assignment.where.not(user_id: user_id)
A clean way to do it is to create a scope in the Assignment Model
class Assignment
has_many :submissions
scope :not_from_user, ->(user_id) {where.not(user_id: user_id) }
end
And then calling
#assignments = Assignment.not_from_user 1

Rails saving data from associated form

How can I get the data from an associated form and insert it to the associated table from the main model?
class Supplier < ActiveRecord::Base
has_one :account, foreign_key: "acc_sup_id", :autosave => true
self.primary_key = 'sup_id'
end
class Account < ActiveRecord::Base
belongs_to :supplier, foreign_key: "acc_sup_id"
self.primary_key = 'acc_id'
self.table_name = 'accounts'
end
I am having a combined form for Supplier and Account. When I submit I need to find a way to insert the corresponding values to Supplier and Account. The problem is Supplier values is inserting properly but not Account.
I have asked the same question in several forums, groups and even in stack but nobody seems to give a convincing answer.
The basic strategy is to first look at what params are being submitted when the form is submitted. You could add a line in the controller action such as raise params.inspect to see that. Make sure that those paras contain all the data you need; if not then there is some problem in the view that generates that form.
Once you have all the data getting to the controller action, then you need to change the controller action so that is properly interprets all the data and puts it into the correct models.
I cannot give any more specific advice unless you show the code for your view, the result from doing params.inspect, and the code for the controller action that takes the data.
Try this.
Let's assume that there are orders and customers tables and that you want to perform CRUD operations on customers from orders form.
Customer model is very simple
class Customer < ActiveRecord::Base
attr_accessible :name
end
Order model must provide virtual attributes for all customer's attributes (attr_accessor construct). CRUD for customers is provided through callbacks. Validations can be used as well.
class Order < ActiveRecord::Base
attr_accessor :customer_name
attr_accessible :description, :number, :customer_name
belongs_to :customer
validates_presence_of :number
validates_presence_of :description
validates_presence_of :customer_name
before_save :save_customer
after_find :find_customer
after_destroy :destroy_customer
protected
def save_customer
if self.customer
self.customer.name = self.customer_name
else
self.customer = Customer.create(name: self.customer_name)
end
self.customer.save
end
def find_customer
self.customer_name = self.customer.name
end
def destroy_customer
self.customer.destroy
end
end
Example grid for Order model.
class Orders < Netzke::Basepack::Grid
def configure(c)
super
c.model = 'Order'
c.items = [
:description,
:number,
:customer_name
]
c.enable_edit_inline = false
c.enable_add_inline = false
end
def preconfigure_record_window(c)
super
c.form_config.klass = OrderForm
end
end
Example form for Order model.
class OrderForm< Netzke::Basepack::Form
def configure(c)
super
c.model = 'Order'
c.items = [
:description,
:number,
:customer_name
]
end
end

Sunspot / Solr / Rails: Model Associations are not updating in the Index

I have a Fieldnote model in my app, which has_many :activities attached to it through a table called :fieldnote_activities. I then define a searchable index this way:
searchable :auto_index => true, :auto_remove => true do
integer :id
integer :user_id, :references => User
integer :activity_ids, :multiple => true do
activities.map(&:id)
end
text :observations
end
And then I have a Search model to store / update searches. The search model thus also has its own associations with activities. I then perform my searches like this:
#search = Search.find(params[:id])
#query = Fieldnote.search do |query|
query.keywords #search.terms
if #search.activities.map(&:id).empty? == false
query.with :activity_ids, #search.activities.map(&:id)
end
end
#fieldnotes = #query.results
Now this all works GREAT. The problem is that if I change which activities that are associated with a fieldnote, the search results do not change because it appears the indices for that fieldnote do not change. I was under the impression that the :auto_index => true and :auto_remove => true flags when I define the searchable index would keep track of new associations (or deleted associations), but this appears not to be the case. How do I fix this?
You're right that :auto_index and :auto_remove don't apply to associated objects, just the searchable object they are specified on.
When denormalizing, you should use after_save hooks on the associated objects to trigger a reindex where necessary. In this case, you want changes to the Activity model and the FieldnoteActivity join model to trigger a reindex of their associated Fieldnote objects when saved or destroyed.
class Fieldnote
has_many :fieldnote_activities
has_many :activities, :through => :fieldnote_activities
searchable do
# index denormalized data from activities
end
end
class FieldnoteActivity
has_many :fieldnotes
has_many :activities
after_save :reindex_fieldnotes
before_destroy :reindex_fieldnotes
def reindex_fieldnotes
Sunspot.index(fieldnotes)
end
end
class Activity
has_many :fieldnote_activities
has_many :fieldnotes, :through => :fieldnote_activities
after_save :reindex_fieldnotes
before_destroy :reindex_fieldnotes
def reindex_fieldnotes
Sunspot.index(fieldnotes)
end
end