ActiveRecord firing 2 query for search - ruby-on-rails

Here's what is happening with my application.
My model looks like this
class Model1 < ActiveRecord::Base
def self.foo(value)
Model1.where(:field => value)
end
end
and then i have a controller using this model
...
Model1.foo('foo)
...
Now, i am expecting it to trigger a single query to get the records. Instead of that, what i am getting is 2 queries.
SELECT COUNT(*) FROM `MODEL1` WHERE `MODEL1`.`field` = 'foo'
SELECT * FROM `MODEL1` WHERE `MODEL1`.`field` = 'foo'
Not able to understand why the first query is being fired and how to avoid it. Couldn't find anything on net.

I'm a bit confused (like others in the comments) but here's what you can try -
class Model1 < ActiveRecord::Base
def self.foo(value)
Model1.where(:field => value)
end
end
should be
class Model1 < ActiveRecord::Base
def foo(value)
self.where("field = ?", value) #see http://guides.rubyonrails.org/active_record_querying.html#array-conditions
end
end

Related

ActiveRecord custom after callback current_scope is not nil

I have a model (let's call it Blog). It looks like:
class Blog < ApplicationRecord
belongs_to :author
def self.foo_all
all.each(&:bar)
end
def bar
author.baz
end
end
Now, the problem I'm having is that when author.blogs.foo_all method is called, it seems that the Blog class has a current_scope! This means that when inside the author.baz method, any query on Blog has the author as a scope on the query (WHERE authors.id == 123)!
Using an example where
blog.id == 123
author.id == 456
this is what happens:
Class Author < ApplicationRecord
has_many :blogs
def baz
id # 456
blogs.count # "SELECT count(*) FROM blogs WHERE blogs.author_id == 456"
Blog.count # "SELECT count(*) FROM blogs WHERE blogs.author_id == 456"
Blog.current_scope # "[Blog id: 123, author_id: 456]"
end
end
Yes, I can work around this why explicitly removing that scope, but it's SO unexpected! In what world does Blog.count not perform an unscoped query?!
Note: I do not use a default_scope anywhere.
Rails: 6.1.3.2
Copy of code with rspec test that replicates the problem https://github.com/hrdwdmrbl/StockOverflow69020511

Rails create a Model for a controller that uses a .group method

I need to collect my customers with Spree::Order.group(:email) (since we have a guest checkout option).
The controller is as such:
class CustomersController < ApplicationController
def index
#customers = Spree::Order.where(state: "complete").group(:email).select(
:email,
'count(id) AS total_orders_count',
'sum(payment_total) AS amount',
'array_agg(number) AS order_numbers',
'array_agg(completed_at) AS completion_dates'
)
end
Can I create a customer.rb model for these #customers so I can move the logic there. I need to .joins(:line_items) and filter by date, so I figure it'd be cleaner.
P.S. As an asside...the 'sum(payment_total' AS amount always returns 0.0 because the payment_total is a BigDecimal object. What's the correct syntax for this request that would act like 'sum(payment_total.to_f)'....
Since you don't have a customers table, and you're trying to abstract the concept of a guest customer out of the orders table, creating a model is a reasonable approach.
Consider the following:
class Customer < ActiveRecord::Base
# Use the `orders` table for this "virtual" model
self.table_name = Spree::Order.table_name
# ensure no writes ever occur from this model as a precaution
def readonly?; true; end
scope :with_completed_orders, -> {
where(state: "complete")
.select(
:email,
'count(id) AS total_orders_count',
'sum(payment_total) AS amount',
'array_agg(number) AS order_numbers',
'array_agg(completed_at) AS completion_dates'
)
.group(:email)
.order(:email) # prevents errors - remove it and try Customer.first in the console.
}
scope :by_email, -> (email) { where(email: email) }
# Default scopes are generally a no-no, but a convenience here
def self.default_scope
# this could be any scope that groups by :email
self.with_completed_orders
end
# example of a potentially useful instance method
def orders
# returns a scope, which you can chain
Spree::Order.where(email: email)
end
end
The selects in the scope populate Customer instance attributes of the same name, as you probably know.
This will allow you to do things like:
customers = Customer.all
c = Customer.by_email('test#example.com')
c.amount # returns BigDecimal
c.order_numbers # returns Array
c.orders.first # example of chaining scope for #orders instance method
In the controller:
class CustomersController < ApplicationController
def index
#customers = Customer.all
end
end
Hopefully this gives you some ideas.

How to check if associated model has entries in Rails 5?

I have a model RegularOpeningHour(dayOfWeek: integer) that is associated to a model OpeningTime(opens: time, closes: time). RegularOpeningHour has an 1:n relation to OpeningTime, so that a specific day can have many opening times.
(I know that I simply could have one entry with 'opens' and 'closes' included in RegularOpeningHour but for other reasons I need this splitting)
Now I want a open?-Method, that returns whether the business is opened or not. I tried the following in my model file regular_opening_hour.rb:
def open?
RegularOpeningHour.where(dayOfWeek: Time.zone.now.wday).any? { |opening_hour| opening_hour.opening_times.where('? BETWEEN opens AND closes', Time.zone.now).any? }
end
Unforutnately, that doesn't work. Any ideas to solve this?
How about this:
def open?
joins(:opening_times)
.where(dayOfWeek: Time.current.wday)
.where("opens <= :time AND closes >= :time", time: Time.current)
.any?
end
EDIT: Missing ':' in the join
You could create some scopes to make selecting open OpeningTimes and open RegularOpeningHours less clunky. This makes creating the given selection much easier.
class OpeningTime < ApplicationRecord
# ...
belongs_to :regular_opening_hour
def self.open
time = Time.current
where(arel_table[:opens].lteq(time).and(arel_table[:closes].gteq(time)))
end
# ...
end
class RegularOpeningHour < ApplicationRecord
# ...
has_many :opening_times
def self.open
where(
dayOfWeek: Time.current.wday,
id: OpeningTime.select(:regular_opening_hour_id).open,
)
end
# ...
end
def open?
RegularOpeningHour.open.any?
end
Since you have has_many association of RegularOpeningHour to OpeningTime you can use join query like below.:
RegularOpeningHour.joins(:opening_times).where(dayOfWeek: Time.zone.now.wday).where('? BETWEEN opening_times.opens AND opening_times.closes', Time.zone.now).any?

How to update instance variable in Rails model?

In my Rails app I have users who can have many payments.
class User < ActiveRecord::Base
has_many :invoices
has_many :payments
def year_ranges
...
end
def quarter_ranges
...
end
def month_ranges
...
end
def revenue_between(range, kind)
payments.sum_within_range(range, kind)
end
end
class Invoice < ActiveRecord::Base
belongs_to :user
has_many :items
has_many :payments
...
end
class Payment < ActiveRecord::Base
belongs_to :user
belongs_to :invoice
def net_amount
invoice.subtotal * percent_of_invoice_total / 100
end
def taxable_amount
invoice.total_tax * percent_of_invoice_total / 100
end
def gross_amount
invoice.total * percent_of_invoice_total / 100
end
def self.chart_data(ranges, unit)
ranges.map do |r| {
:range => range_label(r, unit),
:gross_revenue => sum_within_range(r, :gross),
:taxable_revenue => sum_within_range(r, :taxable),
:net_revenue => sum_within_range(r, :net) }
end
end
def self.sum_within_range(range, kind)
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&:"#{kind}_amount")
end
end
In my dashboard view I am listing the total payments for the ranges depending on the GET parameter that the user picked. The user can pick either years, quarters, or months.
class DashboardController < ApplicationController
def show
if %w[year quarter month].include?(params[:by])
#unit = params[:by]
else
#unit = 'year'
end
#ranges = #user.send("#{#unit}_ranges")
#paginated_ranges = #ranges.paginate(:page => params[:page], :per_page => 10)
#title = "All your payments"
end
end
The use of the instance variable (#sum) greatly reduced the number of SQL queries here because the database won't get hit for the same queries over and over again.
The problem is, however, that when a user creates, deletes or changes one of his payments, this is not reflected in the #sum instance variable. So how can I reset it? Or is there a better solution to this?
Thanks for any help.
This is incidental to your question, but don't use #select with a block.
What you're doing is selecting all payments, and then filtering the relation as an array. Use Arel to overcome this :
scope :within_range, ->(range){ where date: range }
This will build an SQL BETWEEN statement. Using #sum on the resulting relation will build an SQL SUM() statement, which is probably more efficient than loading all the records.
Instead of storing the association as an instance variable of the Class Payment, store it as an instance variable of a user (I know it sounds confusing, I have tried to explain below)
class User < ActiveRecord::Base
has_many :payments
def revenue_between(range)
#payments_with_invoices ||= payments.includes(:invoice => :items).all
# #payments_with_invoices is an array now so cannot use Payment's class method on it
#payments_with_invoices.select { |x| range.cover? x.date }.sum(&:total)
end
end
When you defined #sum in a class method (class methods are denoted by self.) it became an instance variable of Class Payment. That means you can potentially access it as Payment.sum. So this has nothing to do with a particular user and his/her payments. #sum is now an attribute of the class Payment and Rails would cache it the same way it caches the method definitions of a class.
Once #sum is initialized, it will stay the same, as you noticed, even after user creates new payment or if a different user logs in for that matter! It will change when the app is restarted.
However, if you define #payments_with_invoiceslike I show above, it becomes an attribute of a particular instance of User or in other words instance level instance variable. That means you can potentially access it as some_user.payments_with_invoices. Since an app can have many users these are not persisted in Rails memory across requests. So whenever the user instance changes its attributes are loaded again.
So if the user creates more payments the #payments_with_invoices variable would be refreshed since the user instance is re-initialized.
Maybe you could do it with observers:
# payment.rb
def self.cached_sum(force=false)
if #sum.blank? || force
#sum = includes(:invoice => :items)
end
#sum
end
def self.sum_within_range(range)
#sum = cached_sum
#sum.select { |x| range.cover? x.date }.sum(&total)
end
#payment_observer.rb
class PaymentObserver < ActiveRecord::Observer
# force #sum updating
def after_save(comment)
Payment.cached_sum(true)
end
def after_destroy(comment)
Payment.cached_sum(true)
end
end
You could find more about observers at http://apidock.com/rails/v3.2.13/ActiveRecord/Observer
Well your #sum is basically a cache of the values you need. Like any cache, you need to invalidate it if something happens to the values involved.
You could use after_save or after_create filters to call a function which sets #sum = nil. It may also be useful to also save the range your cache is covering and decide the invalidation by the date of the new or changed payment.
class Payment < ActiveRecord::Base
belongs_to :user
after_save :invalidate_cache
def self.sum_within_range(range)
#cached_range = range
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&total)
end
def self.invalidate_cache
#sum = nil if #cached_range.includes?(payment_date)
end

