Suppose we have a Tag model with many associated Post (or none) via a has_many association.
Is there an efficient way to select only the tags that do have a tag.posts.size > 0 via a scope ?
Something that would behave as follows:
scope :do_have_posts, -> { where("self.posts.count > 0") } #pseudo-code
Thanks.
This should only return you the tags with posts since rails does an inner join by default
scope :with_posts, -> { joins(:posts).uniq }
Related
I have a model Deployment, it has following relations:
belongs_to :user
belongs_to :user_group, optional: true
I want to be able to define scope for easier filtering. I can easily filter by user, something like this:
scope :by_owner, -> (ow) { joins(:user).where("users.name like ?", "%#{ow}%") }
But what I really want to use user_group first if association exists and if it doesn't, then fall back on user. Logic would work like this:
If this deployment belongs to at least one user_group
scope :by_owner, -> (ow) { joins(:user_group).where("user_groups.name like ?", "%#{ow}%") }
else
scope :by_owner, -> (ow) { joins(:user).where("users.name like ?", "%#{ow}%") }
end
I am guessing that I have to achieve it somehow with an SQL query, but I can't seem to figure it out. Any suggestions?
Thanks!
Update
I got a bit closer with the following query:
scope :by_owner, -> (ow) { joins(:user, :user_group).where("(users.display_name like ?) OR (user_groups.name like ?)", "%#{ow}%", "%#{ow}%") }
But it's still not it because 1st of all the records that don't have user_groups association are ignored in the response and also it searches by user even if user_group is defined. Still looking for suggestions if there are any.
If you need to pull all duplicates by name in the class you can achieve it by:
Company.select(:name).group(:name).having("count(*) > 1")
By what to do if you want it in the scope
scope :duplicates, -> { where (...?)}
Also in return I need few fields not only name. Did anyone had the same problem to create a scope?
You need to run this in two queries. The first query selects the duplicate names, the second one selects the records with those duplicate names and uses the current_scope so that it can be chained with more scopes if needed (unfortunately current_scope seems to be a very useful but undocumented method):
scope :duplicates,
-> {
dup_names = Company.group(:name).having("count(*) > 1").pluck(:name)
current_scope.where(name: dup_names)
}
(The dup_names variable will contain an array of duplicate names found among the companies.)
Then you can easily add further conditions on the duplicate records, for example:
Company.duplicates.where("name like 'a%'").limit(2)
will select just two companies with the name starting with 'a' (and with duplicate names).
Since
scope :red, -> { where(color: 'red') }
is simply 'syntactic sugar' for defining an actual class method:
class Shirt < ActiveRecord::Base
def self.red
where(color: 'red')
end
end
you could define scope like this:
scope :duplicates, -> { ids = select(:id).group(:name).having("count(name) > 1"); where(id: ids) }
I am building out a tag-based forum in Rails 4, where topics can be associated with tags.
class Topic < ActiveRecord::Base
...
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
...
scope :sort_by_latest_message, -> { order(latest_message_at: :desc) }
scope :sort_by_sticky, -> { order(sticky: :desc) }
...
scope :without_tags, -> { where.not(id: Tagging.select(:topic_id).uniq) }
scope :with_tags, -> { joins(:tags).where("tag_id IS NOT NULL").uniq }
...
end
Topics also have a boolean column called "sticky."
The issue: I want to be able to have topics to sort in a manner that places topics with the sticky property at the top of the list, but only if that topic has an association with at least one of a specified list of tags. Topics will then be sorted by the latest_message_at property.
This all occurs AFTER a filtering process.
So for example, the list of topics will contain topics with tags X, Y, and Z, but only sticky topics with tag X should truly be considered sticky, so any topics that have the sticky property but are associated with tags Y or Z instead of tag X should be sorted normally (by latest message). So ultimately, the list will have sticky topics under tag X at the top (sorted by latest message), then all other topics under tag X plus topics under tag Y and Z whether they are sticky or not, sorted by the latest_message_at property.
I currently have a setup like this:
def self.combine_sort_with_sticky(tag_ids, primary_sort)
if tag_ids.empty?
relevent_sticky_topics = without_tags.where(sticky: true)
other_topics = union_scope(*[with_tags, without_tags.where(sticky: false)]) # union_scope is a method that creates an SQL union based on the scopes within
else
relevent_sticky_topics = joins(:tags).where("tag_id IN (?)", tag_ids).uniq.where(sticky: true)
other_topics = joins(:tags).where("tag_id NOT IN (?) OR sticky = ?", tag_ids, false).uniq
end
combined_topics = relevent_sticky_topics.send(primary_sort) + other_topics.send(primary_sort) # Order is important, otherwise stickies will be at the bottom.
combined_topics.uniq
end
So when I call combine_sort_with_sticky([1], :sort_by_latest_message), only sticky topics with the tag of ID 1 AND the sticky property are moved to the front of the list. I'll also note that when not filtering on any tag, only topics without tags should be considered sticky.
This appears to give the results I want, but that + operator between the two sorted queries has me concerned, as it converts the ActiveRecord association to an Array object.
What I am looking for is a way to maintain the ActiveRecord association (such as a scope, or potentially another class model) while conditionally applying the first of the two sorting scopes. Topic.all.sort_by_sticky.sort_by_latest_message is close to what I want, but the problem is that it indiscriminately sorts by the sticky property, rather than only considering sticky topics with certain tags as true stickies.
I have been playing around with scopes like the following:
scope :sort_by_relevant_sticky, ->(tag_ids) { joins(:tags).order("CASE WHEN tag_id IN (?) THEN sticky ELSE latest_message_at END DESC", tag_ids).uniq }
but that doesn't seem to be working. I am not incredibly familiar with conditional SQL.
My database in Production is Postgresql.
This is not an actual solution for your problem, but I'd take a look at this: https://github.com/mbleigh/acts-as-taggable-on :)
I’m currently setting up a scope in my Rails model to be used by ActiveAdmin. The scope I want to build should find every Job that has a survey_date in the past, with a Job.survey present, and no Job.quotes present.
Here is an abbreviated version of my Job model:
has_many :quotes
has_many :surveys
scope :awaiting_quote, lambda { joins(:surveys, :quotes).where('survey_date < :current_time AND surveys.id IS NOT NULL AND quotes.id IS NULL', { current_time: Time.current }) }
How should I change my scope so that it correctly finds the revelant Job records?
Update for Rails 5
As mad_raz mentions, in Rails 5.0+, you can use left_outer_joins:
scope :awaiting_quote, -> { joins(:surveys).
left_outer_joins(:quotes).
where('survey_date < :current_time', { current_time: Time.current }).
where('quotes.id IS NULL')
}
However, you must still provide a where('quotes.id IS NULL') check to only return those records that have not yet received a quote. See https://stackoverflow.com/a/16598900/1951290 for a great visual representation of outer joins.
It still probably makes the most sense to split these into two separate scopes.
Rails 4
You can create left outer joins using joins, you just have to be a bit more explicit.
scope :awaiting_quote, -> { joins(:surveys).
joins('LEFT OUTER JOIN quotes ON quotes.job_id = jobs.id').
where('survey_date < :current_time', { current_time: Time.current }).
where('quotes.id IS NULL')
}
You don't need surveys.id IS NOT NULL since a successful inner join will not include nil ids.
It probably makes more sense to split these into two separate scopes, :has_survey and :without_quote, which can then be combined into a method.
def self.awaiting_quote
Job.has_survey.without_quote
end
Rails 5 introduced left_outer_joins method that can be used
scope :awaiting_quote, -> { joins(:surveys).left_outer_joins(:quotes).where('yada yada') }
I have Users, Venues, and VenueManagers.
Given a user (current_user), I need to return the venue where this user is a venue_manager.
What's tricky is a venue can have multiple venue managers, and I'm running into the problem where the returned venues will only contain the one venue manager that's the current_user, and not contain the others.
Specifically, this is for a scope.
class Venue < ActiveRecord::Base
has_and_belongs_to_many :venue_managers
scope :for_venue_manager, -> (user) { includes(:venue_managers).where('venues_venue_managers.user_id = ?', user) }
end
That scope does not return all the venue managers unfortunately. Any input is kindly appreciated.
In order for a join to actually filter, it needs to be a joins and not an includes. So, first of all, you'll need to do
scope :for_venue_manager, -> (user) {
joins(:venue_managers).where('venues_venue_managers.user_id = ?', user) }
This will give you the venues where this user is one of the managers. To have only the ones that have only this user as the manager, you can then filter by those who only have 1 manager:
scope :for_venue_manager, -> (user) {
joins(:venue_managers).where('venues_venue_managers.user_id = ?', user).
group(:venue_id).having('count(user_id) = 1') }
Or, in rails
scope :for_venue_manager, -> (user) {
joins(:venue_managers).where('venues_venue_managers.user_id = ?', user).
filter {|v| v.venue_managers.length == 1 } }