CONTAIN or LIKE sql statement for ActiveRecord has_and_belongs_to_many relationship - ruby-on-rails

I have 2 ActiveRecords: Article and Tag, in a many to many relationship. Basically I want to know how to select with a CONTAINS or LIKE condition, ie. to define a condition on a many to many relationship to contain a specified subset within an array.
The code structure I am trying to work out is as follows:
tag_names = ["super", "awesome", "dope"]
tags = Tag.where("name IN (?)", tag_names)
# The following is my non-working code to illustrate
# what I'm trying to do:
articles = Article.where("tags CONTAINS (?)", tags)
articles = Article.joins(:tags).where("articles.tags CONTAINS (?)", tags)

If you have different tables then you have to use joins and then specify a condition on your join:
Article.joins(:tags).where('tags.id IN ?', tag_ids)
If you want more flexibility on you queries, you could also use Arel and write something like the following:
tags = Tag.arel_table
tags_ids = Tag.where(tags[:name].matches("%#{some_tag}%"))
Article.joins(:tags).where(tagss[:id].in(tags_ids))
You can read more about matches in this answer.
I prefer Arel conditions over pure String or even Hash conditions.

Related

How to get a most recent value group by year by using SQL

I have a Company model that has_many Statement.
class Company < ActiveRecord::Base
has_many :statements
end
I want to get statements that have most latest date field grouped by fiscal_year_end field.
I implemented the function like this:
c = Company.first
c.statements.to_a.group_by{|s| s.fiscal_year_end }.map{|k,v| v.max_by(&:date) }
It works ok, but if possible I want to use ActiveRecord query(SQL), so that I don't need to load unnecessary instance to memory.
How can I write it by using SQL?
select t.username, t.date, t.value
from MyTable t
inner join (
select username, max(date) as MaxDate
from MyTable
group by username
) tm on t.username = tm.username and t.date = tm.MaxDate
For these kinds of things, I find it helpful to get the raw SQL working first, and then translate it into ActiveRecord afterwards. It sounds like a textbook case of GROUP BY:
SELECT fiscal_year_end, MAX(date) AS max_date
FROM statements
WHERE company_id = 1
GROUP BY fiscal_year_end
Now you can express that in ActiveRecord like so:
c = Company.first
c.statements.
group(:fiscal_year_end).
order(nil). # might not be necessary, depending on your association and Rails version
select("fiscal_year_end, MAX(date) AS max_date")
The reason for order(nil) is to prevent ActiveRecord from adding ORDER BY id to the query. Rails 4+ does this automatically. Since you aren't grouping by id, it will cause the error you're seeing. You could also order(:fiscal_year_end) if that is what you want.
That will give you a bunch of Statement objects. They will be read-only, and every attribute will be nil except for fiscal_year_end and the magically-present new field max_date. These instances don't represent specific statements, but statement "groups" from your query. So you can do something like this:
- #statements_by_fiscal_year_end.each do |s|
%tr
%td= s.fiscal_year_end
%td= s.max_date
Note there is no n+1 query problem here, because you fetched everything you need in one query.
If you decide that you need more than just the max date, e.g. you want the whole statement with the latest date, then you should look at your options for the greatest n per group problem. For raw SQL I like LATERAL JOIN, but the easiest approach to use with ActiveRecord is DISTINCT ON.
Oh one more tip: For debugging weird errors, I find it helpful to confirm what SQL ActiveRecord is trying to use. You can use to_sql to get that:
c = Company.first
puts c.statements.
group(:fiscal_year_end).
select("fiscal_year_end, MAX(date) AS max_date").
to_sql
In that example, I'm leaving off order(nil) so you can see that ActiveRecord is adding an ORDER BY clause you don't want.
for example you want to get all statements by start of the months you should use this
#companey = Company.first
#statements = #companey.statements.find(:all, :order => 'due_at, id', :limit => 50)
then group them as you want
#monthly_statements = #statements.group_by { |statement| t.due_at.beginning_of_month }
Building upon Bharat's answer you can do this type of query in Rails using find_by_sql in this way:
Statement.find_by_sql ["Select t.* from statements t INNER JOIN (
SELECT fiscal_year_end, max(date) as MaxDate GROUP BY fiscal_year_end
) tm on t.fiscal_year_end = tm.fiscal_year_end AND
t.created_at = tm.MaxDate WHERE t.company_id = ?", company.id]
Note the last where part to make sure the statements belong to a specific company instance, and that this is called from the class. I haven't tested this with the array form, but I believe you can turn this into a scope and use it like this:
# In Statement model
scope :latest_from_fiscal_year, lambda |enterprise_id| {
find_by_sql[..., enterprise_id] # Query above
}
# Wherever you need these statements for a particular company
company = Company.find(params[:id])
latest_statements = Statement.latest_from_fiscal_year(company.id)
Note that if you somehow need all the latest statements for all companies then this most likely leave you with a N+1 queries problem. But that is a beast for another day.
Note: If anyone else has a way to have this query work on the association without using the last where part (company.statements.latest_from_year and such) let me know and I'll edit this, in my case in rails 3 it just pulled em from the whole table without filtering.

