Rails order hash by values and array - ruby-on-rails

I had array of users, and array of they id's. I'm need create hash with {name => id}, but with order of id's array. As example, when i wrote:
keys = [5, 3, 2, 4, 1]
users = User.all.where(id: keys).pluck(:name, :id).to_h
It's return me {"User_1"=>2, "User_2"=>3, "User_3"=>4, "User_4"=>5, "User_0"=>1}
But i'm need to get such thing:
{"User_4"=>5, "User_2"=>3, "User_1"=>2, "User_3"=>4, "User_0"=>1}
Is there opportunity to had such hash on where operation?

The array of users that you get from your database is ordered by the updated_at column. Try this:
users = User.where(id: keys).order(id: :desc).pluck(:name, :id).to_h

Related

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.

rails order array by value when not in model

I created an array in a rails runner(this is not a model and has no attributes) like:
name_1 = 5
name_2 = 14
name_3 = 26
name_4 = 3
...
#names = [name_1, name_2, name_3, name_4, name_5]
Each "name_x" has an integer stored as its value.
How can I order the array so that it orders the output [highest => lowest] by the current values, and also shows the "name" [key, value] ?.
Currently,
puts #names
shows only the values with no order. Tks for pointing me in the right direction, theres many posts relating arrays but most asume its a model with attributes where you can say to order by the attribute. how do you order when you have no specific attributes like in this scenario?
If you want names (keys) and values, use a Hash.
#names = {name_1: 5, name_2: 14, name_3: 26, name_4: 3}
A Hash is Enumerable, so you can sort it. However sorting results in an array, but its easy to make that a Hash again, and hashes in Ruby maintain the order of insertion.
#names.sort_by{|k,v| v}
[[:name_4, 3], [:name_1, 5], [:name_2, 14], [:name_3, 26]]
#names.sort_by{|k,v| v}.to_h
{:name_4=>3, :name_1=>5, :name_2=>14, :name_3=>26}
The natural sort order is ascending (smallest to largest), but you can just negate the sort_by value, or reverse the resulting array.
#names.sort_by{|k,v| -v}.to_h
{:name_3=>26, :name_2=>14, :name_1=>5, :name_4=>3}
#names.sort_by{|k,v| v}.reverse.to_h
{:name_3=>26, :name_2=>14, :name_1=>5, :name_4=>3}

Rails where condition and order

Knowing that by default Rails orders data by ID, how can I order by ids given to the where clause?
ids = Bookmark.where(user_id: 7).order(created_at: :desc).pluck(:company_id)
Result:
[146, 140, 128, 4, 2]
Now, when I try to get the companies in the same order from ids
Company.where(id: ids).pluck(:id)
Result:
[2, 4, 128, 140, 146]
Expected Result:
[146, 140, 128, 4, 2]
My pretended result will be the same in both cases (same order).
The companies should be returned in the same order that the Bookmarks on that company where created.
Company.includes(:bookmarks) .where(id: ids).order('bookmarks.created_at desc').pluck(:id)
So it looks like given a user ID, you want a list of companies sorted by the created_at date of the bookmarks that join Users and Companies.
Am I correct in assuming that Users and Companies share a many-to-many relationship through Bookmarks?
If that's the case, the following "join" could work for you!
Company.joins(:bookmarks).where('bookmarks.user_id = ?', 7).order('bookmarks.created_at')
Of course, in your code, this could be generalized to grab companies for any user!
user_id = 42
companies = Company.joins(:bookmarks).where('bookmarks.user_id = ?', user_id).order('bookmarks.created_at')
ActiveRecord "joins" reference
What exactly are you trying to order by... the company_id?
pluck returns an array, which is why this does NOT work:
ids = Bookmark.where(user_id: 7).pluck(:company_id).order(company_id: :desc)
## undefined method `order' for Array
Instead, you can call sort on the array.
ids = Bookmark.where(user_id: 7).pluck(:company_id).sort
That should do the trick
You are explicitly ordering by created_at in order(created_at: :desc)
You want to order by company_id ids = Bookmark.where(user_id: 7).order(:company_id).pluck(:company_id)
This is how I have solved the question:
Bookmark.includes(:company).where(user_id: current_user).order(created_at: :desc)
and when iterating over the elements I use:
record.company instead of record.
This way I have the companies from the same order that the Bookmarks where created.
Probably you can try this:
company_by_id = Company.find(ids).index_by(&:id) # Gives you a hash indexed by ID
ids.collect {|id| company_by_id[id].id }

How to update collection data for collection of ids?

I am getting collection of ids [1,2,3,4] in the params and I make a call to an API that will return the array for the corresponding ids. E.g. ["Apple","Orange","Mango"]. How can I update this in my database for the corresponding ids?
For example: the ids which are mentioned above are from my user table. ids = [1,2,3,4], which are primary keys in my user table.
From the API response I got an array of fruit_names for the correlated user_ids. E.g.: ids = [1,2,3,4] and fruit_names = ["a","b","c","d"], where the fruit_name column exists in my user table. How do I update fruit_name from the API response correlated ids in my user table?
You can use each_with_index in combination with update for this:
ids.each_with_index do |id, index|
User.update(id, :fruit_name, fruit_names[index])
end
The above code assumes:
ids = [1,2,3,4]
fruit_names = ["a","b","c","d"]
and that the indexes of those arrays match.
Note that this will execute a query for each item in your ids array. If your ids array is very big this is not going to perform well.
Hash[ids.zip fruit_names].each do |id, fruit|
User.update_all({:fruit_name => fruit}, {:id => id})
end
OR
User.where(:id => ids).each do |usr|
usr.update_attribute(:fruit_name, fruit_names[ids.index(usr.id)])
end

How to load users based on a list of id's?

I have a list of id's, I can store this list in any data type as I will be constructed the id's myself.
How can I fetch all users in this list of id's? I want this to be as fast as possible.
I'm using mysql.
Once I retrieve this list, I want to put the User objects into a hash so I can reference them based on id's like:
user_hash[234]
which will return the user in the hash with the user_id of 234.
user_hash = {}
User.where(:id => [1,2,3,4]).each do |user|
user_hash[user.id] = user
end
You can select rows using an array of IDs like this:
ids = [1, 2, 3, 4]
users = User.find(ids)
This will return an array of User records. If you'd like to map that to a hash so you can access by ID as you described, something like this would work:
ids = [1, 2, 3, 4]
users = {}
User.find(ids).each do |user|
users[user.id] = user
end
users[3] # => #<User id: 3, ...

Resources