Iterating over merged AssociationRelation - ruby-on-rails

In my application, Parents have many Children. In ParentsController#show, I'd like for the user to be able to specify more than one parent, so I can show all of their children at once.
In my controller, given an #array which contains three Parents with the ids 1, 2, and 3, this is what happens:
#array.map(&:children).reduce(&:or).map { |i| i.parent_id }.uniq
# => [1, 2, 3]
#array.map(&:children).reduce(&:or).map { |i| i.parent }.uniq
# => [#<Parent:0x00007faff17164b8>]
Why is only one parent returned? Is this some sort of caching in action, and if so, how can it be avoided? Is doing things this way a bad idea altogether?

You are not forced to start from parents. What about just
#children = Children.where(parent: parents)
in this case parents is an array of Parent objects or an ActiveRecord::Relation
or
#children = Children.where(parent_id: parent_ids)
in this case parent_ids is an array of integers, ids for Parent model

Related

Rails query, require all conditions in array

I have two models, both associated with each other through has_many through.
I can query the model and filter based on its associated records:
Car.includes(:equipment).where(equipment: { id: [1, 2, 3] })
The problem is that I want to require all of those records, rather than requiring just one of them.
Is there a way to build a query that requires all of the values in the array (the [1, 2, 3] from the above example).
In other words, I'd like to query for all cars that have all three equipment (ids of 1, 2 and 3).
Assuming you have an id_ary like [1,2,3], how about something like:
id_ary.each_with_object(Car.includes(:equipment)) do |id, scope|
scope.where(equipment: {id: id})
end
Looping like that should and your where conditions, I believe.

Ruby on Rails - select where ALL ids in array

I'm trying to find the cleanest way to select records based on its associations and a search array.
I have Recipes which have many Ingredients (through a join table)
I have a search form field for an array of Ingredient.ids
To find any recipe which contains any of the ids in the search array, I can use
eg 1.
filtered_meals = Recipe.includes(:ingredients).where("ingredients.id" => ids)
BUT, I want to only match recipes where ALL of it's ingredients are found in the search array.
eg 2.
search_array = [1, 2, 3, 4, 5]
Recipe1 = [1, 4, 5, 6]
Recipe2 = [1, 3, 4]
# results => Recipe2
I am aware that I can use an each loop, something like this;
eg 3.
filtered_meals = []
Recipes.each do |meal|
meal_array = meal.ingredients.ids
variable = meal_array-search_array
if variable.empty?
filtered_meals.push(meal)
end
end
end
return filtered_meals
The problem here is pagination. In the first example I can use .limit() and .offset() to control how many results are shown, but in the third example I would need to add an extra counter, submit that with the results, and then on a page change, re-send the counter and use .drop(counter) on the each.do loop.
This seems way too long winded, is there any better way to do this??
Assuming you are using has_many through & recipe_id, ingredient_id combination are unique.
recipe_ids = RecipeIngredient.select(:recipe_id)
.where(ingredient_id: ids)
.group(:recipe_id)
.having("COUNT(*) >= ?", ids.length)
filtered_meals = Recipe.find recipe_ids
How about
filtered_meals = Recipe.joins(:ingredients)
.group(:recipe_id)
.order("ingredients.id ASC")
.having("array_agg(ingredients.id) = ?", ids)
You'll need to make sure your ids parameter is listed in ascending order so the order of the elements in the arrays will match too.
Ruby on Rails Guide 2.3.3 - Subset Conditions
Recipe.all(:ingredients => { :id => search_array })
Should result in:
SELECT * FROM recipes WHERE (recipes.ingredients IN (1,2,3,4,5))
in SQL.
Would the array & operator work for you here?
Something like:
search_array = [1, 2, 3, 4, 5]
recipe_1 = [1, 4, 5, 6]
recipe_2 = [1, 3, 4]
def contains_all_ingredients?(search_array, recipe)
(search_array & recipe).sort == recipe.sort
end
contains_all_ingredients(search_array, recipe_1) #=> false
contains_all_ingredients(search_array, recipe_2) #=> true
This method compares the arrays and returns only the elements present in both, so if the result of the comparison equals the recipe array, all are present. (And obviously you could have a little refactor to have the method sit in the recipe model.)
You could then do:
Recipes.all.select { |recipe| contains_all_ingredients?(search_array, recipe) }
I'm not sure it passes your example three, but might help on your way? Let me know if that starts off OK, and I'll have more of a think in the meantime / if it's useful :)
I had a similar need and solved it using the pattern below. This is what the method looks like in my Recipe model.
def self.user_has_all_ingredients(ingredient_ids)
# casts ingredient_ids to postgres array syntax
ingredient_ids = '{' + ingredient_ids.join(', ') + '}'
return Recipe.joins(:ingredients)
.group(:id)
.having('array_agg(ingredients.id) <# ?', ingredient_ids)
end
This returns every recipe where all of the required ingredients are included in an ingredients array.
The Postgres '<#' operator was the magic solution. The array_agg function creates an array of each recipe's ingredient ids and then the left-pointing bird operator asks whether all of the unique ids in that array are contained in the array on the right.
Using the array_agg function required me to cast my search_array into Postgres syntax.
My Recipes model has many Ingredients through Portions.
I'd love to know if anyone has any better optimizations or knows how to avoid the casting to Postgres syntax that I needed to do.

Array of ActiveRecord objects not allowing calls to the objects