Combine multiple queries into one active record relation?

I am trying to write a search query for my app where based on the query string it will search for groups or users matching the string.
Here is what I have written:
def search_api
#groups = Group.where("name ILIKE '#{params[:query]}'")
#users = User.where("first_name ILIKE '#{params[:query]}' OR last_name ILIKE '#{params[:query]}")
end
Is there a way to combine these two queries into one activerecord relation array? Besides the brute force iterating over both arrays and putting them into one?
I am not sure about the solution but I have a security suggestion.
Your queries are not SQL Injection safe. You could pass array instead of injecting params in SQL string.
The following queries are SQL injection safe:
#groups = Group.where("name ILIKE ?", "#{params[:query]}")
#users = User.where("first_name ILIKE ? OR last_name ILIKE ?", "#{params[:query]}")
So here is a solution. Not sure that it is better than leaving everything as it is, but formally it's the answer to a question (besides that the result is not an ActiveRecord relation):
Group.connection.execute("(SELECT id, 'groups' as table FROM groups WHERE...) UNION (SELECT id, 'users' as table FROM users WHERE...)")
This returns an object of type PG::Result which you can treat as array of hashes. And, as it has already been said, it is good to pass arguments as an array instead of inserting them directly into SQL. Unfortunately, if You want to get a result as ActiveRecord, you may use UNION only for different queries to one table. In that case it looks like:
Group.find_by_sql("(SELECT * FROM groups WHERE...) UNION (SELECT * FROM groups WHERE...)")

Rails: finding records where attribute includes some value

I have a "Stores" model that contains various locations. Among the attributes for each store is the the "brands" that is carries.
Example: Store1, brands: "Nike, Adidas, Polo"; Store2, brands: "Jcrew, Polo"
I want to be able to select all stores where brand contains "Adidas" (may also contain other brands)
Something along the lines of:
#search = Stores.where(brands: params[:brand])
but need it to be
#search = Stores.where(brands.include? params[:brand])
which clearly doesn't work
What's the best way to deal with this?
If brands is a string and params[:brand] contains a single brand name, you can use MySQL's LIKE function:
#search = Stores.where(['BRANDS LIKE ?', "%#{params[:brand]}%"])
You can do this with the following statement.
#search = Stores.where("brands = ?", params[:brand])
A similar example is given in Listing 11.43 of the Hardtl rails tutorial
You should also note that rails models generally are meant to have singular names, i.e. Store instead of Stores.

rails getting attributes of associated model for many objects in one query

My title might be confusing, I wasn't sure what to write.
In rails I understand how to fetch Many Objects for One parent object
#first_user = User.first
#first_user_posts = #first_user.posts
But how can I fetch Many Objects for Many parent objects and select its attributes in one query?. I am trying to do something like that:
#many_posts = Post.all
#posts_by_user_gender = #many_posts.joins(:user).map(&:gender)
hoping it would give me an array that could look something like this:
#posts_by_user_gender => ["male", nil, "female", nil]
#I know I can do this map technique if I fetch it directly from the User model
# User.all.map(&:gender),
# but I want to start with those that posted in a specific category
# Post.where(:category_id => 1)
and then to count the males I could use the Ruby Array method .count
#males_count_who_posted = #posts_by_user_gender.count("male")
=> 1
I could always do 3 separate queries
#males_count_who_posted = #many_posts.select(:user_id).joins(:user)
.where("gender = ?", "male").count
#females_count_who_posted = ...
but I find that extremely inefficient, especially if I do the same for something like "industry" where you could have more than 3 options.
you can join model via SQL syntax
#posts_by_user_gender = #many_posts.joins("LEFT JOIN Users where users.id=posts.user_id").joins("LEFT JOIN Genders where genders.id=user.gender_id")

Rails ActiveRecord Join

I'm using rails and am trying to figure out how to use ActiveRecord within the method to combine the following into one query:
def children_active(segment)
parent_id = Category.select('id').where('segment' => segment)
Category.where('parent_id'=>parent_id, 'active' => true)
end
Basically, I'm trying to get sub categories of a category that is designated by a unique column called segment. Right now, I'm getting the id of the category in the first query, and then using that value for the parent_id in the second query. I've been trying to figure out how to use AR to do a join so that it can be accomplished in just one query.
You can use self join with a alias table name:
Category.joins("LEFT OUTER JOIN categories AS segment_categories on segment_categories.id = categories.parent_id").where("segment_categories.segment = ?", segment).where("categories.active = ?", true)
This may looks not so cool, but it can implement the query in one line, and there will be much less performance loss than your solution when data collection is big, because "INCLUDE IN" is much more slower than "JOIN".

Resources