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.
Related
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()".)
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'm having difficulty writing a query, whether in SQL or not, but I'd like to be able to write it in ruby using the gem Squeel. Basically, I have a class:
class Article < ActiveRecord::Base
belongs_to :genre
belongs_to :publisher
...
end
I want a scope that takes in an array of ordered pairs of the format (genre_id, publisher_id) and outputs an ActiveRecord::Relation that contains all of the records with genre, publisher pairs equal to the pairs passed in.
Basically, I want Article.where{ genre_id.eq_any [1, 5, 76] }, except instead of a single attribute, I want it on a pair of attributes:
Article.where{ genre_id_publisher_id.eq_any [(1,2), (1,4), (2, 4)] }
The only way I can think of doing it is making a scope with a select call that adds a column which would be the concatenation of the two ids with some delimiter, then searching based on that:
Article.select(*,
"#{genre_id}-#{publisher_id}" as genre_publisher_pair).where(
genre_publisher_pair.eq_any ["1-2", "1-4", "2-4"])
However, that seems like too much overhead. I also already have a compound index on [genre_id, publisher_id] and I'm afraid this won't use that index, which is going to be a requirement for me.
Is there an easier way to write these compound scopes? Thanks!
I'm wondering if there's a way to fetch objects from the DB via ActiveRecord, without having Rails build the whole objects (just a few fields).
For example,
I sometimes need to check whether a certain object contains a certain field.
Let's say I have a Student object referencing a Bag object (each student has one bag).
I need to check if a female student exists that her bag has more than 4 pencils.
In ActiveRecord, I would have to do something like this:
exists = Student.female.find(:all, conditions => 'bags.pencil_count > 4', :include => :bag).size > 0
The problem is that if there are a 1000 students complying with this condition,
a 1000 objects would be built by AR including their 1000 bags.
This reduces me to using plain SQL for this query (for performance reasons), which breaks the AR.
I won't be using the named scopes, and I would have to remember to update them all around the code,
if something in the named scope changes.
This is an example, but there are many more cases that for performance reasons,
I would have to use SQL instead of letting AR build many objects,
and this breaks encapsulation.
Is there any way to tell AR not to build the objects, or just build a certain field (also in associations)?
If you're only testing for the existence of a matching record, just use Model.count from ActiveRecord::Calculations, e.g.:
exists = Student.female.count( :conditions => 'bags.pencil_count > 4',
:joins => :bag
) > 0
count simply (as the name of the class implies), does the calculation and doesn't build any objects.
(And for future reference it's good to know the difference between :include and :joins. The former eager-loads the associated model, whereas the latter does not, but still lets you use those fields in your :conditions.)
Jordan gave the best answer here - especially re: using joins instead of include (because join won't actually create the bag objects)
I'll just add to it by saying that if you do actually still need the "Student" objects (just with the small amount of info on it) you can also use the :select keyword - which works just like in mysql and means the db I/O will be reduced to just the info you put in the select - and you can also add derived fields form the other tables eg:
students = Student.female.all(
:select => 'students.id, students.name, bags.pencil_count AS pencil_count',
:conditions => 'students.gender = 'F' AND bags.pencil_count > 4',
:joins => :bag
)
students.each do |student|
p "#{student.name} has #{student.pencil_count} pencils in her bag"
end
would give eg:
Jenny has 5 pencils in her bag
Samantha has 14 pencils in her bag
Jill has 8 pencils in her bag
(though note that a derived field (eg pencil_count) will be a string - you may need to cast eg with student.pencil_count.to_i )
I'm building a report in a Ruby on Rails application and I'm struggling to understand how to use a subquery.
Each 'Survey' has_many 'SurveyResponses' and it is simple enough to retrieve these however I need to group them according to one of the fields, 'jobcode', as I only want to report the information relating to a single jobcode in one line in the report.
However I also need to know the constituent data that makes up the totals for that jobcode. The reason for this is that I need to calculate data such as medians and standard deviations and so need to know the values that make the total.
My thinking is that I retrieve the distinct jobcodes that were reported on for the survey and then as I loop through these I retrieve the individual responses for each jobcode.
Is this the correct way to do this or should I follow a different method?
You could use a named scope to simplify getting the groups of responses:
named_scope :job_group, lambda{|job_code| {:conditions => ["job_code = ?", job_code]}}
Put that in your response model, aand use it like this:
job.responses.job_group('some job code')
and you'll get an array of responses. If you're looking to get the mean of the values of one of the attributes on the responses, you can use map:
r = job.responses.job_group('some job code')
r.map(&:total)
=> [1, 5, 3, 8]
Alternatively, you might find it quicker to write custom SQL in order to get the mean / average / sum of groups of attributes. Going through rails for this sort of work may cause significant lag.
ActiveRecord::Base.connection.execute("Custom SQL here")
You can also use Model.find_by_sql()
For example:
class User < Activerecord::Base
# Your usual AR model
end
...
def index
#users = User.find_by_sql "select * from users"
# etc
end