Rails - pass argument to chained scopes - ruby-on-rails

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.

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.

Change 'ORDER BY' Chain on ActiveRecord Subset

Background:
I have Product model which includes 4 categories
class Product < ActiveRecord::Base
enum category: [:recent, :cheapest, :most_expensive, :popular]
end
I've implemented a custom ORDER BY for each category with pagination (LIMIT 10), so when I'm getting products list, I'm getting multiple SQL queries with different ORDER BY in each query like this:
recent:
SELECT "products".* FROM "products" ORDER BY "products"."created_at" DESC LIMIT 10
cheapest:SELECT "products".* FROM "products" ORDER BY "products"."price" ASC LIMIT 10
most_expensive: SELECT "products".* FROM "products" ORDER BY "products"."price" DESC LIMIT 10
popular: SELECT "products".* FROM "products" ORDER BY "products"."popularity" DESC, "products"."created_at" DESC LIMIT 10
So as mentioned, each of the above queries, results a Product::ActiveRecord_Relation contains 10 products with different order for each query.
Question:
I've added new column to Product model which is featured with boolean value, and I need to apply ORDER BY featured DESC at the beginning of each query with keeping the other ORDER BY fields as it is(i.e. popular query should be like this SELECT "products".* FROM "products" ORDER BY "products"."featured" DESC, "products"."popularity" DESC, "products"."created_at" DESC LIMIT 10).
Note: ORDER BY featured DESC is just appended at the beginning of the previous ORDER BY statement, and it is applied on the subset not on the whole model.
What I have tried?
I have tried the following scenarios:
Add #products = #products.order(featured: :desc) in the controller but the result is not as expected because it adds the order to the end of existing order by chain.
Use default_scope in Product model default_scope { order(featured: :desc) } but the result is not as expected because it implements the order on the whole model, but the expected result is applying the order only on the subset(10 records).
using reorder in the controller #products = #products.reorder('').order(featured: :desc) but the result still not as expected because this remove the old order and actually I need to keep it but at the end of ORDER BY chain
The only solution I'm able to do is by using string variable to save previous order by chain, then use reorder('').order(featured: :desc) and finally append the string at the end of new ORDER BY:
current_order = #products.to_sql[#products.to_sql.downcase.index('order by')+8..#products.to_sql.downcase.index('limit')-1]
#products = #products.reorder("featured desc, #{current_order}" )
But I'm sure there is a better solution which I need your support to achieve it.
Summary:
As summarised in the comments below, I need the following implementation:
Given just r where r = M.order(:a), I want to run r.something(:b) and get the effect of M.order(:b).order(:a) rather than the M.order(:a).order(:b) that r.order(:b) would give you
Is there a reason you're not using scope chaining here? This seems like a perfect case to use it in. The use of enum is unclear, as well.
Something like this:
# /app/models/product.rb
class Product < ActiveRecord::Base
scope :recent, { order(created_at: :desc) }
scope :cheapest, { order(price: :asc) }
scope :most_expensive, { order(price: :desc) }
scope :popular, { order(popularity: :desc) }
scope :featured, { where(featured: true) }
end
Then in your controller you could do:
# /app/controllers/products_controller.rb
...
Product.featured.cheapest.limit(10)
Product.featured.most_expensive.limit(10)
...
and so on.
AREL should build the query correctly, and IIRC you can swap the sequence of the scopes (featured after cheapest, for example) if you want them to be applied differently.

Writing scope to complex multiple associations - Rails 4

I am having challenges writing a scope that displays live_socials created by users belonging to a category_managementgroup called client_group.
Social.rb
belongs_to :user
scope :live_socials, -> {where(['date >= ?', Date.current])}
CategoryManagementgroup.rb
has_many :users
User.rb
has_many :socials
belongs_to :category_managementgroup
the below scope displays all the socials for users in the category_managementgroup called client_group:
users.joins(:socials, :category_managementgroup).client_group.flat_map(&:socials)
i am unsure how to extend the scope to display the live_socials
(socials that have not expired). i tried the below but no success:
users.joins(:socials, :category_managementgroup).client_group.flat_map(&:socials).live_socials
i get the below error:
2.3.0 :264 > ap users.joins(:socials, :category_managementgroup).client_group.flat_map(&:socials).live_socials
User Load (0.5ms) SELECT "users".* FROM "users" INNER JOIN "socials" ON "socials"."user_id" = "users"."id" INNER JOIN "category_managementgroups" ON "category_managementgroups"."id" = "users"."category_managementgroup_id" WHERE "category_managementgroups"."name" = 'Client Group'
Social Load (0.1ms) SELECT "socials".* FROM "socials" WHERE "socials"."user_id" = ? [["user_id", 10]]
NoMethodError: undefined method `live_socials' for #<Array:0x007ff2f9834570>
Try applying live_socials scope before flat_map and add below scope to User model
scope :live_socials, -> { joins(:socials).where(['socials.date >= ?', Date.current])}

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

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.

Resources