Selecting in rails with an array and a element-related-condition - sql

I have an array of products ids, which I would like to use for filtering a list of buying sessions. So far so good:
Session.where('product_id': [23, 32, 17])
I also have an array of dates, which specify for each product (differently) from which date should I even consider their sessions: ['12/2/17', '1/1/17', '20/3/17']
So in other words: product id 23, should only be considered for sessions created after 12/2/17, product id 32, should only be considered for sessions created after 1/1/17, and so on...
What is the right rails command to also consider the product dates when selecting sessions about the wanted product ids?

Something like this, chaining your where clauses to your needs.
Session.where('product_id = ? AND date > ?', 23, Time.new(2017, 2, 12))
.or(Session.where('product_id = ? AND date > ?', 32, Time.new(2017, 1, 17)))

Related

Active Record query to match every subset element

In my RoR application, I've got a database lookup similar to this one:
Client.joins(:products).where({'product.id' => [1,2,3]})
Unfortunately this will return all clients that have bought product 1, 2 or 3 but I only want to get back the clients, that bought all of the three products. In other words, I'd like to write a query that matches for n elements in a given set.
Are there any elegant solutions for this?
This is not really elegant. But it should translate into the needed SQL.
Client.joins(:products).
where({'products.id' => [1,2,3]}).
group('users.id').
having('COUNT(DISTINCT products.id) >= 3')
Same answer with more dynamic way
ids = [1,2,3]
Client.joins(:products).
where({'products.id' => ids}).
group('users.id').
having('COUNT(DISTINCT products.id) >= ?', ids.size)

Query that finds objects with ALL association ids (Rails 4)

BACKGROUND: Posts have many Communities through CommunityPosts. I understand the following query returns posts associated with ANY ONE of these community_ids.
Post.joins(:communities).where(communities: { id: [1,2,3] })
OBJECTIVE: I'd like to query for posts associated with ALL THREE community_ids in the array. Posts having communities 1, 2, and 3 as associations
EDIT: Please assume that length of the array is unknown. Used this array for explanation purposes.
Try this,
ids=[...]
Post.joins(:communities).select(“count(communities.id) AS cnt”).where(id: ids).group(‘post.id’).having(cnt: ids.size)
ids = [1, 2, 3] # and etc
Post.joins(:communities).where("communities.id IN ?", ids)
Wish it helps .

Find all records which have a count of an association greater than zero

I'm trying to do something that I thought it would be simple but it seems not to be.
I have a project model that has many vacancies.
class Project < ActiveRecord::Base
has_many :vacancies, :dependent => :destroy
end
I want to get all the projects that have at least 1 vacancy.
I tried something like this:
Project.joins(:vacancies).where('count(vacancies) > 0')
but it says
SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0).
1) To get Projects with at least 1 vacancy:
Project.joins(:vacancies).group('projects.id')
2) To get Projects with more than 1 vacancy:
Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')
3) Or, if Vacancy model sets counter cache:
belongs_to :project, counter_cache: true
then this will work, too:
Project.where('vacancies_count > ?', 1)
Inflection rule for vacancy may need to be specified manually?
joins uses an inner join by default so using Project.joins(:vacancies) will in effect only return projects that have an associated vacancy.
UPDATE:
As pointed out by #mackskatz in the comment, without a group clause, the code above will return duplicate projects for projects with more than one vacancies. To remove the duplicates, use
Project.joins(:vacancies).group('projects.id')
UPDATE:
As pointed out by #Tolsee, you can also use distinct.
Project.joins(:vacancies).distinct
As an example
[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""
Yeah, vacancies is not a field in the join. I believe you want:
Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
# None:
Project.left_joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')
Performing an inner join to the has_many table combined with a group or uniq is potentially very inefficient, and in SQL this would be better implemented as a semi-join that uses EXISTS with a correlated subquery.
This allows the query optimiser to probe the vacancies table to check for the existence of a row with the correct project_id. It doesn't matter whether there is one row or a million that have that project_id.
That's not as straightforward in Rails, but can be achieved with:
Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)
Similarly, find all projects that have no vacancies:
Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)
Edit: in recent Rails versions you get a deprecation warning telling you to not to rely on exists being delegated to arel. Fix this with:
Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)
Edit: if you're uncomfortable with raw SQL, try:
Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)
You can make this less messy by adding class methods to hide the use of arel_table, for example:
class Project
def self.id_column
arel_table[:id]
end
end
... so ...
Project.where.not(
Vacancies.where(
Vacancy.project_id_column.eq(Project.id_column)
).arel.exists
)
In Rails 4+, you can also use includes or eager_load to get the same answer:
Project.includes(:vacancies).references(:vacancies).
where.not(vacancies: {id: nil})
Project.eager_load(:vacancies).where.not(vacancies: {id: nil})
I think there's a simpler solution:
Project.joins(:vacancies).distinct
Without much Rails magic, you can do:
Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')
This type of conditions will work in all Rails versions as much of the work is done directly on the DB side. Plus, chaining .count method will work nicely too. I've been burned by queries like Project.joins(:vacancies) before. Of course, there are pros and cons as it's not DB agnostic.
You can also use EXISTS with SELECT 1 rather than selecting all the columns from the vacancies table:
Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")
If I want to know how many records have at least one of an associated record, I would do:
Project.joins(:vacancies).uniq.count
The error is telling you that vacancies is not a column in projects, basically.
This should work
Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')

AND multiple JOIN statements in Rails

I have a table for users and roles. I'm using a has many through relationship. I am trying to create a query that will find users that have all of the roles in an array.
ex.
role_ids = [2, 4, 6]
User.filter(role_ids) would return all users that have roles with ids 2, 4, 6.
This is what I have so far.
def self.filter(role_ids)
results = User.joins(:roles).where(roles: {id: role_ids} )
end
The problem with this statement is it returns all users who have at least one of the roles in role_ids.
How do I make this statement give me an intersection, not a union?
I think you are asking for only unique instances of users who meet the role filter criteria. If so, then this should work.
def self.filter(role_ids)
results = User.joins(:roles).where(roles: {id: role_ids} ).uniq
end

Can you select unique by count in Rails Active Record?

If you can't do it, how would SQL do it?
Basically I want to select all my question objects where there is at least two with the same attribute. That attribute is called, lets say, word_id.
So how would I only select all the objects that that share only once a common attribute with another object?
If I have three objects :
# Question(id: 1, word_id: 1)
# Question(id: 2, word_id: 2)
# Question(id: 3, word_id: 2)
# Question(id: 4, word_id: 1)
# Question(id: 5, word_id: 1)
# Question(id: 6, word_id: 1)
I would want to return just id's 2 and 3 since they both share a common attribute twice.
Is that possible? I crudely do this by making two calls to the DB where first I call all the objects in question, add them to an array, and subtract from that array objects that match my requirements. I was just curious if there was a more elegant way to do it all at once.
Just SQL:
SELECT * FROM questions WHERE world_id IN (
SELECT world_id FROM questions GROUP BY world_id HAVING count(*) = 2
)
Rails:
Question.where("world_id IN (?)", Question.find(:all, select: "world_id",
group: "world_id HAVING count(*) = 2"))
I guess that's still two queries though...