match all in active record relations in a query - sql

I need an activerecord query to Match ALL items in a params array.
Lets say user has_many roles. and each role has a name.
when i pass ['actor', 'producer', 'singer']. I expect the query to return me the users with all the those three roles or more.
But my method implementation below would return users with having atleast one role name matching to those in the array passed
My current method gives results based on finding any of the tags, not "MATCH ALL"
class User < ActiveRecord::Base
has_many :roles
def self.filter_by_roles(roles)
User.joins(:roles).includes(:roles).where(:roles => {:name => roles})
end
end
I don't want to do any array operations after the query checking if the returned result objects contain all the roles or not. This is because I need the Active Record Relation object to be returned from this.
Thanks in advance.

Try this.
User.joins(:roles).includes(:roles).where(:roles => {:name => roles}).group('usermail').having("COUNT(DISTINCt role_id) = 3")
assuming that field usermail is using to identify users.

You could try this:
def self.filter_by_roles(roles)
scope = User.joins(:roles).includes(:roles)
roles.each do |role|
scope = scope.where(roles: {name: role})
end
scope
end
It's untested, so I'm not sure whether it works.

If you pass role_ids array ([1,2,3]) you can do smth like this:
def self.filter_by_roles(role_ids)
User.select{|user| (role_ids - user.role_ids).empty?}
end
But if you pass roles by title (['actor', 'producer', 'singer']) you need smth like this:
def self.filter_by_roles(roles)
role_ids = Role.find_all_by_title(roles).map(&:id)
User.select{|user| (role_ids - user.role_ids).empty?}
end

Related

Add Arbitrary Attribute to SQL Query from Joins Record without WHERE clause (Active record)

I'm trying to create an attribute in my select statement that depends on whether or not an association exists. I'm not sure if it's possible with a single query, and the goal is to not have to iterate a list afterward.
Here is the structure.
class Project < ApplicationRecord
has_many :subscriptions
has_many :users, through: :subscriptions
end
class User < ApplicationRecord
has_many :subscriptions
has_many :projects, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :project
belongs_to :user
end
Knowing a project, the goal of the query is to return ALL users and include on them a new attribute call subscribed - denoting whether or not they are subscribed.
non-working code (pseudo code):
project = Project.find_by(name: 'has_subscribers')
query = 'users.*, (subscriptions.project_id = ?) AS subscribed'
users = User.includes(:subscriptions).select(query, project.id)
user.first.subscribed
# => true or false
I'm open to whether or not there is a better way of going about this. However, the information is:
You know the project record.
You query a list of ALL users
Each user record has a subscribed attribute, denoting whether its
subscribed to the given project
Solution:
I was able to figure out a straight forward solution using the bool_or aggregate method. Coalesce ensures that the value returned is false instead of nil, should no subscriptions exists.
query = "users.*, COALESCE(bool_or(subscriptions.project_id = '#{project_id}'::uuid), false) as subscribed"
User.left_outer_joins(:subscriptions)
.select(query)
.group('users.id')
Yep, you can do this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id])
Which will result in a SQL query like this:
SELECT *, "subscriptions"."project_id" FROM "users" INNER JOIN "subscriptions" ON "subscriptions"."user_ud" = "users"."id";
If you want to specify a specific project (i.e. use an expression), you can do it with Arel like this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42))
Unfortunately, you won't have a column name alias, and you can't call as on an Arel::Nodes::Equality instance. I don't know enough about the internals of Arel to have a way out of that box. But you can do this if you want the composability of Arel (e.g. if this is going to be something that needs to work with multiple models or columns):
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42).to_sql + " as has_project")
This is a bit clunky, but it works and provides a user.has_project method that returns a boolean. You can pretty it up like so:
class User
scope :with_project_status, lambda do |project_id|
has_project =
Subscription.arel_table[:project_id].
eq(project_id).to_sql + " as has_project"
joins(:projects).select(Arel.star, has_project)
end
end
User.with_project_status(42).where(active: true)

Paginating joined results with calculated columns

