How to simplify this ActiveRecord eager-load query? - ruby

In a project of mine, I'm using the Ruby ActiveRecord (not a Rails application, though) and I use the following structure:
class Customer < ::ActiveRecord::Base
has_and_belongs_to_many :categories
end
class Categories < ::ActiveRecord::Base
has_and_belongs_to_many :customers
end
In one part of the application, I load all the customers to process them and I try to eager-load their relevant categories (within a ActiveRecord::IdentityMap.use block):
Customer.includes(:categories).all
This does what I need to do, but when I look at the resulting eager-load query, it reads like:
SELECT "categories".*, "t0"."customer_id" AS ar_association_key_name_customer_id
FROM "categories" INNER JOIN "customers_categories" "t0"
ON "categories"."id" = "t0"."category_id" WHERE
((("t0"."customer_id" = 1) OR ("t0"."customer_id" = 2) OR ("t0"."customer_id" = 3) OR ... ))
I am loading all the customers and there is no need to filter them on the join table. There are only several categories, but many customers and the resulting query has thousands of unneeded OR statements.
Is there a way to simplify the query (in the ActiveRecord way) to not include the WHERE conditions in form of customer_id = X ?

Related

Rails 5 ActiveRecord - combine OR adn AND clauses

I can't figure out the right syntax to use when including several models and using AND or OR clauses.
For example, there Shop model that has_one relation with Address model and belongs_to with Country.
How for example add OR to the below query:
Shop.includes(:address, :country)
Trying like this:
Shop.includes(:address, :country).where('countries.code'=> 'FR').and('counties.updated_at > ?', Date.today.days_ago(7))
raises the error:
NoMethodError: undefined method `and' for #<Shop::ActiveRecord_Relation:0x00007fb90d0ea3f8>
I found this thread at SO, but in this case, I have to repeat the same where clause before each OR statement? - looks not so DRY :(
What am I missing ?
Don't kick yourself... you don't need to use and at all, just string another where in:
Shop.includes(:address, :country).where('countries.code'=> 'FR').where('counties.updated_at > ?', Date.today.days_ago(7))
There is a better solution if you need to add multiple OR clause to AND clause. To get around it, there is arel_table method that can be used as follows.
Let's say we have the following models
Shop -> has_one :address
Shop -> belongs_to :country
and we would like to find all the shops by country code and address updated_at or country updated_at should be greater then a date you pass in:
some_date = Date.today
countries = Country.arel_table
addresses = Address.arel_table
# creating a predicate using arel tables
multi_table_predicate = countries[:updated_at].gt(some_date).or(addresses[:updated_at].gt(some_date))
# building the query
shops = Shop.includes(:address, :country).where(countries: {code: 'FR'}).where(multi_table_predicate)
This will execute a LEFT OUTER JOIN and here is where clause:
WHERE "countries"."code" = $1 AND ("countries"."updated_at" > '2019-03-12' OR "addresses"."updated_at" > '2019-03-12')
Sure, you can chain more tables and multiply OR conditions if you want.
Hope this helps.

ActiveRecord: check whether association exists without loading it?

Suppose I've got ActiveRecord models such that User has_one :photo. In the database, photos has a t.binary column which may hold a lot of data, so I don't want to SELECT that column unless I need to.
I want to do something like:
users.each do |user|
image_tag(user_photo_path) if user.photo.present?
end
However, I don't want to call user.photo.present? because:
Doing so loads the photo association, including SELECT * from photos
Even if it could be made to only SELECT id FROM photos to check existence, it's still an N + 1 query.
What I really want is to load users with a single query which gives each one a property telling me whether it has an associated photo or not.
With ActiveRecord 5, this works:
class User < ActiveRecord::Base
scope :with_photo_id, -> {
left_outer_joins(:photo).select(
"users.*, photos.id AS photo_id"
)
}
end
Then I can call User.with_photo_id and check user.photo_id.present?.
Prior to AR 5, the join would be uglier:
joins(
"LEFT OUTER JOIN photos ON photos.user_id = users.id"
)

Ruby, Retrieve Child Object By Key

I am trying to retrieve a child object based on the key in its parent's table. For instance, I have the Customer class which contains a "store_id" key to the Stores tables. If a customer has a "store_id" key, I would like to bring back that Store object and not the parent Customer object.
EDIT: Here is a sql statement showing what I am trying to do.
So the SQL statement would look something like this.
"SELECT storeS.* FROM customers INNER JOIN stores ON customers.store_id = storeS.id WHERE customers.id = '9'"
I know the sql is probably wrong, but thats a very concise way to show it.
I am assuming you are using rails with the out-of-the-box configuration (using ActiveRecord).
By convention, the "store_id" key in the "customers" table should match an "id" field in the "stores" table. You should also have the following class models setup:
class Store < ActiveRecord::Base
has_many :customers # this is not required for what you want to do here, but recommended
end
class Customer < ActiveRecord::Base
belongs_to :store
end
Assuming this is true, you can either do this if you have the store key:
# assuming we have store key == 9
Store.find(key)
Or you could do this if you already have the customer:
# assuming we have customer.store_id == 9
customer.store
Or if you only have the customer key:
# assuming we have a customer key == 9
customer = Customer.find(9)
store = customer.store
I don't use ActiveRecord a lot, but I think it's this:
Store.find(customer.store_id)

Why is my has_many through associated record (sometimes) readonly?

I have three ActiveRecord models: Partner, MembershipChannel (which is an STI model, inheriting from Channel) and ChannelMembership (I was not responsible for naming these models…)
When I load a ChannelMembership through the Partner association, I sometimes(!) end up with a readonly record. This is in Rails 3.0.9. The same code did not behave this way in 2.3.11.
> p = Partner.first
> p.channel_memberships.map(&:readonly?)
# => [false, false, false, false, false, false]
> p.reload.channel_memberships.limit(1).first.readonly?
# => false
> p.reload.channel_memberships.first.readonly?
# => true
Why is readonly? true when first is called on the association, but not on the relation from limit?
I understand that readonly is triggered if I use SQL fragments when finding a record, but this isn't the case here. It is just a plain has_many through association. The only complicating matter is that it joins on an STI model. What's more, looking at the generated SQL from the last two examples, they are identical!
I can get the behaviour I want by specifying :readonly => false on the association, but I want to understand what is going on.
There are no default scopes on Channel, MembershipChannel or ChannelMembership. Here is the association declaration on Partner:
class Partner
has_many :membership_channels
has_many :channel_memberships, :through => :membership_channels
end
Here is the generated SQL from my logs:
Partner Load (0.4ms) SELECT "partners".* FROM "partners" LIMIT 1
ChannelMembership Load (0.7ms) SELECT "channel_memberships".* FROM "channel_memberships" INNER JOIN "channels" ON "channel_memberships".channel_id = "channels".id WHERE (("channels".partner_id = 2) AND (("channels"."type" = 'MembershipChannel')))
Partner Load (0.5ms) SELECT "partners".* FROM "partners" WHERE "partners"."id" = 2 LIMIT 1
ChannelMembership Load (1.0ms) SELECT "channel_memberships".* FROM "channel_memberships" INNER JOIN "channels" ON "channel_memberships".channel_id = "channels".id WHERE (("channels".partner_id = 2) AND (("channels"."type" = 'MembershipChannel'))) LIMIT 1
Partner Load (0.4ms) SELECT "partners".* FROM "partners" WHERE "partners"."id" = 2 LIMIT 1
ChannelMembership Load (0.6ms) SELECT "channel_memberships".* FROM "channel_memberships" INNER JOIN "channels" ON "channel_memberships".channel_id = "channels".id WHERE (("channels".partner_id = 2) AND (("channels"."type" = 'MembershipChannel'))) LIMIT 1
I was able to reproduce your problem through a basic has_many :through association and am also as to what's causing it.
From what I can tell, it only happens when the reload method is called on the original object. I'm not sure if this is because of anything that reload's doing specifically, or perhaps because certain attribute flags are being reset?
My second theory is that it has something to do with the fact that
p.reload.channel_memberships.limit(1)
returns an ActiveRecord::Relation through which you obtain your first ChannelMembership, and
p.reload.channel_memberships.first
loads it directly from the association. Perhaps some combination of reload resetting certain cached items (I don't know the AR source) is flagging the association as read only. When you apply the limit(1) scope on it, it may be resetting these in a new relation, and working as you'd expect.
I'd poke around ActiveRecord::Persistence / Associations a bit more for the full answer.

Rails 3 Query: How to get most viewed products/articles/whatever?

I always wondered how to query and get results that doesn't fit in a model. Similar how it's done using LINQ and projecting into anonymous objects.
So here's the simple schema:
# Product.rb
class Product < ActiveRecord::Base
has_many :product_views
# attributes: id, name, description, created_at, updated_at
end
# ProductView.rb
class ProductView < ActiveRecord::Base
belongs_to :product
# attributes: id, product_id, request_ip, created_at, updated_at
end
Basically I need to get a list of Products (preferably just id and name) along with the count of views it had. Obviously ordered by view count desc.
This is the SQL I want to get:
select
p.id,
p.name,
count(pv.product_id) as views
from
product_views pv
inner join
products p on pv.product_id = p.id
group by
pv.product_id
order by
count(product_id) desc
I tried the following and similar, but I'm getting ProductView objects, and I would like to get just an array or whatever.
ProductView.includes(:product)
.group('product_id')
.select("products.id, products.name, count(product_id)")
This kind of thing are trivial using plain SQL or LINQ, but I find myself stucked with this kind of queries in Rails. Maybe I'm not thinking in the famous 'rails way', maybe I'm missing something obvious.
So how do you do this kind of queries in Rails 3, and specifically this one? Any suggestions to improve the way I'm doing this are welcome.
Thank you
You can use Arel to do what you're looking for:
products = Product.arel_table
product_views = ProductView.arel_table
# expanded for readability:
sql = products.join(product_views)
.on(product_views[:product_id].eq(product[:id]))
.group(product_views[:product_id])
.order('views DESC')
.project(products[:id],
products[:name],
product_views[:id].count.as('views'))
products_with_views = Product.connection.select_all(sql.to_sql) # or select_rows to just get the values
Yes, it is long, but Arel is a very smart way to deal with creating complex queries that can be reused regardless of the database type.
Within a class method in the Product class:
Product.includes(:product_views).all.map { |p| [p.id, p.name, p.product_views.size] }
Then sort it however you want.
I don't know if there's a way to do it using your models. I would probably resort to:
Product.connection.select_rows(sql)
Which will give you an array of arrays. You can use select_all if you'd rather have an array of hashes.
Try this:
#product = Product.find(#product_id)
#product_views = #product.product_views.count
(Source - http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html#M000292)
Hope this helps!

Resources