I have an issue calling objects within an array of ActiveRecord objects.
Here is my controller code building an array of things:
onething = Thing.where(this: id, that: id)
#things.push(onething) if onething.present?
This is looped in order to build an array of specific things with different ids passed inside the where method for this and that.
This and That being parents from thing:
class Thing < ApplicationRecord
belongs_to :this
belongs_to :that
end
Though in my view, when I call elements of the #things variable I get undefined methods errors.
When showing the #things variable in my view, in order to debug, I get things like this:
[#<ActiveRecord::Relation [#<Thing id: 1 ....>]>, #<ActiveRecord::Relation [#<Thing id: 2.......>]>]
Whereas, a variable with records coming from a direct query such as Thing.find(params[:id]) return something slightly different :
#<Thing id: 1, ....>
Why does the first one doesn't allow me to query the objects with simple queries such as Thing.id as the second works perfectly fine?
It's because you're pushing an ActiveRecord::Relation to the #things array (assuming that #things is indeed an array). So instead of ending up with an array of objects, you're winding up with an array consisting of arrays of objects. Use concat instead:
onething = Thing.where( this: id, that: id)
#things.concat(onething) if onething.present?
This will combine the two collections of objects into a single array.

Filter the children of a has_many/belongs_to relation, and get all their parents as well

I have two models, lets call them Parent and Child, where
class Parent < ActiveRecord::Base
has_many :child
end
class Child < ActiveRecord::Base
belongs_to :parent
end
For an API, I am interested in returning a list of all Child objects matching a certain condition, as well as the parent ID and their corresponding parent objects (but only the parents for the filtered children objects!) without repeating.
Something like:
{children: [{id: 1, parent_id: 20, attr1: x, attr2: y},...], parents: [{id: 20, attr3: z, attr4: w},...]}
What would the most efficient way of doing this be?
I have tried doing:
children = Child.eager_load(:parent).where(condition).all
parents = children.map(&:parent).uniq{|p| p.id}
But the use of map and uniq seems slow and wasteful?
Alternatively, I could query them separately with two SQL queries, i.e.
children = Child.where(condition).all
parent_ids = children.map(&:parent_id)
parents = Parent.where(id: parents_ids).all
However, since there might be hundreds if not thousands of different parents, this still seems somewhat inefficient.
Is there a better way?
Try pushing this to the database by using pluck
children = Children.eager_load(:parent).where(condition).all
parents = children.pluck('distinct parent_id')
edit
I misunderstood the question. There is an alternative, but I'm not sure it's much better than running through all of them (like you show in your question) or running several queries (like you would need to do with pluck). You could use a Set to ensure uniqueness of your parent objects:
parents = Set.new
children = Children.eager_load(:parent).where(condition).all
children.each {|child| parents << child.parent }

Array manipulation (summing one column by certain group, keeping others the same)

I have a table of Logs with the columns name, duration, type, ref_id.
I update the table every so often so perhaps it will look like a col of ['bill', 'bob', 'bob', 'jill'] for names, and [3, 5, 6, 2] for duration, and ['man', boy', 'boy', 'girl'] for type, and [1, 2, 2, 3] for ref_id.
I would like to manipulate my table so that I can add all the durations so that I get a hash or something that looks like this:
{'name' => ['bill', 'bob', 'jill'], 'duration' => [3, 11, 2], 'type' => ['man', 'boy', 'girl'], ref_id => [1, 2, 3]}
How can I do this?
(for more info--currently I'm doing Log.sum(:duration, :group => 'name') which gives me the names themselves as the keys (bill, bob, jill) instead of the column name, with the correct duration sums as their values (3, 11, 2). but then I lose the rest of the data...)
I guess I could manually go through each log and add the name/type/ref_id if it's not in the hash, then add onto the duration. If so what's the best way to do that?
If you know of good sources on rails array manipulation/commonly used idioms, that would be great too!
Couple of notes first.
Your table is not properly normalized. You should split this table into (at least) two: users, and durations. You should do this for lots of reasons, that's relational databases 101.
Also, the hash you want as a result also doesn't look right, it suggests that you are pre-grouping data to suit your presentation. It's usually more logical to put these results in an array of hashes, than in a hash of arrays.
Now on to the answer:
With your table, you can simply do GROUP BY:
SELECT name, type, ref_id, SUM(duration) as duration
FROM logs
GROUP BY name, type, ref_id
or, using AR:
durations = Log.find(:all,
:select => 'name, type, ref_id, SUM(duration) as duration',
:group => 'name, type, ref_id'
)
In order to convert this to a hash of arrays, you'd use something like:
Hash[
%w{name, type, ref_id, duration}.map{|f|
[f, durations.map{|h|
h.attributes[f]
}]
}
]
Maybe all you need is something like this that spins through all the log entries and collects the results:
# Define attributes we're interested in
operate_on = %w[ name duration type ref_id ]
# Create a new hash with placeholder hashes to collect instances
summary = Hash[operate_on.map { |k| [ k, { } ] }]
Log.all.collect do |log|
operate_on.each do |attr|
# Flag this attribute/value pair as having been seen
summary[attr][log.send(attr)] = true
end
end
# Extract only the keys, return these as a hash
summary = Hash[summary.map { |key, value| [ key, value.keys ] }]
A more efficient method would be to do this as several SELECT DISTINCT(x) calls instead of instancing so many models.
Didn't quite understand if you want to save records from your hash, or you want to query the table and get results back in this form. If you want to get a hash back, then this should work:
Log.all.inject({}) do |hash, l|
hash['name'] ||= []
hash['duration'] ||= []
hash['type'] ||= []
hash['ref_id'] ||= []
hash['name'] << l.name
hash['duration'] << l.duration
hash['type'] << l.type
hash['ref_id'] << l.ref_id
hash
end

Resources