Ruby on Rails: Subquerying an array of found objects' relational objects? - ruby-on-rails

I have a model Profile that has a onetoone with the model Interest. If I query an array of Profiles using where, how would I then get another separate array composed of the Interests associated with those originally queried Profiles?
Something like
#foundProfiles = Profile.where(field: data)
into
#foundInterests = #foundProfiles.Interest.all
this doesn't work, but that's the idea I'm trying to get at.

Make use of association:
#foundProfiles = Profile.includes(:interest).where(field: data)
# Eager load interest to avoid n + 1 queries
#foundInterests = #foundProfiles.map(&:interest)
EDIT
If you need to query further on the Interest records you can do something like:
#foundInterests = Interest.where(profile_id: #foundProfiles.map(&:id))
This will return you Interest records associated with #foundProfiles and you can chain where on it

Related

How to sanitise multiple variables into SQL query ActiveRecord Rails

In our application, the Recipe model has many ingredients (many-to-many relationship implemented using :through). There is a query to return all the recipes where at least one ingredient from the list is contained (using ILIKE or SIMILAR TO clause). I would like to pose two questions:
What is the cleanest way to write the query which will return this in Rails 6 with ActiveRecord. Here is what we ended up with
ingredients_clause = '%(' + params[:ingredients].map { |i| i.downcase }.join("|") + ')%'
recipes = recipes.where("LOWER(ingredients.name) SIMILAR TO ?", ingredients_clause)
Note that recipes is already created before this point.
However, this is a bit dirty solution.
I also tried to use ILIKE = any(array['ing1', 'ing2',..]) with the following:
ingredients_clause = params[:ingredients].map { |i| "'%#{i}%'" }.join(", ")
recipes = recipes.where("ingredients.name ILIKE ANY(ARRAY[?])", ingredients_clause)
This won't work since ? automatically adds single quotes so it would be
ILIKE ANY (ARRAY[''ing1', 'ing2', 'ing3'']) which is of course wrong.
Here, ? is used to sanitise parameters for SQL query, so avoid possible SQL injection attacks. That is why I don't want to write a plain query formed from params.
Is there any better way to do this?
What is the best approach to order results by the number of ingredients that are matched? For example, if I search for all recipes that contains ingredients ing1 and ing2 it should return those which contains both before those which contains only one ingredient.
Thanks in advance
For #1, a possible solution would be something like (assuming the ingredients table is already joined):
recipies = recipies.where(Ingredients.arel_table[:name].lower.matches_any(params[:ingredients]))
You can find more discussion on this kind of topic here: Case-insensitive search in Rails model
You can access a lot of great SQL query features via #arel_table.
#2 If we assume all the where clauses are applied to recipies already:
recipies = recipies
.group("recipies.id")
# Lets Rails know you meant to put a raw SQL expression here
.order(Arel.sql("count(*) DESC"))

Ruby on Rails / ActiveRecord: How Can I (Elegantly) Retrieve Data from Multiple Tables?

It's rather trivial to retrieve data from multiple tables that are related through foreign keys using raw SQL. I can do, for example:
SELECT title, domestic_sales
FROM movies
JOIN boxoffice
ON movies.id = boxoffice.movie_id;
This would give me a table with two colums: title and domestic_sales, where the data in the first column comes from the table movies and the data in the second column comes from the table boxoffice.
How can I do this in Rails using Ruby code? I can, of course, get the same result if I use raw SQL. So, I could do the following:
ActiveRecord::Base.connection.execute(<<-SQL)
SELECT title, domestic_sales
FROM movies
JOIN boxoffice
ON movies.id = boxoffice.movie_id;
SQL
This would give me a PG::Result object with the data I want. But this is super inelegant. I would like to be able to get this information without using raw SQL.
So, this is the first thing that comes to mind is:
Movie.select(:name, :domestic_sales).joins(:box_office)
The problem, however, is that the aforementioned line of code returns a bunch of Movie objects. Since the Movie class doesn't have the domestic_sales attribute, I don't get access to that information.
The next thing I thought was to use a loop. So, I could do something like:
Movie.joins(:box_office).to_a.map do |m|
{name: m.name, rating: m.box_office.domestic_sales}
end
This gives me exactly the data I want. But it costs n + 1 SQL queries, which is not good. I should be able to get this with just one query...
So: How can I retrieve the data I want without using raw SQL and without using loops that cost multiple queries?
SELECT title, domestic_sales
FROM movies
JOIN boxoffice
ON movies.id = boxoffice.movie_id;
translated to ActiveRecord would look like this
Movie
.select(:title, :domestice_sales)
.joins("boxoffice ON movies.id = boxoffice.movie_id")
When you have proper associations defined in your models you would would be able to write:
Movie
.select(:title, :domestice_sales)
.joins(:boxoffices)
And when you do not need an instance of ActiveRecord and would be fine with a nested array, you can even write:
Movie
.joins(:boxoffices)
.pluck(:title, :domestice_sales)
Try this way.
Movie.joins(:box_office).pluck(:title, :domestic_sales)