We are calculating statistics for our client. Statistics are calculated for each SpecialtyLevel, and each statistic can have a number of error flags (not to be confused with validation errors). Here are the relationships (all the classes below are nested inside multiple modules, which I have omitted here for simplicity):
class SpecialtyLevel < ActiveRecord::Base
has_many :stats,
:class_name =>"Specialties::Aggregate::Stat",
:foreign_key => "specialty_level_id"
.......
end
class Stat < Surveys::Stat
belongs_to :specialty_level
has_many :stat_flags,
:class_name => "Surveys::PhysicianCompensation::Stats::Specialties::Aggregate::StatFlag",
:foreign_key => "stat_id"
......
end
class StatFlag < Surveys::Stats::StatFlag
belongs_to :stat, :class_name => "Surveys::PhysicianCompensation::Stats::Specialties::Aggregate::Stat"
......
end
In the view, we display one row for each SpecialtyLevel, with one column for each Stat and another column indicating whether or not there are any error flags for that SpecialtyLevel. The client wants to be able to sort the table by the number of error flags. To achieve this, I've created a scope in the SpecialtyLevel class:
scope :with_flag_counts,
select("#{self.columns_with_table_name.join(', ')}, count(stat_flags.id) as stat_flags_count").
joins("INNER JOIN #{Specialties::Aggregate::Stat.table_name} stats on stats.specialty_level_id = #{self.table_name}.id
LEFT OUTER JOIN #{Specialties::Aggregate::StatFlag.table_name} stat_flags on stat_flags.stat_id = stats.id"
).
group(self.columns_with_table_name.join(', '))
Now each row returned from the database will have a stat_flags_count field that I can sort by. This works fine, but I run into a problem when I try to paginate using this code:
def always_show_results_count_will_paginate objects, options = {}
if objects.total_entries <= objects.per_page
content_tag(:div, content_tag(:span, "Showing 0-#{objects.total_entries} of #{objects.total_entries}", :class => 'info-text'))
else
sc_will_paginate objects, options = {}
end
end
For some reason, objects.total_entries returns 1. It seems that something in my scope causes Rails to do some really funky stuff with the result set that it gives me.
The question is, is there another method I can use to return the correct value? Or is there a way that I can adjust my scope to prevent this meddling from occurring?
The group statement makes me suspicious. You may want to fire up a debugger and step through the code and see what's actually getting returned.
Is there a special reason you're using a scope and not just an attribute on the SpecialtyLevel model? Couldn't you just add a def on SpecialtyLevel that would function as a "virtual attribute" that just returns the length of the list of StatFlags?
The answer here is to calculate total_entries separately and pass that into the paginate method, for example:
count = SpecialtyLevel.for_participant(#participant).count
#models = SpecialtyLevel.
with_flag_counts.
for_participant(#participant).
paginate(:per_page => 10, :page => page, :total_entries => count)

Using includes with AREL functions

I have the following classes in my application:
class Prompt
has_many :entries
end
class Entry
belongs_to :prompt
belongs_to :user
def self.approved
where("is_approved")
end
end
class User
has_many :entries
end
And I want to display a table of all "approved" entries for a given prompt and the users that they belong_to. To generate this list I do the following query:
prompt = Prompt.find(prompt_id, :include => {:entries => :user})
But when I run the following loop, it makes a query for each user rather than using the prefetched users
prompt.entries.approved.each do |entry|
puts entry.user.id
end
How do I rewrite this so that it doesn't do a query for each iteration of the loop?
It does a query for each user because entries.approved is calling the query *where('is_approved')* for each entry. Your find statement is merely pulling all of the prompts and creating objects with access to their child attributes. I think what you need is a where statement that selects all of the entries that have the attribute 'is_approved' and then run through a loop printing their ids.
Maybe try something like #entries = Entry.where(is_approved: true).includes(entries: user) )
Then
entries.user.each do |user|
puts user.id
end
Hope thats helps.

Rails and SQL, how to .last on all members of a table's associations

I am attempting to form the proper query with limited success so far.
Say I have Users and posts
Users has_many posts and posts belongs to user
posts has a visible boolean field
I am trying to write a scope in the user model that goes through all users, finds their last post, and only returns the user if the value if the visible boolean is true on their last post.
Something like Users.posts.last.where(:visible => true) but in as a scope.
EDIT
I'm trying to keep the return value as an AR relation object, a regular array will not work for what I am trying to use the scope for. Thanks though so far for the input.
maybe something like this should work.. (non-tested)
class User
def User.users_with_visible_posts
User.includes(:posts).map {|user| user.posts.last && !!user.posts.last.visible}
end
end
User.users_with_visible_posts #=> [Users with last post visible]
class User < ActiveRecord::Base
has_many :posts
scope :last_visable_post, posts.where("visible = ?",true).last
end

Strange behavior of references in MongoDB

I am using Rails 3 with Mongoid.
I have two documents:
class MyUser
include Mongoid::Document
field ......
references_many :statuses, :class_name => "MyStatus"
end
class MyStatus
include Mongoid::Document
field ......
referenced_in :user, :class_name => "MyUser"
end
The problem is, I can get the user of any given status, but I cannot get the list of statuses from a user!
ie.
status = MyStatus.first
status.user # the output is correct here
user = MyUser.first
user.statuses # this one outputs [] instead of the list of statuses...
Please tell me what have I done wrong? I am just a few days with mongo......
Your code looks correct to me.
Are you sure that MyStatus.first.user == MyUser.first ?
It's possible that you have multiple users in your db.. where the first user has no statuses, and the second user has status1 in his list.
To test this, try doing:
status = MyStatus.first
user = status.user
user.statuses # Should return at least one status