find_by_sql in Rails, accessing the resulting array - ruby-on-rails

I'm trying to run a query in a very quick and dirty way in Rails, without putting the rest of the model in place. I know this is bad practice but I just need a quick result in a tight timeframe until I've got the whole solution in place.
I've got items that have a shipping price, based on weight. The weight is stored in the item, the price is stored in the table shipping_zone_prices, and all I currently do is look for the price relating to the first row where the weight is heavier than the item for sale:
class Item < ActiveRecord::Base
def shipping_price
item_id = self.id
shipping_price = ShippingZonePrice.find_by_sql(
"SELECT z.price as price
FROM shipping_zone_prices z, items i
WHERE i.id = '#{item_id}'
AND z.weight_g > d.weight
ORDER BY z.weight_g asc limit 1")
end
end
This sort of works. The SQL does the job, but when plugged into the app as follows:
<%= #item.shipping_price %> Shipping
I get the following displayed:
[#<ShippingZonePrice price: 12>] Shipping
In this example, '12' is the price that is being pulled from the db, and is correct. #item.shipping_price.class returns 'Array'. Trying to access the array using [0] (or any other integer) returns a blank.
Is there another way to access this, or am I missing something fundamental?

Since you are defining an instance method, I think it should return the price if it exists or nil
Try something like this:
def shipping_price
ShippingZonePrice.find_by_sql(
"SELECT z.price as price
FROM shipping_zone_prices z, items i
WHERE i.id = '#{self.id}'
AND z.weight_g > d.weight
ORDER BY z.weight_g asc limit 1").first.try(:price)
end
Then this should work for you:
#item.shipping_price
The first.try(:price) part is needed because find_by_sql may return an empty array. If you tried to do something like first.price on an empty array, you would get an exception along the lines of NoMethodError: undefined method 'price' for nil:NilClass.

This is because find_by_sql returns a model, not data. If you want to do a direct fetch of the data in question, use something like this:
ShippingZonePrice.connection.select_value(query)
There are a number of direct-access utility methods available through connection that can fetch single values, a singular array, rows of arrays, or rows of hashes. Look at the documentation for ActiveRecord::ConnectionAdapters::DatabaseStatements.
As when writing an SQL directly, you should be very careful to not create SQL injection bugs. This is why it is usually best to encapsulate this method somewhere safe. Example:
class ShippingZonePrice < ActiveRecord::Base
def self.price_for_item(item)
self.connection.select_value(
self.sanitize_sql(
%Q[
SELECT z.price as price
FROM shipping_zone_prices z, items i
WHERE i.id=?
AND z.weight_g > d.weight
ORDER BY z.weight_g asc limit 1
],
item.id
)
)
end
end

#item.shipping_price.first.price
or
#item.shipping_price[0].price
Thanks Atastor for pointing that out!
When you use AS price in find_by_sql, price becomes a property of the result.

If not for you saying that you tried and failed accessing [0] i'ld say you want to put
#item.shipping_price.first.price # I guess BSeven just forgot the .first. in his solution
into the view...strange

So, I had a hacky solution for this, but it works great.
Create a table that has the same output as your function and reference it, then just call a function that does a find_by_sql to populate the model.
Create a dummy table:
CREATE TABLE report.compliance_year (
id BIGSERIAL,
year TIMESTAMP,
compliance NUMERIC(20,2),
fund_id INT);
Then, create a model that uses the empty table:
class Visualization::ComplianceByYear < ActiveRecord::Base
self.table_name = 'report.compliance_year'
def compliance_by_year(fund_id)
Visualization::ComplianceByYear.find_by_sql(["
SELECT year, compliance, fund_id
FROM report.usp_compliance_year(ARRAY[?])", fund_id])
end
end
In your controller, you can populate it:
def visualizations
#compliancebyyear = Visualization::ComplianceByYear.new()
#compliancefunds = #compliancebyyear.compliance_by_year(current_group.id)
binding.pry
end
Then, you can see it populate with what you need:
[1] pry(#<Thing::ThingCustomController>)> #compliancefunds
[
[0] #<Visualization::ComplianceByYear:0x00000008f78458> {
:year => Mon, 31 Dec 2012 19:00:00 EST -05:00,
:compliance => 0.93,
:fund_id => 1
},
[1] #<Visualization::ComplianceByYear:0x0000000a616a70> {
:year => Tue, 31 Dec 2013 19:00:00 EST -05:00,
:compliance => 0.93,
:fund_id => 4129
},
[2] #<Visualization::ComplianceByYear:0x0000000a6162c8> {
:year => Wed, 31 Dec 2014 19:00:00 EST -05:00,
:compliance => 0.93,
:fund_id => 4129
}
]

Related

Convert Ruby Hash to ranks with no repeats

[{:listing_id=>1, :vote_size=>1, :created_at=>Wed, 13 Nov 2019 02:19:45 UTC +00:00},
{:listing_id=>2, :vote_size=>0, :created_at=>Thu, 14 Nov 2019 02:19:45 UTC +00:00},
{:listing_id=>3, :vote_size=>0, :created_at=>Fri, 15 Nov 2019 02:19:45 UTC +00:00}]
I have the following hash of listing IDs, the number of votes and the created_at date for said listing in Ruby (higher vote_size is better), and I'd like to rank them. In other words, I want to get the rank from some sort of function and then update the rank on the listing via listing.update_attribute(:rank, function-call) or something of that sort. Rank 1 is the best. If there are listings with the same amount of votes only one of them should get the rank and the other listing should get a rank below. (Let's say the tiebreaker is the listing created_at date, whoever created the listing first gets the higher rank.)
This is what I have so far and well I'm stuck lol and could really use some help.
namespace :server do
desc "update the listings rank"
task update_listing_rank: :environment do
listings = Listing.all
all_listings_with_votes = total_votes_for_listing listings
all_listings_with_votes.map{ |e|
puts all_listings_with_votes.index(e) + 1
}
end
def total_votes_for_listing listings
listings.map do |listing|
{listing_id: listing.id, vote_size: listing.votes.size, created_at: listing.created_at}
end
end
end
Here's a big hint... Take your array and
arr.sort{|x,y| x[:vote_size] <=> y[:vote_size]}
This will give you a sorted array of vote sizes.
You can try this, that means we sort by combination vote_size and created_at, vote_size descending and created_at ascending in case there are two equal values for vote_size
an_array.sort_by {|a| [-a[:vote_size] , a[:created_at]] }

In Active Record (Rails), how to select a sum of a column from a has_many relation with every parent record?

Considering this model:
User:
id: int
Valuable:
id: int
user_id: int
value: int
User has many Valuables.
Now here's the thing. I want to use ActiveRecord to select multiple users with any query, after which I want to be able to see the sum of all their valuables, without having to do N+1 queries.
So, I want to be able to do this:
# #var ids [Array] A bunch of User IDs.
User.where(id: ids).each do |u|
puts "User ##{u.id} has total value of #{u.total_value}
end
and it should do 1 (or max 2) queries and not instantiate all Valuables. I tried playing around with select('*, SUM(valuables.value) as total_value).joins(valuables), but with no luck. I'm using PostgreSQL
If at all possible, I would like this to happen automatically (e.g. using default_scope) and I still want to be able to use includes.
UPDATE: Sorry I haven't been clear about this. (Actually, I did write it). I do not want all valuables to be instantiated. I would like to have PostgreSQL do the calculation for me.
UPDATE: What I mean is, I want to depend on PostgreSQL's SUM method to get the total sum in the resultset. I thought my effort in using SELECT and GROUP BY made that clear. I don't want any data or record objects from the Valuables table be part of the result, because it consumes too much memory and calculating the fields using Ruby simply uses too much CPU and takes too long.
In raw SQL you want something like this:
SELECT users.*, SUM(valuable.value) AS values_sum
FROM users
LEFT OUTER JOIN valuables ON users.id = valuables.user_id
GROUP BY users.id
So to translate this in a ActiveRecord query would look like this:
User.select('users.*, SUM(valuables.value) AS total_value')
.joins('LEFT OUTER JOIN valuables ON users.id = valuables.user_id')
Note that you are not actually selecting any columns from valuables.
[10] pry(main)> #users = User.select('users.*, SUM(valuables.value) AS total_value').joins('LEFT OUTER JOIN valuables ON users.id = valuables.user_id')
User Load (1.4ms) SELECT users.*, SUM(valuables.value) as total_value FROM "users" LEFT OUTER JOIN valuables ON users.id = valuables.user_id
=> [#<User:0x007f96f7dff6e8
id: 8,
created_at: Fri, 05 Feb 2016 20:36:34 UTC +00:00,
updated_at: Fri, 05 Feb 2016 20:36:34 UTC +00:00>]
[11] pry(main)> #users.map(&:total_value)
=> [6]
[12] pry(main)>
However the "default_scope" and "I still want to be able to use includes" requirements might be a little tall unless you want to manually load the associations.
Method 1:
hash = User.joins(:valuables).group('users.id').sum('valuables.value')
hash.each { |uid, tval| puts "User ID #{uid} has total value #{tval}." }
Note that the hash will only contain entries for users that have valuables. To get all users (even those without valuables), you can use includes instead of joins.
Method 2:
value_hash = Valuable.group(:user_id).sum(:value)
all_user_ids = User.pluck(:id)
all_user_ids.each do |uid|
tval = value_hash[uid] || 0
puts "User ID #{uid} has total value #{tval}."
end
I would go with Method 1 and includes.
Edit: After better understanding the question:
user_columns = User.column_names.join(', ')
users = User.
includes(:valuables).
group('users.id').
select("#{user_columns}, sum(valuables.value) AS total_value")
users.each { |u| puts "User ID #{user.id} has #{u.total_value} total value." }

Rails options_from_collection_for_select return unique year values

I am using this to get a select with all dates:
options_from_collection_for_select(ObjectModel.all, :id, :get_year)
And this is my get_year method from model:
def get_year
date_attr.strftime("%Y")
end
But it will return this:
2015
2015
2015
However, I need this years to be unique.
How Can I do this?
If no matter which ID from records with the same year attribute must be taken then you can do it this way:
options_from_collection_for_select(ObjectModel.all.to_a.uniq{ |o| o.get_year}, :id, :get_year)
But the better way is to replace this part
ObjectModel.all.to_a.uniq{ |o| o.get_year}
with scope that retrieves unique (by year) records on DB level. This will depend on your DB.
You may also have to sort the records. Then:
ObjectModel.order(:date_attr).uniq{ |o| o.get_year}

Add to cart created at today rails 3.2

My db is postgresql and using rails 3.2.13,
the method add_to_cart is not working as expected,
I want to add to the cart created at today or create if it is not created,
but it doesn't create a new one and adds to the cart created at 2-3 days ago sometimes,
what is wrong in my code?
class Cart < ActiveRecord::Base
has_many :line_items
scope :from_today, where("created_at >= ? AND created_at <= ?",
Time.zone.now.beginning_of_day,
Time.zone.now.end_of_day)
def self.add_to_cart(user_id)
cart = Cart.from_today.first_or_create
line_item = cart.line_items.where(user_id: user_id).first_or_initialize
line_item.amount += 1
line_item.save
end
end
Seems like you have "dynamic data" in your scope. Check out this article to understand what's happening.
Your scope is executed at the initialization process but you need it to be executed at the runtime. The workaround is:
To use lambdas in the scope: scope :from_today, -> { where(...) }
Avoid using scopes and use a class method:
def self.from_today
where(...)
end
Also, when you query a range it would be better to pass an array or range as a value: where(created_at: (Time.zone.now.beginning_of_day)..(Time.zone.now.end_of_day))
My first guess would be the format of your dates inside your scope. At the moment it'll return:
Time.zone.now.beginning_of_day
=> Mon, 28 Apr 2014 00:00:00 UTC +00:00
but in a database query you need it in the format:
Time.zone.now.beginning_of_day.to_s(:db)
=> "2014-04-28 00:00:00"
You might also need to surround the time with single quotes - give it a test and see.

grouping comments by parent object, ordering parent object by oldest comment

I have objects that have comments. As part of a periodic email summary, I want to determine comments for a period, and present the objects in the order of oldest commented object first.
Data:
object_id comment_id
30 40
40 42
32 41
30 43
32 44
Output:
Object #30
comment 40
comment 43
Object #40
comment 42
Object #32
comment 41
comment 44
I am using this code to get the data to an intermediate array - I tried to get it all in one swoop using .group_by(&:commentable_id) but the data didn't come out in correct order.
comments = account.comments.all(
:conditions => ["comments.created_at > ?", 8.hours.ago],
:order => "comments.created_at asc" ).map { |c| [c.commentable_id,c.id] }
=> [ [30,40], [40,42], [32,41], [30,43], [32,44] ]
If I can get that data to transform into the following form, I could just iterate over the array to build the email content...
[ [30,[40,43]], [40,[42]], [32,[41,44]] ]
But I wonder if I'm making this harder than I need to... Any advice?
(I'm using Rails 2.3 and Ruby ree-1.8.7)
You can use a group with an array aggregate to get to the array form that you're looking for.
Array aggregates are massively db dependent. MySQL's is GROUP_CONCAT. Postgres' is ARRAY_AGG. Sqlite doesn't have one out of the box, but I know you can define custom aggregate functions, so it's not impossible.
Haven't actually tried running this code, but here's something that should point you in the right direction:
result = Object.all(
:select => 'objects.id, GROUP_CONCAT(comment_id) AS comment_array',
:group => 'comments.id'
).map { |c| [c.id, c.comment_array] }
I used the naming from the first example, so you'll need to change 'object' to whatever your table is called. Hope it makes sense. Rails probably doesn't have inbuilt support for parsing an array, so it will probably return a string for comment_array, and you might have to parse it.
Having all the comments for a single object in a single block/element will definitely make life easier while doing any operation on them. However, I won't go as far as turning them into an array of array of arrays because it is already an array of arrays. I would prefer creating a hash like so:
comments_array = [ [30,40], [32,41], [40,42], [30,43], [32,44] ]
obj_with_comments = {}
comments_array.each do |x|
obj_with_comments[x.first] ||= []
obj_with_comments[x.first] << x.last
end
obj_with_comments #=> {40=>[42], 30=>[40, 43], 32=>[41, 44]}
But this presents another problem which is, hashes are not ordered, so you loose your ordering in some random fashion if you just iterate over the hash. However, you can create an array of objects then iterate over the hash like so:
objects = comments_array.collect{|x| x.first}.uniq
objects #=> [30, 32, 40]
# now get the hash value for each object key in order
objects.each { |obj| puts obj_with_comments[obj].inspect }
Hope that makes sense.
Try this:
comments = account.comments.all(
:conditions => ["comments.created_at > ?", 8.hours.ago],
:order => "comments.commentable_id ASC, comments.id ASC"
).map { |c| [c.commentable_id,c.id] }
This will return the following result set:
=> [ [30,40], [30,43], [32,41], [32,44], [40,42] ]
In the query above I am using id for sorting instead of create_at. If you are using MySQL and if the id's are auto generated this logic will work as the id of a new object will be higher than the id of an older object. If you don't allow editing of comments then this logic will work.
If you want to explicitly sort by the dates then use the following syntax:
comments = account.comments.all(
:joins => "accounts AS accounts ON comments.commentable_type = 'Account' AND
comments.commentable_id = accounts.id",
:conditions => ["comments.created_at > ?", 8.hours.ago],
:order => "accounts.id ASC, comments.id ASC"
).map { |c| [c.commentable_id,c.id] }

Resources