Rails sql query with dynamic attribute - ruby-on-rails

Say I have an ActiveRecord object that contains a quantity and a price stored in the database.
I have defined a accessor for the total_price:
def total_price
quantity * price
end
Now what if I want to use this dynamic "attribute" in multiple ActiveRecord query contexts? I might to sum on it, compute average, for multiple scope, etc.
What would be the best practices so that I don't have to repeat this quantity * price with ActiveRecord and if I don't want to denormalize by writing it in DB?
Thanks!

Well we wanted to get caption (from join model) to appear on our associated image model (I.E if you called #user.images, you'd be able to call image.caption (even though caption was in the join model)
So we looked at this RailsCast (you'll benefit from around 6:40) which gave us some information about how you can use join to create more dynamic queries. We ended up using this:
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }
I'm thinking you could use something similar for your request (include some SQL to create the pseudo column in the object). Since it's the origin model, I'm thinking about a scope like this:
default_scope select("(table.quantity * table.price) as total_price")

I assume price is stored in the database. Is quantity stored in the database? If both are stored, why not make total_price a database column as well? You can update total_price whenever you update the record.'
class Order < AR::Base
before_update :update_total_price
def update_total_price
self[:total_price] = quantity * price
end
end
Obviously you can do anything you would with an ordinary column, like Order.where("total_price > 1.0") and what-not.

Related

Rails 4 ActiveRecord group_by and nested loops

The project I am working on has the following models:
Button: has many extra_prices (one for each Currency)
ExtraPrice: belongs to a product (in that case a Button) and has as an attribute a currency_id
Currency: there is a reference to a currency_id on an ExtraPrice as I mentioned above. FYI there are 4 currencies so far in the app.
Some of the buttons don't have an extra_price set in one of the currencies which causes an error in another part of the app.
I am trying to write a rake task that would:
- check all buttons missing an extra_price for one a more currencies
- find out which currency is missing
- create the extra price
So far I toyed with a few options but I am stuck (I am pretty junior as a dev, and especially on the back-end side/DB query).
I was thinking something like:
Button.transaction do
currencies = Currency.all.pluck :id
buttons_no_extra_price = Button.select { |button| button.extra_prices.length <
currencies.length }
end
and then I'm stuck :)
I would like to do something like
buttons_no_extra_price.group_by(|button| button.extra_prices.currency_ids)
(wrong formatting of course since extra_prices is an array and currency_id is an attribute on each extra_price)
but instead of grouping them by currency_id, I would like to group them by the missing currency_id or ids, maybe using the currencies variable above.
missing_prices = {currency1: [button1, button2], currency2: [button192, button208], currency3: [button392, button220]...}
This way I could loop through every Currency and create an extra_price on each button object of the nested array like:
missing_prices.each |currency, array_of_buttons| do
array_of_buttons.each do |button|
ExtraPrice.create!(currency: currency, product: button)
end
end
I am also thinking that from a performance standpoint it needs to be optimized so maybe work more with includes, joins, etc. but it's a bit above my current abilities to be totally honest.
So any help would be appreciated :)
Thanks!
So I think I follow your question, and if I am this should do the trick. Let me know if you have any questions. Note that there is probably a more performant way to do this, but given that it's a rake task performance won't need to be fully optimized unless you are dealing with millions of records.
all_currency_ids = Currency.all.pluck(:id)
Button.eager_load(:extra_price).group('buttons.id').having('count(extra_prices.id) < ?', all_currency_ids.count).each do |button|
missing_currency_ids = all_currency_ids - button.extra_prices.pluck(:currency_id)
missing_currency_ids.each do |missing_currency_id|
ExtraPrice.create!(currency: Currency.find_by(id: missing_currency_id), product: button)
end
end
Button.eager_load(:extra_price).group('buttons.id').having('count(extra_prices.id) < ?', all_currency_ids.count) is what gets you the buttons with missing extra prices. This hinges on the fact that each button has an extra price per currency, so I hope I interpreted that correctly.
(Note: I'm not 100% familiar with Rails 4, only 5 and 6. The underlying SQL principles remain the same regardless and are adaptable.)
You can do this in a single query if your relationships are set up correctly.
class Button < ApplicationRecord
has_many :extra_prices
has_many :currencies, through: :extra_prices
end
class ExtraPrice < ApplicationRecord
belongs_to :button
belongs_to :currency
end
class Currency < ApplicationRecord
has_many :extra_prices
end
I've added a relationship between Currency and ExtraPrice. This allows us to set up a relationship between Button and Currency using has_many :currencies, through: :extra_prices. Then we can get a Button's Currencies with button.currencies.
Now we can do a left join between Button and ExtraPrice and Currency. A left join, as opposed to the normal inner join, will pick up Buttons that have no ExtraPrices nor Currencies. We can't use Rails's left_joins, it will not do the right thing.
buttons_missing_currencies = Button
.includes(:currencies)
.joins("left join extra_prices ep on ep.button_id = buttons.id")
.joins("left join currencies c on c.id = ep.currency_id")
.having("count(c.id) < ?", Currency.count)
.group("buttons.id")
That will give you all the Buttons which lack a Currency in a single, efficient query. includes(:currencies) means each Button's Currencies will already be loaded avoiding making N+1 queries.
Now we can look through each button, discover which currencies are missing, and fill them in.
all_currencies = Currency.all
buttons_missing_currencies.each do |button|
missing_currencies = all_currencies - button.currencies
missing_currencies.each do |missing_currency|
button.extra_prices.create!(currency: missing_currency)
end
end

Is there a way to back a new Rails 5 attribute with two database columns

I'm looking at using the new Rails 5 attributes API for a custom data type, ideally storing the data in two database columns, one for the data value and one for some extra type information.
The Attributes API seems to be designed to work with just one database column and I'm wondering if I'm missing a way to use two columns.
Example
Imagine a Money object, with one decimal or integer column for value and one string column for currency code. I'd pass in my custom money object, store it two columns, and then reading it back would combine the two columns into a Money object.
I've considered serializing the value and currency into a single Postgres JSON column, but I want to be able to do fast SQL SUM queries and sorting on just the value columns, so this doesn't seem ideal.
Thanks in advance for any insight.
I guess you're thinking about creating a ValueObject within your model.
There is ActiveRecord::Aggregations for that. Example:
class Customer < ActiveRecord::Base
composed_of :balance, class_name: "Money", mapping: %w(balance amount)
end
class Money
include Comparable
attr_reader :amount, :currency
EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
def initialize(amount, currency = "USD")
#amount, #currency = amount, currency
end
def exchange_to(other_currency)
exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
Money.new(exchanged_amount, other_currency)
end
def ==(other_money)
amount == other_money.amount && currency == other_money.currency
end
def <=>(other_money)
if currency == other_money.currency
amount <=> other_money.amount
else
amount <=> other_money.exchange_to(currency).amount
end
end
end
Can't answer your question directly unfortunately, but your example got me thinking. the money-rails gem allows use of a separate currency column. Perhaps it would be worth it to dig thru that gem to see what they are doing behind the scenes.

custom attributes in rails

I have a Student model and a method that does some calculations and returns a value
class Student < ActiveRecord::Base
def total_result
#some calculations
return result
end
end
Now in my students controller I would like to do the following
Student.where("total_result > ?", params[:result])
but this brings a PG::UndefinedColumn: ERROR. I am using postgres. How do I achieve this?
You could use:
Student.select { |student| student.total_result > params[:result] }
A word of warning: This will load all students from the database and calculate the value for each of them. This will be slow depending on the number of students in the table.
If you need this more frequently then it would make sense to store/cache the result of the calculation in the database.

How to set a counter cache value to a given one?

I am using Ruby on Rails 3.2.2 and I would like to set a counter cache value to a "custom" one. That is, at this time (in my migration file) I am trying to use the following code:
def up
add_column :articles, :comments_count, :integer, :default => 0
Article.reset_column_information
Article.find_each do |article|
# Note: The following code doesn't work (when I migrate the database it
# raises the error "comments_count is marked as readonly").
Article.update_column(:comments_count, article.custom_comments.count)
end
end
In other words, I would like to set the :comments_count value (a counter cache database table column) to a custom value (in my case that value is article.custom_comments.count - note: the custom_comments is not an ActiveRecord Association but a method stated in the Article model class; it returns an integer value as well) that is not related to a has_many associations.
Maybe, I could / should use something like
Article.reset_column_information
Article.find_each do |article|
Article.reset_counters(article.id, ...)
end
but it seems that the reset_counters method cannot work without has_many associations.
How can I set the :comments_count counter cache value to a given value that is related to a "custom association"?
The accept answer includes the iterating method, which is wrong for existing values of comment_count other than 0: update_counter sets the counter relative to it's current values. To set an absolute value, do:
Article.update_counters(article.id, comments_count: comments.count - article.comments_count)
If you have to fetch each row's correct count anyway, you can also more easily use Article.reset_counters(article.id, :comments)
To do it with far fewer queries, use this:
Author
.joins(:books)
.select("authors.id, authors.books_count, count(books.id) as count")
.group("authors.id")
.having("authors.books_count != count(books.id)")
.pluck(:id, :books_count, "count(books.id)")
.each_with_index do |(author_id, old_count, fixed_count), index|
puts "at index %7i: fixed author id %7i, new books_count %4i, previous count %4i" % [index, author_id, fixed_count, old_count] if index % 1000 == 0
Author.update_counters(author_id, books_count: fixed_count - old_count)
end
You describe comments_count as a counter cache, yet a counter cache is strictly defined as the number of associated records in a has_many relationship, which you say this isn't.
If the only way to get the value you want is via method on Article, then you're going to have to iterate over all your Article objects and update each one.
Article.find_each do |article|
article.update_attribute(:comments_count, article.custom_comments.count)
end
This is pretty inefficient, since it's loading and saving every object.
If the definition of custom_comments (which you don't actually explain) is something you can express in SQL, it would undoubtedly be faster to do this update in the database. Which might look something like this:
CREATE TEMP TABLE custom_comment_counts_temp AS
SELECT articles.id as id, count(comments.id) as custom_comments
FROM articles
LEFT JOIN comments ON articles.id = comments.article_id
WHERE <whatever condition indicates custom comments>
GROUP BY articles.id;
CREATE INDEX ON custom_comments_counts_temp(id);
UPDATE articles SET comments_count = (SELECT custom_comments FROM custom_comment_counts_temp WHERE custom_comment_counts_temp.id = articles.id);
DROP TABLE custom_comment_counts_temp;
(this assumes postgresql - if you're using mySQL or some other database, it may look different. If you're not using a relational database at all, it may not be possible)
Additionally, since it's not a counter cache according to Rails' fairly narrow definition, you'll need to write some callbacks that keep these values updated - probably an after_save callback on comment, something like this:
comment.rb:
after_save :set_article_custom_comments
def set_article_custom_comments
a = self.article
a.update_attribute(:comments_count, a.custom_comments.count)
end

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

Resources