Rails 4: Modify eager load query when using .includes(:association) - ruby-on-rails

I have two models:
class User < ActiveRecord::Base
has_many :purchases
# Perform joins and attach some calculations to the User object
scope :add_stats, -> { group("users.id").joins(:purchases).select("users.*, SUM(purchases.price) AS total_purchases") }
end
class Purchase < ActiveRecord::Base
belongs_to :user
end
The add_stats scope represents heavy calculations attached to the User objects. So if I want to get all User objects with stats, I just write User.all.add_stats.
So far so good. Now I want to fetch some Purchase objects and eager load the Users with stats as well. I've tried this:
belongs_to :user, -> { add_stats }
But then when Rails eager load the users, it seems to remove .group("user.id").joins(:purchases) and complain on purchases.price - "purchases table unknown". So the .select() is the only thing preserved from the scope.
How do I apply a scope (with working .group().joins()) to the eager load query of all included belongs_to :user objects?

i just tried it in my rails4 app and it works
class User < ActiveRecord::Base
scope :add_stats, -> { group("users.id").joins(:events).select("users.*, SUM(events.id) AS total_events") }
class Event
belongs_to :user, -> { add_stats }
in rails console
Event.includes(:users).first.user.total_events
Reloading...
Event Load (0.1ms) SELECT "events".* FROM "events" WHERE "events"."label" = 'hamburg' ORDER BY "events"."id" ASC LIMIT 1
Participant Load (0.3ms) SELECT "participants".* FROM "participants" WHERE "participants"."event_id" IN (2)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 8, 10, 11, 12, 14, 16, 17, 18, 19, 20)
User Load (0.3ms) SELECT users.*, SUM(events.id) AS total_events FROM "users" INNER JOIN "events" ON "events"."user_id" = "users"."id" WHERE "users"."id" = ? GROUP BY users.id ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]
=> 68
Event.first.user.total_events
Reloading...
Event Load (0.2ms) SELECT "events".* FROM "events" WHERE "events"."label" = 'hamburg' ORDER BY "events"."id" ASC LIMIT 1
User Load (0.2ms) SELECT users.*, SUM(events.id) AS total_events FROM "users" INNER JOIN "events" ON "events"."user_id" = "users"."id" WHERE "users"."id" = ? GROUP BY users.id ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]
=> 68
i guess that this is not what you want, as this does not use the scope for the include.

Related

Ruby on Rails - order not working with distinct.pluck

app/model/line_item.rb
class LineItem < ApplicationRecord
default_scope { order(:order_date, :line_item_index) }
scope :sorted, -> { order(:order_date, :line_item_index) }
scope :open_order_names, -> { distinct.pluck(:order_name) }
end
What I have tried:
LineItem.open_order_names # Way 1
LineItem.sorted.open_order_names # Way 2
LineItem.open_order_names.sorted # Way 3
But I am always getting this error.
ActiveRecord::StatementInvalid (PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
LINE 1: ...ne_items"."order_name" FROM "line_items" ORDER BY "line_item...
^
):
Anyone can help me?
The issue is that you need to specify how they should be distinct, the following should work for you, the select may not be needed.
scope :open_order_names, -> { select(:order_name).distinct(:order_name).pluck(:order_name) }
So it's database restriction. For example we have users table with (id, email).
You can do:
SELECT DISTINCT "users"."email" FROM "users"
or
SELECT "users"."email" FROM "users" ORDER BY "users"."id" ASC
but can not:
SELECT DISTINCT "users"."email" FROM "users" ORDER BY "users"."id" ASC
i.e. you can not order by column which abcent in the SELECT part of query if you use the DISTINCT.
As mentioned above the
scope :open_order_names, -> { select(:order_name).distinct(:order_name).pluck(:order_name) }
could be nice solution.

How to unscope default_scope in join/eager_load?

