How to unscope default_scope in join/eager_load? - ruby-on-rails

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

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.

Rails - pass argument to chained scopes

I'm trying to figure out how to chain multiple scopes together. What I'm trying to do is have a search box that will pass params[:user_search] to the controller which calls the by_keyword scope in the user model. the by_keyword scope is working as i have it now, but i would like to make it also search all of the other scopes i have as well. So essentially the by_keyword scope should query all scopes for whatever keyword a user entered.
in my users_controller index action
if params[:user_search].present?
#users = #users.by_keyword(params[:user_search])
end
in my user model i have
scope :by_keyword, -> (keyword) { where('experience LIKE ? OR current_job_title LIKE ?', "%#{keyword}%", "%#{keyword}%" ).order(updated_at: :desc) if keyword.present? }
i would like to find a way to chain all of these to the by_keyword scope
# these scopes call child classes of User such as skills, languages, patents, etc...
scope :by_skill, -> (sk) { joins(:skills).distinct.where( 'skills.name LIKE ?', "%#{sk}%" ).order(updated_at: :desc) if sk.present? }
scope :by_language, -> (lang) { joins(:languages).distinct.where( 'languages.language LIKE ?', "%#{lang}%" ).order(updated_at: :desc) if lang.present? }
scope :by_certification_or_cert_authority, -> (cert) { joins(:certifications).distinct.where( 'certifications.certification_name LIKE ? OR certifications.certification_authority LIKE ?', "%#{cert}%", "%#{cert}%" ).order(updated_at: :desc) if cert.present? }
scope :by_education_level, -> (ed) { joins(:qualifications).distinct.where( 'qualifications.education LIKE ?', "%#{ed}%" ).order(updated_at: :desc) if ed.present? }
scope :by_university_major, -> (maj) { joins(:qualifications).distinct.where( 'qualifications.university_major LIKE ?', "%#{maj}%" ).order(updated_at: :desc) if maj.present? }
i read over http://guides.rubyonrails.org/active_record_querying.html#scopes
and the example they give is this, but I'm not sure how to do this with more than just 2 scopes chained together.
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
You can do it by make a function in user controller which call send and give it an array like here
In user model
def self.send_chain(methods)
methods.inject(self, :send)
end
Then call it like
User.send_chain(["by_skill", "by_language"])
If you have to send params you can do it like this:
scopes = ["by_skill", "by_language"]
parameters = ["clever", "English"]
result = []
scopes.each_with_index do |scope, index|
result = result + User.send(scope, parameters[index])
end
Hope this helps.
Try this:
scope :all_clild, -> (val) { |val| by_skill(val).or.by_language(val).or.by_certification_or_cert_authority(val).........# all other scopes}
merged_scope = by_keyword.merge(all_clild)
I was able to pass an argument to the scopes like this
scope :by_keyword, -> (k) { by_skill(k) | by_language(k) | by_certification_or_cert_authority(k) | by_education_level(k) | by_university_major(k)}
I'm not sure if this is really considered "chaining" them though. I'm guessing there is probably a better way to do this, if there is please let me know.
it's making a ton of queries so I'm not sure if this is advisable performance wise to even have this many scopes being called the way they are. This is the result when searching on the term "CCNA"
User Load (0.5ms) SELECT DISTINCT "users".* FROM "users" INNER JOIN "skills" ON "skills"."user_id" = "users"."id" WHERE (skills.name LIKE '%CCNA%') ORDER BY "users"."updated_at" DESC
User Load (0.5ms) SELECT DISTINCT "users".* FROM "users" INNER JOIN "languages" ON "languages"."user_id" = "users"."id" WHERE (languages.language LIKE '%CCNA%') ORDER BY "users"."updated_at" DESC
User Load (0.6ms) SELECT DISTINCT "users".* FROM "users" INNER JOIN "certifications" ON "certifications"."user_id" = "users"."id" WHERE (certifications.certification_name LIKE '%CCNA%' OR certifications.certification_authority LIKE '%CCNA%') ORDER BY "users"."updated_at" DESC
User Load (0.5ms) SELECT DISTINCT "users".* FROM "users" INNER JOIN "qualifications" ON "qualifications"."user_id" = "users"."id" WHERE (qualifications.education LIKE '%CCNA%') ORDER BY "users"."updated_at" DESC
User Load (0.5ms) SELECT DISTINCT "users".* FROM "users" INNER JOIN "qualifications" ON "qualifications"."user_id" = "users"."id" WHERE (qualifications.university_major LIKE '%CCNA%') ORDER BY "users"."updated_at" DESC
Also from a security standpoint I'm not sure if it's all that good to allow the user to enter one query that touches so many tables.

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