categories ordering not as expected - ruby-on-rails

I have a categories model with many categories, that has an attribute called tag.
A main category has a tag e.g 1
and a child category has a tag 1-1
so in order to show those categories one after the other in correct order, i order them by tag using the following
#categories = Category.order("tag ASC")
but for some reason let say i have tags,
[1, 1-1, 1-2, 3,4, 10, 10-1 ]
which is the desired ordering i want
the ordering becomes
[1, 1-1, 1-2, 10,10-1, 3,4 ]
how can i fix that?

This is because tag is a string and if you order strings ASC you order them alphabetically. You'll have to write a custom method to sort records by.
Let's say your sort method is called order_by_tag. Then you can order your categories like so: Category.all.sort_by(&:order_by_tag).
Note that this is more difficult if you don't want to iterate over a large number of records every time.
To write a custom sorting method, you'll have to come up with a way to calculate a value of your tag. You could do something like this:
def order_by_tag
tag.split('-').reverse.each_with_index.map do |tag, index|
tag.to_i * 2**(index + 1) }
end.reduce(:+) # => 1*(2^1) + 10*(2^2) = 42
end
Or, shorter:
def order_by_tag
tag.split('-').reverse.each_with_index.map { |tag, index| tag.to_i * 2**(index + 1) }.reduce(:+) # => 1*(2^1) + 10*(2^2) = 42
end
But this example assumes a limit on a possible tag value.
EDIT:
There is an easier way. You can sort records by multiple attributes, if you return an array:
def order_by_tag
tag.split('-').map { |t| t.to_i } # "10-1" => [10, 1]
end
This way Rails will sort records by the first member of the array, then the second and so forth. This way is much better than my first example because it's much easier and values of your categories won't overlap.
SUMMARY:
#categories = Category.all.sort_by(&:order_by_tag)
def order_by_tag
tag.split('-').map { |t| t.to_i }
end

Related

Is there a way to create a temporary column for a single query in Activerecord?

