I have a shopping basket which has items in it. My class is BasketItem < ActiveRecord::Base.
A BasketItem belongs_to :item.
Item has many item_tags. It also has many tags through item_tags.
Tags have a key-value set up. The key can be things like "price", "perishable", "produce", etc. The "produce" key has values like "citrus fruit", "berry fruit", "melon fruit", "vegetable", "root", "fungus" and so on.
When pulling a basket, I want the items to come back in a default order of: all the fruits, fungus, then everything else.
In SQL, I'd do my joins and then add:
ORDER BY (
CASE
WHEN tags.value LIKE '%fruit%' THEN 0
WHEN tags.value = 'Vegetable' THEN 1
ELSE 2
END)
I have tried:
has_many :tags, through: :produce
with a default scope of:
default_scope { order(tags: :desc) }
Just to see if I can access the tags, which I can't. In fact, looking at the SQL generate, it's straight up pulling from basket_items with no joins.
1) So how do I order on that tag relationship?
2) How do I get my CASE in there?
3) And how do I make that the default? (If it's not default_scope.)
Thanks!
Going off of Mike Heft's comment and cleaning up the syntax a bit, I ended up with:
default_scope {joins(:tags).order("CASE WHEN tags.name LIKE ...")}
And that created an SQL query that returns things in the right order.
(This ends up being scrambled again, because the code above that is just pulling the IDs for the product, and returning them in that order. I applied the same gag on that level and...still got them scrambled. 🤣So, while I still have to sort out who's responsible for what, it was a simple "join().order()".)
Related
I'm using Rails 5. I have the following model ...
class Order < ApplicationRecord
...
has_many :line_items, :dependent => :destroy
The LineItem model has an attribute, "discount_applied." I would like to return all orders where there are zero instances of a line item having the "discount_applied" field being not nil. How do I write such a finder method?
First of all, this really depends on whether or not you want to use a pure Arel approach or if using SQL is fine. The former is IMO only advisable if you intend to build a library but unnecessary if you're building an app where, in reality, it's highly unlikely that you're changing your DBMS along the way (and if you do, changing a handful of manual queries will probably be the least of your troubles).
Assuming using SQL is fine, the simplest solution that should work across pretty much all databases is this:
Order.where("(SELECT COUNT(*) FROM line_items WHERE line_items.order_id = orders.id AND line_items.discount_applied IS NULL) = 0")
This should also work pretty much everywhere (and has a bit more Arel and less manual SQL):
Order.left_joins(:line_items).where(line_items: { discount_applied: nil }).group("orders.id").having("COUNT(line_items.id) = 0")
Depending on your specific DBMS (more specifically: its respective query optimizer), one or the other might be more performant.
Hope that helps.
Not efficient but I thought it may solve your problem:
orders = Order.includes(:line_items).select do |order|
order.line_items.all? { |line_item| line_item.discount_applied.nil? }
end
Update:
Instead of finding orders which all it's line items have no discount, we can exclude all the orders which have line items with a discount applied from the output result. This can be done with subquery inside where clause:
# Find all ids of orders which have line items with a discount applied:
excluded_ids = LineItem.select(:order_id)
.where.not(discount_applied: nil)
.distinct.map(&:order_id)
# exclude those ids from all orders:
Order.where.not(id: excluded_ids)
You can combine them in a single finder method:
Order.where.not(id: LineItem
.select(:order_id)
.where.not(discount_applied: nil))
Hope this helps
A possible code
Order.includes(:line_items).where.not(line_items: {discount_applied: nil})
I advice to get familiar with AR documentation for Query Methods.
Update
This seems to be more interested than I initially though. And more complicated, so I will not be able to give you a working code. But I would look into a solution using LineItem.group(order_id).having(discount_applied: nil), which should give you a collection of line_items and then use it as sub-query to find related orders.
If you want all the records where discount_applied is nil then:
Order.includes(:line_items).where.not(line_items: {discount_applied: nil})
(use includes to avoid n+1 problem)
or
Order.joins(:line_items).where.not(line_items: {discount_applied: nil})
Here is the solution to your problem
order_ids = Order.joins(:line_items).where.not(line_items: {discount_applied: nil}).pluck(:id)
orders = Order.where.not(id: order_ids)
First query will return ids of Orders with at least one line_item having discount_applied. The second query will return all orders where there are zero instances of a line_item having the discount_applied.
I would use the NOT EXISTS feature from SQL, which is at least available in both MySQL and PostgreSQL
it should look like this
class Order
has_many :line_items
scope :without_discounts, -> {
where("NOT EXISTS (?)", line_items.where("discount_applied is not null")
}
end
If I understood correctly, you want to get all orders for which none line item (if any) has a discount applied.
One way to get those orders using ActiveRecord would be the following:
Order.distinct.left_outer_joins(:line_items).where(line_items: { discount_applied: nil })
Here's a brief explanation of how that works:
The solution uses left_outer_joins, assuming you won't be accessing the line items for each order. You can also use left_joins, which is an alias.
If you need to instantiate the line items for each Order instance, add .eager_load(:line_items) to the chain which will prevent doing an additional query for every order (N+1), i.e., doing order.line_items.each in a view.
Using distinct is essential to make sure that orders are only included once in the result.
Update
My previous solution was only checking that discount_applied IS NULL for at least one line item, not all of them. The following query should return the orders you need.
Order.left_joins(:line_items).group(:id).having("COUNT(line_items.discount_applied) = ?", 0)
This is what's going on:
The solution still needs to use a left outer join (orders LEFT OUTER JOIN line_items) so that orders without any associated items are included.
Groups the line items to get a single Order object regardless of how many items it has (GROUP BY recipes.id).
It counts the number of line items that were given a discount for each order, only selecting the ones whose items have zero discounts applied (HAVING (COUNT(line_items.discount_applied) = 0)).
I hope that helps.
You cannot do this efficiently with a classic rails left_joins, but sql left join was build to handle thoses cases
Order.joins("LEFT JOIN line_items AS li ON li.order_id = orders.id
AND li.discount_applied IS NOT NULL")
.where("li.id IS NULL")
A simple inner join will return all orders, joined with all line_items,
but if there are no line_items for this order, the order is ignored (like a false where)
With left join, if no line_items was found, sql will joins it to an empty entry in order to keep it
So we left joined the line_items we don't want, and find all orders joined with an empty line_items
And avoid all code with where(id: pluck(:id)) or having("COUNT(*) = 0"), on day this will kill your database
In order to learn Ruby on Rails I am writing a web app that will be used to sort teams within a tournament given their performance to date.
The complication is that I want each tournament organiser (system user) to be able to use a variety of metrics in an arbitrary order.
Expressed as SQL (my background) I want User 1 to be able to choose:
ORDER BY
METRIC1
,METRIC2
,METRIC3
Whilst User 2 could choose:
ORDER BY
METRIC2
,METRIC3
,METRIC1
How would I accept this user input and use it to create a query on the Team table?
Edit 1 Neglected to mention (sorry) that the metrics themselves are calculated on the fly. Currently they are instance methods (e.g #team.metric1 etc). The abortive attempts I have made so far all involve trying to convert user strings to method names which just seems wrong (and I haven't been able to get it to work).
Edit 2 some example code in teams_controller.rb:
class Team < ApplicationRecord
belongs_to :tournament
has_many :matches
def score_for
matches.sum(:score_for)
end
def score_diff
matches.sum(:score_for) - matches.sum(:score_against)
end
end
ActiveRecord allows multiple arguments to be passed to the order method. So you could do something like:
Team.order(:metric2, :metric3, metric1: :desc)
Another options is you can also use ActiveRecord to dynamically construct a query. ActiveRecord queries are lazily evaluated, so the SQL won't be executed until you call an operation that requires loading the records.
For example you could construct a scope on Team like this:
class Team < ApplicationRecord
scope :custom_order, lambda { |sorting_order|
sorting_order.each do |metric|
order(metric)
end
}
end
You would then just need to input a collection of attributes in the order you wanted the order by clauses to be executed. For example:
Team.custom_order([:metric2, :metric3, :metric1])
A working but probably awful solution:
class Tournament < ApplicationRecord
has_many :teams
serialize :tiebreaker, Array
TIEBREAKER_WHITELIST = %w[score opponent_score possession].freeze
def sorted_teams
list = teams.shuffle
(TIEBREAKER_WHITELIST & tiebreaker).reverse.each do |metric|
list = list.sort_by { |team| [team.send(metric), list.find_index(team)] }
end
list.reverse
end
end
Each tournament has many teams. A tournament instance has a serialized field called tiebreaker. This contains an array of strings something like ["score", "possession"] where each string matches the name of a public instance method on team. Each of these methods returns a number.
The tiebreaker field is in descending order of precedence, so for the above example I would only expect possession to affect sorting for teams with an equal score.
list = teams.shuffle - this randomises the list to start with, in case teams are tied for all of the following tiebreakers.
(TIEBREAKER_WHITELIST & tiebreaker) - this returns only strings that appear in both the tiebreaker field and the whitelist constant to protect against end users running arbitrary methods.
.reverse.each do |metric| - this reverses the array of metrics so that the list is sorted by the lowest precedence metric first.
[team.send(metric), list.find_index(team)] - this is the sort for each metric. send turns the string into a method call. I found find_indexwas necessary to preserver sort order from previous sorts. i.e. if I had first sorted for possession this would preserve the order for teams with the same score.
list.reverse - reverse the list then return it. This was because I wanted higher scoring/possession teams first on my list and sort_by sorts ascending.
I wanted some metrics sorted ascending (opponent_score) and others descending (score) so I handled this in the respective methods, returning negative values for opponent_score for example.
I'm not entirely happy with the solution as is but it does seem to work!
I have a Workout model that has and belongs to many Equipment models. I have an array of some Equipment IDs. I want to find all Workouts that don't have any Equipment assigned that matches any of the array of Equipment IDs.
So, if my array = [2,3,5] I want to find all workouts where the assigned equipment ids does not include 2, 3 or 5.
EDIT:
Workout.joins(:equipment).where("equipment.id not in(?)",[2,3,5]).uniq
Assuming five instances of Equipment, the code above returns workouts with equipment.ids 1 and 4 (good), but also returns partial matches for example Workouts with equipment.id = [1,2], [1,2,3].
It helps to think of what result set your query returns.
Workout.joins(:equipment).where("equipment.id not in(?)",[2,3,5]).uniq
Joins all the related equipments to their workouts. If a workout was linked to 4 equipments then you'd get 4 rows for that workout. The where clause just filters that 4 down to a smaller number - it can't wipe them all out just because one matches.
What you need to do instead is add conditions to the join itself. Something like
select workouts.*
left join equipments_workouts on workout_id = workouts.id and equipment_id in (2,3,5)
where equipment_id is null
Should return the correct workouts (it should also return a workout with 0 equipments but I don't know if that's a consideration.)
This works by trying to join 'bad' equipments. Because it's a left join, if no such row can be found then the result set will still include a row for that workout but with the columns for equipmnts_workouts all set to null. As a bonus you no longer have to eliminate duplicates.
Activerecord doesn't have a very nice way of writing queries like this. The joins method will accept an arbitrary SQL fragment though:
Workout.joins("left join equipment_workouts on workout_id = workouts.id and equipment_id in (2,3,5)").
where("equipment_id is null")
You might find the sanitize_sql method useful for generating that sql fragment
Workout.joins(:equipment).merge(Equipment.where("id not in(?)",[2,3,5])).uniq
or
Workout.joins(:equipment).where("equipments.id not in(?)",[2,3,5]).uniq
also u can try this, it should find all Workouts that don't have any Equipment
Workout.includes(:equipment).where("equipments.id not in(?)",[2,3,5])
This can be improved, but should work:
class Workout < ActiveRecord::Base
scope :without_equipments, lambda{|ids| joins(:equipment).where("equipments.id not in (?)", ids.repeated_permutation(ids.size).map(&:uniq).uniq)}
end
Workout.without_equipments 2,3,5
i would like to have your opinion in a project i am currently working on.
class Product
has_many :orders
end
class Order
attr_accessor :deliverable # to contain temporary data on how many items can be delivered for this order
belongs_to :product
end
somehow i want to have
Order.all_deliverable
that will calculate the Product's quantity, subtract from list of Orders until the Product is empty or there is no more Order for this Product
to illustrate
Product A, quantity: 20
Product B, quantity: 0
Order 1, require Product A, quantity: 12
Order 2, require Product B, quantity: 10
Order 3, require Product A, quantity: 100
so if i call Order.all_deliverable, it will give
Order 1, deliverable:12
Order 3, deliverable: 8 #(20-12)
i have been thinking on using named_scope, but i think the logic will be too complex to be put in a named_scope. Any suggestion?
the pseudo code for all_deliverable will be something like this:
go to each orders
find the remaining quantity for specific product
deduct the product to max amount of order, if product is not enough, add the maximum product
add to the order
end
From what i read around in the web, named_scope deal mostly like find and have not many method calling and looping.
I would use a class method. Named scopes are good for adding to the options list you normally pass to find. You should make them as simple as possible, so that callers can chain them together in a way that makes sense in a particular context, and that allow the scopes to be reused.
Design aside, I'm not sure this can work as a named scope anyway:
Scopes return proxies that delay loading from the database until you access them. I'm not sure how you'd do that when you're computing the records to return.
I'm not sure you can set non-column attributes from within a scope.
Even if the above two items don't apply, the delayed load of scopes means you build it now, but potentially don't load the data until some later time, when it could be stale.
If you just want to manipulate things in a named scope, you can do it like this:
named_scope :foobar, lambda {
# do anything here.
# return hash with options for the named scope
{
:order => whatever,
:limit => 50
}
}
Be aware that Rails 3 deprecates long-used parts of activerecord.
I've been banging my head against the wall attempting to come up with this query, so I figured it was time that I post here.
I have a class called Flair -- it's polymorphic, since lots of things can have Flair:
class Flair < ActiveRecord::Base
belongs_to :flairable, polymorphic: true, touch: true
belongs_to :user
end
And a Comment has many Flairs:
class Comment < ActiveRecord::Base
has_many :flairs, as: :flairable
end
When getting a list of Comments, I also want to know which ones have a Flair that belongs to a given user.
The best I've been able to come up with so far is
# don't worry about the interpolation; just for the example
# and assume we have valid #comments and user_id
#comments.select('comments.*').
select('flairs.id as has_flaired').
joins("left join flairs on flairable_id = comments.id and flairs.user_id = #{user_id}")
But this returns one result for every Flair on the Comment (as expected from a left join), effectively multiplying the number of appearances of each Comment in the array by the number of flairs it has.
I've tried using distinct on (user_id), applying a limit, etc., but those efforts only turn up syntax errors.
Can anyone offer any guidance? Especially useful would be a pointer to somewhere in the docs that has examples a bit more involved than the ones offered here: http://www.postgresql.org/docs/9.4/static/sql-select.html. I've also tried the suggestions at http://www.postgresql.org/docs/9.4/static/queries-table-expressions.html, but nothing seems to stick.
Thanks!
Section 7.2.1.3 in www.postgresql.org/docs/9.4/static/queries-table-expressions.html and the first example here, and an extremely patient boss ended up helping most with this problem. In the end (after banging on the keyboard for a while), I finally managed to please the Postgres/ActiveRecord gods with a syntactically correct query (we're sanitizing user_id before passing it to the joins statement.
#comments.select(
"comments.*,
flairs.id as has_flaired"
).joins(
"left join
flairs on flairs.id = (
select id from flairs where flairs.user_id = #{user_id}
and flairs.flairable_id = comments.id
limit 1
)"
)
This works because I'm only interested in whether or not a row with that user and flairable_id exists in the flairs table, so we just check the table in a subquery and use the results in the main query.
It makes sense now that I've written it out, but I'll leave this here in case anyone else gets similarly flummoxed.