Adding a scope to a has_many through association in Rails - ruby-on-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.

Related

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
```

Find method is throwing RecordNotFound when using parent class to search in Single Table Inheritance

When I try to do a find using the parent class, it doesn't work until I use the child class first. This problem exists with Single Table Inheritance. I have my models like this:
class User < ActiveRecord::Base
self.inheritance_column = :meta_type
scope :doctors, -> { where(meta_type: 'Doctor') }
end
class Provider < User
end
class Doctor < Provider
end
When I do Provider.find(1), rails throws record not found. But after I do a successful Doctor.find(1), Provider.find(1) works perfectly fine.
What could be the problem?
EDIT
Here are the SQL queries, notice how rails correctly makes the SQL query to include the Provider in meta_type in the third query:
Provider.find(1):
SELECT "public"."users".* FROM "public"."users" WHERE "public"."users"."meta_type" IN ('Provider') AND "public"."users"."id" = $1 LIMIT 1 [["id", 1]]
Doctor.find(1):
SELECT "public"."users".* FROM "public"."users" WHERE "public"."users"."meta_type" IN ('Doctor') AND "public"."users"."id" = $1 LIMIT 1 [["id", 1]]
Provider.find(1) (second time):
SELECT "public"."users".* FROM "public"."users" WHERE "public"."users"."meta_type" IN ('Provider', 'Doctor') AND "public"."users"."id" = $1 LIMIT 1 [["id", 1]]

How to reduce database queries and is it Worth it?

I currently have an app that lists flights from one location to another, its price and other information. I have implemented a search through a drop down list so it only shows flights either from a certain location, to a certain location or from and to a certain location, depending on how the user searches.
def index
#flights = Flight.all
#flights_source = #flights.select('DISTINCT source') #this line is used for options_from_collection_for_select in view
#flights_destination = #flights.select('DISTINCT destination') #this line is used for options_from_collection_for_select in view
if params[:leaving_from].present? && params[:going_to].blank?
#flights = Flight.where(:source => params[:leaving_from])
elsif params[:going_to].present? && params[:leaving_from].blank?
#flights = Flight.where(:destination => params[:going_to])
elsif params[:leaving_from].present? && params[:going_to].present?
#flights = Flight.where(:source => params[:leaving_from]).where(:destination => params[:going_to])
end
end
The problem is every time I want to add another search parameter, for example price, it's going to be another query. Is there a way to take Flight.all and search within the result and make a new hash or array with only the records that match the search terms, instead of doing a new query with select DISTINCT.
The closest thing I could come up with is somehow turning the result of Flight.all into a array[hash] and using that get the results for distinct source and destination. But not sure how to do that.
And finally would it be worth it to do this to reduce the number of database queries?
These are the current queries:
Flight Load (1.4ms) SELECT "flights".* FROM "flights"
User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]
Flight Load (1.4ms) SELECT DISTINCT source FROM "flights"
Flight Load (0.8ms) SELECT DISTINCT destination FROM "flights"
EDIT:
I changed the select distinct to
#flights_source = #flights.uniq.pluck(:source)
#flights_destination = #flights.uniq.pluck(:destination)
And used options_for_select instead of options_from_collection_for_select in the view. But the queries are still, I think this means I eliminated us much as I can, not sure though.
(0.8ms) SELECT DISTINCT "flights"."source" FROM "flights"
(0.6ms) SELECT DISTINCT "flights"."destination" FROM "flights"
Request Load (1.3ms) SELECT "requests".* FROM "requests"
Flight Load (1.0ms) SELECT "flights".* FROM "flights"
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 2]]

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?

Rails 4: Modify eager load query when using .includes(:association)

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.

Resources