Let's say we have a ChessPiece class with the following migration.
create_table :chess_pieces do |t|
t.string :type
t.integer :row
t.integer :column
t.integer :game_id, index: true
end
If a Bishop wanted to move from (0,0) to (7,7) then I need to check each space in between the two positions for other pieces that might be blocking my Bishop. IE if there was a Pawn on (5,5).
I could query the database for EACH position that might have a blocking piece like this:
pieces = []
8.times do |i|
pieces << ChessPiece.where(game_id: #game_id, row: i, column: i)
end
But I want to cut down on queries. Alternatively, I could grab all the chess pieces for a game and iterate over them in Ruby, but that also seems inefficient. What I'd like to do is tell the database to combine the row and column columns together into the form "row-column" and call it position. That way I could run something like the following query:
ChessPiece.where(game_id: #game_id, position: ["1-1", "2-2", "3-3"])
How do I dynamically create this new column so I can run something like the above query? (Assume I can't just edit the database schema.)
You can use .select to create any select statement you want:
#chess_pieces = ChessPiece.select(
"chess_pieces.*",
"CONCAT(chess_pieces.row, '-', chess_pieces.column) AS position"
)
When you use an alias the column in the result will be available as an attribute on the model:
#chess_pieces.first.position
You can also use column aliases in the WHERE, ORDER and GROUP clauses on some (PG, MySQL) but not all dbs:
ChessPiece.select(
"chess_pieces.*",
"CONCAT(chess_pieces.row, '-', chess_pieces.column) AS position"
).where('position = ?', '1-1')
.group('position')
.order('position ASC')
On those who don't support it you need to concat, aggregate or whatever it is you are doing in-situ.
ChessPiece.order("CONCAT(chess_pieces.row, '-', chess_pieces.column)")
Why not make this a permanent column?
Add a position column of type integer.
Create a before save hook that sets the position to row * 10 + column
You can now query the position column
ChessPiece.where(game_id: #game_id, position: [11, 22, 33])
If you want you can also override the position= to also set the column and row. Allowing you to use it for writes as well.
You could add a helper to your ChessPiece model to simplify building the correct query. Something like this should do the trick:
class ChessPiece < ApplicationRecord
def self.matches_position(*positions)
return none if positions.empty?
constraints = positions.map do |row, column|
arel_table[:row].eq(row).and(arel_table[:column].eq(column))
end
where(constraints.reduce(:or))
end
end
ChessPiece.where(game_id: #game_id).matches_position([1, 1], [2, 2], [3, 3])
This should result in the SQL (syntax might differ based on the database used):
SELECT "chess_pieces".*
FROM "chess_pieces"
WHERE "chess_pieces"."game_id" = 1
AND ("chess_pieces"."row" = 1 AND "chess_pieces"."column" = 1
OR "chess_pieces"."row" = 2 AND "chess_pieces"."column" = 2
OR "chess_pieces"."row" = 3 AND "chess_pieces"."column" = 3)

How to get a unique set of parent models after querying on child

Order has_many Items is the relationship.
So let's say I have something like the following 2 orders with items in the database:
Order1 {email: alpha#example.com, items_attributes:
[{name: "apple"},
{name: "peach"}]
}
Order2 {email: beta#example.com, items_attributes:
[{name: "apple"},
{name: "apple"}]
}
I'm running queries for Order based on child attributes. So let's say I want the emails of all the orders where they have an Item that's an apple. If I set up the query as so:
orders = Order.joins(:items).where(items: {name:"apple"})
Then the result, because it's pulling at the Item level, will be such that:
orders.count = 3
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com", "beta#example.com"]
But my desired outcome is actually to know what unique orders there are (I don't care that beta#example.com has 2 apples, only that they have at least 1), so something like:
orders.count = 2
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
How do I do this?
If I do orders.select(:id).distinct, this will fix the problem such that orders.count == 2, BUT this distorts the result (no longer creates AR objects), so that I can't iterate over it. So the below is fine
deduped_orders = orders.select(:id).distinct
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
But then the below does NOT work:
deduped_orders.each do |o|
puts o.email # ActiveModel::MissingAttributeError: missing attribute: email
end
Like I basically want the output of orders, but in a unique way.
I find using subqueries instead of joins a bit cleaner for this sort of thing:
Order.where(id: Item.select(:order_id).where(name: 'apple'))
that ends up with this (more or less) SQL:
select *
from orders
where id in (
select order_id
from items
where name = 'apple'
)
and the in (...) will clear up duplicates for you. Using a subquery also clearly expresses what you want to do–you want the orders that have an item named 'apple'–and the query says exactly that.
use .uniq instead of .distinct
deduped_orders = orders.select(:id).uniq
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
If you want to keep all the attributes of orders use group
deduped_orders = orders.group(:id).distinct
deduped_orders.each do |o|
puts o.email
end
#=> output: "alpha#exmaple.com", "beta#example.com"
I think you just need to remove select(:id)
orders = Order.joins(:items).where(items: {name:"apple"}).distinct
orders.pluck(:email)
# => ["alpha#exmaple.com", "beta#example.com"]
orders = deduped_orders
deduped_orders.each do |o|
puts o.email # loop twice
end

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.

Sort an activerecord result set by another array of ids

How do I order this activerecord result set based on a another array of ids?
I perform an initial query on my activerecord model:
results = Foo.first(5)
results.each do |r|
#do some stuff initially with these records.
#5 records returned with ids: 1, 2, 3, 4, 5
end
I then do some further processing which results in another array of ids:
desired_order_ids = [3,1,4,5,2]
I want to order results by Foo.id in the order specified by the desired_order_ids array of ids. How to do this? In reality the above query will retrieve 1000 records, so I am looking for the most memory efficient way to do this?
Do you need the end result to be an ActiveRecord::Relation? If you're fine with converting to an Array, this should be the simplest solution:
desired_order_ids.map { |id| results[id - 1] }
Another option could be:
results.sort_by { |result| desired_order_ids.index(result.id) }
It depends on the DBMS you are using, but I think this will work on most databases:
Foo.all.order('id=3 desc, id=1 desc, id=4 desc, id=5 desc, id=2 desc')
You can use a method to build the argument passed to order like this:
def order_string(field_name, arr)
arr.map { |val| "#{field_name}=#{val} desc" }.join(', ')
end
and call it like:
Foo.all.order(order_string('id', [3, 1, 4, 5, 2]))

Best way to order records by predetermined list

In order to keep things simple I have avoided using an enum for an attribute, and am instead storing string values.
I have a list of all possible values in a predetermined order in the array: MyTypeEnum.names
And I have an ActiveRecord::Relation list of records in my_recs = MyModel.order(:my_type)
What is the best way to order records in my_recs by their :my_type attribute value in the order specified by the array of values in MyTypeEnum.names ?
I could try this:
my_recs = MyModel.order(:my_type)
ordered_recs = []
MyTypeEnum.names.each do |my_type_ordered|
ordered_recs << my_recs.where(:my_type => my_type_ordered)
end
But am I killing performance by building an array instead of using the ActiveRecord::Relation? Is there a cleaner way? (Note: I may want to have flexibility in the ordering so I don't want to assume the order is hardcoded as above by the order of MyTypeEnum.names)
You are definitely taking a performance hit by doing a separate query for every item in MyTypeEnum. This requires only one query (grabbing all records at once).
ordered_recs = Hash[MyTypeEnum.names.map { |v| [v, []] }]
MyModel.order(:my_type).all.each do |rec|
ordered_recs[rec.my_type] << rec
end
ordered_recs = ordered_recs.values.flatten
If MyTypeEnum contains :a, :b, and :c, ordered_recs is initialized with a Hash of Arrays keyed by each of the above symbols
irb(main):002:0> Hash[MyTypeEnum.names.map { |v| [v, []] }]
=> {:a=>[], :b=>[], :c=>[]}
The records are appended to the proper Array based on it's key in the Hash, and then when all have bene properly grouped, the arrays are concatenated/flattened together into a single list of records.

Resources