Sort a resource based on the number of associated resources of other type

I have a Movie model that has many comments,
I simply want to sort them (Movies) using SQL Inside active record based on the number of associated comments per movie.
How can we achieve a behavior like this in the most efficient way.
I want to do this on the fly without a counter cache column
you can do something like this
#top_ten_movies = Comment.includes(:movie).group(:movie_id).count(:id).sort_by{|k, v| v}.reverse.first(10)
include(:movie) this to prevent n+1 in sql
group(:movie_id) = grouping based on movie for each comment
sort_by{|k,v|v} = this will result an array of array for example [[3,15],[0,10][2,7],...]
for first part [3,15] = meaning movie with id = 3, has 15 comments
you can access array #top_ten_movies[0] = first movie which has top comments
default is ascending, with reverse you will get descending comments

does x = User.all create a hash? How do I traverse it?

Let's say I have a User table and a Messages table, they have a has_many belongs_to relationship. I want to find the id: for users who's names are "Bob", then pull the message history for one of the id's.
x = User.where(name: "Bob")
Does that create a hash in variable x, with all the results of users whose names were Bob? The result in the console certainly looks like a hash when I run x. To includes the messages tied to all the Bobs, I think I do:
x = User.where(name: "Bob").includes(:messages)
Now that I have x...how do I find the id's of the people whose names are Bob? I don't want to query the db again, I'd like to do it all via the variable, is that possible?
I then want to get the first message of the first id (the first Bob) in my table. Can that be done via the variable, or do I have to go back to the DB once I have the first id?
Thanks for all the help guys and gals!
Most ActiveRecord queries return a Relation.
You can call x = x.to_a to make rails perform the actual query(there will be 2 SQL queries - one for users and one for messages) and then traverse the resulting array.
This will do it. As referenced in the rails guides. http://guides.rubyonrails.org/active_record_querying.html section 13.2
x = Message.includes(:users).where(users: { name: "Bob"})
and then to get the first message just tack on .first at the end of the query.
x = Message.includes(:users).where(users: { name: "Bob"}).first
You need to query from Message, not User. Joins (inner join) and includes (left outer join) can be used for eager loading, like in your question, or to do query across multiple tables.
Message.joins(:user).where('user.name = "bob"')

Mongoid: Return documents related to relation?

Say I'm modeling Students, Lessons, and Teachers. Given a single student enrolled in many lessons, how would I find all of their teachers of classes that are level 102? For that matter, how would I find all of their lessons' teachers? Right now, I have this:
s = Mongoid::Student.find_by(name: 'Billy')
l = s.lessons.where(level: 102)
t = l.map { |lesson| lesson.teachers }.flatten
Is there a way to do turn the second two lines into one query?
Each collection requires at least one query, there's no way to access more than one collection in one query (i.e. no JOINs) so that's the best you can do. However, this:
t = l.map { |lesson| lesson.teachers }.flatten
is doing l.length queries to get the teachers per lesson. You can clean that up by collecting all the teacher IDs from the lessons:
teacher_ids = l.map(&:teacher_ids).flatten.uniq # Or use `inject` or ...
and then grab the teachers based on those IDs:
t = Teacher.find(teacher_ids)
# or
t = Teacher.where(:id.in => teacher_ids).to_a
If all those queries don't work for you then you're stuck with denormalizing something so that you have everything you need embedded in a single collection; this would of course mean that you'd have to maintain the copies as things change and periodically sanity check the copies for consistency problems.

Resources