Rails named_scope inheritance?

I'm trying to generalize some of my models by providing a common base model to inherit from that contains some mutual named_scope declarations and a filter method that activates that search for simpler querying on the controller side. This appears to be working when I run it in the console, but fails when in the controller:
# in the base model
class GenericModel < ActiveRecord::Base
named_scope :by_name, lambda { |name|
( name.blank? ) ? {} : { :conditions => [ "#{self.table_name}.name like ?", "%#{name}%" ] }
}
def filter(params)
res = []
res = self.by_name( (params[:name] or '') ) if params[:name]
return res
end
end
class MyModel < GenericModel
set_table_name 'my_models'
end
# works in in console!
>> params = { :name => 'jimmy' }
>> MyModel.filter(params)
=> [ <#MyModel ...>, ... ]
nil
# fails in controller
#model = MyModel.filter(params)
# ActiveRecord::StatementInvalid (Mysql::Error Unknown column 'generic_models.name' in where clause...)
Apparently the parent class' named_scope is being called when in rails, but works fine in rails console. Any ideas how to mend this? thanks.
That's a bit of a train-wreck because of the way ActiveRecord is trying to interpret what you're saying. Generally the first class derived from ActiveRecord::Base is used to define what the base table name is, and sub-classes of that are defined to use Single Table Inheritance (STI) by default. You're working around this by using set_table_name but, as is often the case, while it's possible to go against the grain in Rails, things often get messy.
You should be able to do this a lot more cleanly using a mixin as suggested by Beerlington.
module ByNameExtension
def self.extended(base)
# This method is called when a class extends with this module
base.send(:scope, :by_name, lambda { |name|
name.blank? ? nil : where("#{self.table_name}.name LIKE ?", "%#{name}%")
})
end
def filter(params)
params[:name].present? ? self.by_name(params[:name]) : [ ]
end
end
class MyModel < ActiveRecord::Base
# Load in class-level methods from module ByNameExtension
extend ByNameExtension
end
You should be able to keep your extensions contained to that module. If you want to clean this up even further, write an initializer that defines a method like scoped_by_name for ActiveRecord::Base that triggers this behavior:
class ActiveRecord::Base
def scoped_by_name
extend ByNameExtension
end
end
Then you can tag all classes that require this:
class MyModel < ActiveRecord::Base
scoped_by_name
end

Resources