I have two models:
class User
default_scope -> { where(deleted_at: nil) }
end
class Order
belongs_to :user
end
And I want to get orders with deleted or not deleted users:
Order.joins(:user).merge(User.unscoped)
Order.joins(:user).merge(User.unscope(where: :deleted_at))
# SELECT "orders".* FROM "orders"
# INNER JOIN "users" ON "users"."id" = "orders"."user_id" AND "users"."deleted_at" IS NULL
# ORDER BY "orders"."id" DESC LIMIT 1
Order.eager_load(:user).merge(User.unscoped)
Order.eager_load(:user).merge(User.unscope(where: :deleted_at))
# SELECT "orders"."id" AS t0_r0, "orders"."user_id" AS t0_r1,
# "users"."id" AS t1_r0, "users"."deleted_at" AS t1_r1 FROM "orders"
# LEFT OUTER JOIN "users" ON "users"."id" = "orders"."user_id" AND "users"."deleted_at" IS NULL
# ORDER BY "orders"."id" DESC LIMIT 1
None of these work.
Every query adds "AND "users"."deleted_at" IS NULL" into join statement.
Nothing changes if I specify association scope:
class Order
belongs_to :user, -> { unscoped }
end
However includes works as expected:
Order.includes(:user).merge(User.unscoped).last
# SELECT "orders".* FROM "orders" ORDER BY "orders"."id" DESC LIMIT 1
# SELECT "users".* FROM "users" WHERE "users"."id" = 1054
How can I make rails to unscope association in a join?
You can try like this. It works in Rails >= 3
User.unscoped { Order.joins(:user) }
I solved this issue by writing join query manually. For your case it should look like:
Order.joins('INNER JOIN users ON users.id=orders.user_id')
Although Order.includes(:user).merge(User.unscoped)solution, that you found, looks a bit nicer, unless you really want to have only one query
You can define another association on same model to target specifically those deleted users:
This example works assuming you use act-as-paranoid to handle soft-deletion.
class Order
belongs_to :user
belongs_to :all_user, -> { with_deleted }, foreign_key: 'user_id'
end
Then choose your weapons:
Order.includes(:user).pluck(:email) # Only non soft-deleted emails
Order.includes(:all_user).pluck(:email) # All emails including where deleted_at is null
```

How to lock a parent record in Rails/ActiveRecord?

When there is a single parent record associated with multiple child records, using row locking on the parent record is an obvious way to ensure consistency. However, I cannot seem to find a clean way to do this in ActiveRecord.
For example, say we have two models: Order and OrderProduct.
class Order < ActiveRecord::Base
has_many :order_products
...
end
class OrderProduct < ActiveRecord::Base
belongs_to :order
...
end
Updating an OrderProduct affects the overall state of the Order, so we want to make sure only one transaction is updating an Order at any given time.
If we're trying to achieve this when editing an OrderProduct, the cleanest way in ruby I can see is:
def edit
product = OrderProduct.find params[:id]
Order.transaction do
product.order.lock!
# Make sure no changes have occurred while we were waiting for the lock
product.reload
# Do stuff...
product.order.some_method
end
end
However this if rather inefficient with SQL queries, producing:
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", "2"]]
SELECT "orders".* FROM "orders" WHERE "orders"."id" = 2 LIMIT 1
SELECT "orders".* FROM "orders" WHERE "orders"."id" = $1 LIMIT 1 FOR UPDATE [["id", 2]]
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", 2]]
SELECT "orders".* FROM "orders" WHERE "orders"."id" = 2 LIMIT 1
We can reduce the number of queries by changing the to something along the lines of:
def edit
product = OrderProduct.find params[:id]
Order.transaction do
order = Order.find product.order_id, lock: true
# Make sure no changes have occurred while we were waiting for the lock
product.reload
# Cache the association
product.order = order
# Do stuff...
product.order.some_method
end
end
which produces better SQL:
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", "2"]]
SELECT "orders".* FROM "orders" WHERE "orders"."id" = 2 LIMIT 1 FOR UPDATE [["id", 2]]
SELECT "order_products".* FROM "order_products" WHERE "order_products"."id" = $1 LIMIT 1 [["id", 2]]
However the code is messier.
Is there a cleaner way of doing this with ActiveRecord? Calling product.order = order just to get the association cached seems a little dangerous.
For a simple Rails way of locking, check out http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
.lock.load
Is what you are looking for.
probably?

Adding a scope to a has_many through association in Rails

I have a Project and User models joined by a Membership model. I want to retrieve a project's members except one user.
project.members.where.not(id: current_user)
Like this answer, I want to use a scope:
class User < ActiveRecord::Base
scope :except, ->(user) { where.not(id: user) }
end
But this doesn't work.
p.members.except(User.find(1))
User Load (1.0ms)
SELECT "users".* FROM "users"
WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
User Load (0.4ms)
SELECT "users".* FROM "users"
INNER JOIN "memberships" ON "users"."id" = "memberships"."user_id"
WHERE "memberships"."project_id" = $1 [["project_id", 2]]
As you can see this results in two queries, not one. And returns all the members, not taking into account the exclusion.
Why doesn't this work?
Try renaming the scope to something else, like except_for or all_except. Name except is already used by active_record
http://api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html#method-i-except
Also you get 2 queries because you are doing User.find(1) which results in the first query.

Rails 3.1: Optimizing many_to_many through join table in single query?

User has many Tracks, through Favorite. Favorite has some extra per-user meta-data about the related track, and the whole thing is returned as a json blob using custom :as_public hashing method.
Even though I'm accessing the related objects using a JOIN, I'm making hundreds of very basic SELECT track FROM tracks WHERE track.id='1' queries. I want to optimize this lookup.
users_controller:
def show
#user = User.find(params[:id])
respond_to do |format|
format.html # index.html.erb
format.json { render json: #user.to_json(:methods => [:favorites_as_public_tracks]) }
end
end
user.rb
def favorites_as_public_tracks
favorites.joins(:track).sort_by(&:created_at).map(&:as_public_track)
end
favorite.rb
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :track
#Grabs some stuff from Favorite, merging it with the public data from Track
def as_public_track
track.public_attributes.merge(public_attributes_for_merging_onto_track)
end
# This stuff gets added onto track.to_json and used by javascript
def public_attributes_for_merging_onto_track
return {
:favorite_id => id,
:from_service => from_service,
:favorited_at => created_at,
:collection_name => "#{collection_name}, #{from_service}"
}
end
def public_attributes
private_attrs = [:user_id]
attributes.reject {|key, val| private_attrs.include? key.to_sym }
end
end
track.rb
def public_attributes
private_attrs = [] #[:id]
attributes.reject {|key, val| private_attrs.include? key.to_sym }
end
The SQL that gets run when I access the user's favorites as public tracks is:
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", "1"]]
Favorite Load (7.8ms) SELECT "favorites".* FROM "favorites" INNER JOIN "tracks" ON "tracks"."id" = "favorites"."track_id" WHERE "favorites"."user_id" = 1 ORDER BY "favorites".created_at DESC
Track Load (1.3ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 1 LIMIT 1
Track Load (0.5ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 2 LIMIT 1
Track Load (0.3ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 3 LIMIT 1
Track Load (0.3ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 4 LIMIT 1
Track Load (0.5ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 5 LIMIT 1
Track Load (0.2ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 6 LIMIT 1
Track Load (0.2ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 7 LIMIT 1
Track Load (0.2ms) SELECT "tracks".* FROM "tracks" WHERE "tracks"."id" = 8 LIMIT 1
How do I do this without making hundreds of SELECT track where track.id='...' queries?
Thanks!
If you have a list that stays the same for all instances, consider making it a class method rather than an instance method. See if you can make the public attributes and private attributes lists as class methods. Otherwise, to build these lists for each instance, you will get a hit for each record.
Usually, :include :tracks in the finds can fix it, but I don't think that is the issue here.
Good luck!

Resources