Writing scope to complex multiple associations - Rails 4 - ruby-on-rails

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])}

Related

Combining a 'where' clause with a method

In my app, a Team belongs_to :hunt. Once a Hunt is confirmed, then all associated teams are ready.
This a sample from my team.rb file, where I use the method ready? to check if team.hunt is confirmed.
#team.rb
def ready?
hunt.confirmed? ? true : false
end
I would love to have a scope in team.rb file so I can call Teams.all.ready.count to show the number of teams that are ready.
How can I write a method or a scope to achieve the behaviour above without adding anything to my DB or iterating through an array etc?
Updated:
Thanks to #TomLord's insight, you'd rather do Solution 1 below instead of Solution 2. Also, added sample SQL to show comparison.
Solution 1
class Team < ApplicationRecord
belongs_to :hunt
scope :ready, -> { joins(:hunt).where(hunts: { confirmed: true }) }
end
Usage:
Team.ready # or: Team.all.ready
# SELECT "teams".* FROM "teams" INNER JOIN "hunts" ON "hunts"."id" = "teams"."hunt_id" WHERE "hunts"."confirmed" = ? LIMIT ? [["confirmed", "t"], ["LIMIT", 11]]
Or, Solution 2
class Team < ApplicationRecord
belongs_to :hunt
end
class Hunt < ApplicationRecord
scope :confirmed, -> { where(confirmed: true) }
end
Usage:
# you can also move the logic below as a method/scope inside `Team` model (i.e. as `ready` method/scope)
# Example 1 (using JOINS):
Team.joins(:hunt).where(hunts: { id: Hunt.confirmed })
# SELECT "teams".* FROM "teams" INNER JOIN "hunts" ON "hunts"."id" = "teams"."hunt_id" WHERE "hunts"."id" IN (SELECT "hunts"."id" FROM "hunts" WHERE "hunts"."confirmed" = ?) LIMIT ? [["confirmed", "t"], ["LIMIT", 11]]
# Example 2 (same as Example 1 above but faster and more efficient):
Team.where(hunt_id: Hunt.confirmed)
# SELECT "teams".* FROM "teams" WHERE "teams"."hunt_id" IN (SELECT "hunts"."id" FROM "hunts" WHERE "hunts"."confirmed" = ?) LIMIT ? [["confirmed", "t"], ["LIMIT", 11]]
Solution: if Hunts#confirmed is a database column:
class Team < ApplicationRecord
belongs_to :hunt
scope :ready, -> { joins(:hunt).where(hunts: { confirmed: true }) }
end
In this case, Team.ready will return ActiveRecord::Relation.
Solution: If Hunts#confirmed? is a NOT a database column:
class Team < ApplicationRecord
belongs_to :hunt
scope :ready, -> { includes(:hunts).select(&:ready?) }
end
In this case, Team.ready will return an Array
You need to be aware that the second solution is looping over the Team records calling ready? on each of them while the first is performing a database query. First is more efficient.

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.

Rails 3 ActiveRecord#includes bug?

I have the following models
class Account < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
attr_accessible :email
has_many :accounts
end
As far as i know, in order to select all accounts that for all users with email match a pattern, we must do a join
Account.joins(:user).where("users.email ILIKE ?", '%pattern%') -> work
And here comes the magic, replacing includes with joins, still work like a charm
Account.includes(:user).where("users.email ILIKE ?", '%pattern%') -> work
But
Account.includes(:user).where("users.email ILIKE ?", '%pattern%').count -> error
Any explanation ? isn't includes just for Eager Loading only ?
Because without explicit reference includes loads relation in a separate query. Take a look at rails console:
[11] pry(main)> Account.includes(:user)
AccountsUser Load (4.6ms) SELECT "accounts".* FROM "accounts"
User Load (11.7ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (...
[11] pry(main)> Account.includes(:user).where("users.email ILIKE ?", '%pattern%').to_sql
SELECT "accounts".* FROM "accounts" WHERE (users.email ILIKE '%admin%')
That is why you are getting an error - users table is not referenced in a query. To reference users table use either references or eager_load:
Account.includes(:user).references(:users).where("users.email ILIKE ?", '%pattern%').count
or
Account.eager_load(:user).where("users.email ILIKE ?", '%pattern%').count
Note that includes works with association names while references needs the actual table name

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 Scoping for Existence

I am running Rails 4.0. Ruby 2.0
For signing up, I only ask users to provide me with there email in the new page.
In the update action, I then ask users for their name.
When listing users in the Index action, I only want to show users have updated their name.
I know I need to scope based on if users have updated their name.
User Model
scope :name, where(name: true)
User Controller
def index
#users = User.name
end
Error
undefined method `gsub' for #
I Think the issue is the way, I am calling the scope. I might need to use exists?
Any help is greatly, appreciated. Thank you.
Personally, just to be a little more conventional, I would use
class User < ActiveRecord::Base
scope :has_name, where("users.name != ''")
end
This way, when your model gets joined with another, you won't introduce a column ambiguity in the event multiple tables have a name column
Consider this example
$ rails new app
$ cd app
$ rails g resource User name:string company_id:integer
$ rails g resource Company name:string
$ rake db:migrate
Our models
class User < ActiveRecord::Base
belongs_to :company
scope :has_name, where("name != ''")
end
class Company < ActiveRecord::Base
has_many :users
end
A problem
$ rails c
irb> User.has_name.join(:company)
Oh noes!
User Load (0.4ms) SELECT "users".* FROM "users" INNER JOIN
"companies" ON "companies"."id" = "users"."company_id" WHERE
(name != '')
SQLite3::SQLException: ambiguous column name: name: SELECT
"users".* FROM "users" INNER JOIN "companies" ON
"companies"."id" = "users"."company_id" WHERE (name != '')
ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous
column name: name: SELECT "users".* FROM "users" INNER JOIN
"companies" ON "companies"."id" = "users"."company_id" WHERE
(name != '')
Let's fix the scope
class User < ActiveRecord::Base
belongs_to :company
scope :has_name, where("users.name != ''")
end
Re-run our query
irb> reload!
irb> User.has_name.join(:company)
Proper output
User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "companies"
ON "companies"."id" = "users"."company_id" WHERE (users.name != '')
=> []
You could use:
scope :with_name, where("name <> ''")
Though the above doesn't tell you if they've actually modified their name, just that it isn't blank. If you wanted to track the name column for changes, you could use something like the PaperTrail gem for this.
Based on additional feedback, I'd recommend:
scope :with_name, where("users.name != ''")

Resources