Rails: Sum of values in all Transactions that belong_to an Activity - ruby-on-rails

Live site: http://iatidata.heroku.com
Github: https://github.com/markbrough/IATI-Data
Based on aid information released through the IATI Registry: iatiregistry.org
I'm a bit of a Rails n00b so sorry if this is a really stupid question.
There are two key Models in this app:
Activity - which contains details
such as recipient country, funding
organisation
Transaction - which contains details such as how much money (value) was committed or disbursed (transaction_type), when, to whom, etc.
All Transactions nest under an Activity. Each Activity has multiple Transactions. They are connected together by activity_id. has_many :transactions and belongs_to :activity are defined in the Activity and Transaction Models respectively.
So: all of this works great when I'm trying to get details of transactions for a single activity - either when looking at a single activity (activity->show) or looping through activities on the all activities page (activity->index). I just call
#activities.each do |activity|
activity.transactions.each do |transaction|
transaction.value # do something like display it
end
end
But what I now really want to do is to get the sum of all transactions for all activities (subject to :conditions for the activity).
What's the best way to do this? I guess I could do something like:
#totalvalue = 0
#activities.each do |activity|
activity.transactions.each do |transaction|
#totalvalue = #totalvalue + transaction.value
end
end
... but that doesn't seem very clean and making the server do unnecessary work. I figure it might be something to do with the model...?! sum() is another option maybe?
This has partly come about because I want to show the total amount going to each country for the nice bubbles on the front page :)
Thanks very much for any help!
Update:
Thanks for all the responses! So, this works now:
#thiscountry_activities.each do |a|
#thiscountry_value = #thiscountry_value + a.transactions.sum(:value)
end
But this doesn't work:
#thiscountry_value = #thiscountry_activities.transactions.sum(:value)
It gives this error:
undefined method `transactions' for #<Array:0xb5670038>
Looks like I have some sort of association problem. This is how the models are set up:
class Transaction < ActiveRecord::Base
belongs_to :activity
end
class Activity < ActiveRecord::Base
has_and_belongs_to_many :policy_markers
has_and_belongs_to_many :sectors
has_many :transactions
end
I think this is probably quite a simple problem, but I can't work out what's going on. The two models are connected together via id (in Activity) and activity_id (in Transactions).
Thanks again!

Use Active Record's awesome sum method, available for classes:
Transaction.sum(:value)
Or, like you want, associations:
activity.transactions.sum(:value)

Let the database do the work:
#total_value = Transaction.sum(:value)
This gives the total for all transactions. If you have some activities already loaded, you can filter them this way:
#total_value = Transaction.where(:activity_id => #activities.map(&:id)).sum(:value)
You can do it with one query:
#total_value = Transaction.joins(:activity).where("activities.name" => 'foo').sum(:value)

My code was getting pretty messy summing up virtual attributes. So I wrote this little method to do it for me. You just pass in a collection and a method name as a string or symbol and you get back a total. I hope someone finds this useful.
def vsum collection, v_attr # Totals the virtual attributes of a collection
total = 0
collection.each { |collect| total += collect.method(v_attr).call }
return total
end
# Example use
total_credits = vsum(Account.transactions, :credit)
Of course you don't need this if :credit is a table column. You are better off using the built in ActiveRecord method above. In my case i have a :quantity column that when positive is a :credit and negative is a :debit. Since :debit and :credit are not table columns they can't be summed using ActiveRecord.

As I understood, you would like to have the sum of all values of the transaction table. You can use SQL for that. I think it will be faster than doing it the Ruby way.
select sum(value) as transaction_value_sum from transaction;

You could do
#total_value = activity.transactions.sum(:value)
http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html

Related

rails, how to combine two ActiveRecord query results

I have following association
class Location < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :location
end
Suppose I have some instances of Location, I what to query all items belong to those locations. Currently I managed to get the result as an array
items =[]
Location.near(latitude,longitude,distance).find_each do |location|
location.items.find_each do |item|
items << item
end
end
However, is there any way I can get the results as ActiveRecord::Relation. Because I want to further query the results by using "where" with ActiveRecord::Relation.
P.S. The "near" method is from geocoder gem, it returns a ActiveRecord::Relation.
---------------------Edit----------------------------
Thank you for replies I nearly find the solution
locations = Location.near(latitude,longitude,distance)
Item.where(location_id: locations.pluck(:id))
Is it the right way to do it? to me it is a bit unintuitive.
----------------------Edit again ---------------------------
Just a small comment: I say it is unintuitive because I am switching from DataMapper. If it is Datamapper, it would be quite simple, like
Location.near(blabla).items
It is very simply to make queries through associations. Compared to Datamapper, can not understand why ActiveRecord association is so useless?
Edit to use one query with mapping...
What billy said above, but another option that might be faster:
locations = Location.near(1, 2, 3)
items = Item.where(:location_id => locations.map(&:ids)

How to populate rails table with data from other tables?

I'm a bit of a noob programmer so apologies if the question isn't clear enough.
I'm trying to create a basic rails app where I have 3 different tables: usages(month, usage), prices(month, price) and spends(month, spend).
I'm trying to get it so that spend = usages.usage * prices.price. I've put the following code into my Spend model:
class Spend < ActiveRecord::Base
c = Usage.all.count
i = 1
while i <= c
u = Usage.find(i)
p = Price.find(i)
Spend.create(month:u.month, spend:u.usage*p.price)
i += 1
end
end
This works great initially, but as soon as I start adding and removing usages and prices, their id's change so it isn't as clear cut. How can I do this in a much better way?
Thanks,
Kev
In this case, I would lean against making a separate Spend model, since all it does is calculate data that is already present in the database. Unless you have severe caching requirements (and I doubt it in your case), you can use simple instance methods to retrieve the data you want.
First figure out how your Usage and Price models are related. Since you seem to be associating them by id, it appears to be a one-to-one relationship (correct me if I'm wrong on this). However, associating by assuming they have the same primary key is a dangerous approach - rather have one model point to the other using a foreign key. We'll pick the Price model to hold a primary key for Usage, but the reverse can also work. You'll need to add a column using a migration like this:
def change
add_column :prices, :usage_id, :integer
end
Your models should then look like this:
class Usage < ActiveRecord::Base
has_one :price
def spend
usage * price.price
end
end
class Price < ActiveRecord::Base
belongs_to :usage
end
And you can find your spend value for an individual usage item like this:
usage = Usage.find(some_id)
puts usage.spend
Or you can get multiple 'spends' like this:
Usage.include(:price).each do |usage|
puts usage.spend
end
I've left out any reference to month, as I'm not sure how you are using it or if it's needed at all for calculating spend.
Have a look at the Active Record association guide: http://guides.rubyonrails.org/association_basics.html

Associated tables: Join or Include with Rails

I am feeling a bit slow when it comes to rails and the Active Record associations... I have two tables.
Table = Rings
Table = Variations with foreign_key => "ring_id".
class Ring < ActiveRecord::Base
has_many :variations
end
class Variation < ActiveRecord::Base
belongs_to :ring
end
So in my "index/list" view i want to display all the rings, and both the variations, and i was thinking it would be possible to do this through one SQL query... however, i have tried the join and the include methods and i think i am just not understanding how they work properly.
So my question is, how would i write a query in my controller, that would pull my "title" and "value" column values from the "variations" and combine them into one simple object for easy looping? Or do i have to loop through all rings and look up the variation values during the loop?
thanks
In your controller:
#rings = Ring.includes(:variations).all
In index.html.erb:
#rings.each do |ring|
...
ring.variations.each do |variation|
...
end
end
The includes portion of the query will prevent Rails from repeatedly querying the database as you loop through and render your rings and variations in the view.
You need to use the includes method: Ring.inclues(:variations). Then the variation will be loaded along with the rings in a single SQL query.
For more info: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations

Rails Associations

I have a Rails 2.3.11 app which has two key Models:
Activity
Transaction
Live site: http://iatidata.heroku.com
Github: https://github.com/markbrough/IATI-Data
Every transaction nests under an activity. Each activity has multiple transactions.
I think I've got confused about how associations work in Rails, but maybe what I'm trying to do isn't possible.
Essentially, I want to get the total value of the transactions of all the activities which belong to each country. So how much money went to India, how much to Afghanistan, etc.
This works:
#thiscountry_activities.each do |a|
#thiscountry_value = #thiscountry_value + a.transactions.sum(:value)
end
But this doesn't work:
#thiscountry_value = #thiscountry_activities.transactions.sum(:value)
It gives this error:
undefined method `transactions' for #<Array:0xb5670038>
#thiscountry_activities is defined like this:
#activities = Activity.find(:all, :conditions=> #conditions)
This is placed within a loop which gets each recipient country code. #conditions are :
#conditions[:recipient_country_code]=*each recipient country code, e.g. AF*
Looks like I have some sort of association problem. This is how the models are set up:
class Transaction < ActiveRecord::Base
belongs_to :activity
end
class Activity < ActiveRecord::Base
has_and_belongs_to_many :policy_markers
has_and_belongs_to_many :sectors
has_many :transactions
end
I think this is probably quite a simple problem, but I can't work out what's going on. The two models are connected together via id (in Activity) and activity_id (in Transactions).
Thank you!
Nothing is broken, the find method in Rails2 returns an Array - and there is no transactions method on the Array class. Use the first thing you have that's working, with the each iterator, or work from the other side - from Transaction and grouping by the activity. It's been a while since I wrote Rails2 code, but it'll be something like this:
Transaction.all( :select => "activity_id, SUM(value) AS total", :group => 'activity_id' )
That's not ready to copy-paste into your app as-is because there are some things about your app which aren't clear to me, but hopefully it sets you in the right direction.

select many through many... kind of

This is my first post on Stack, so please bear with me if I breach any protocol.
I'm working on a project in Rails (2.1.2), and I have a relations scenario that looks like this:
event [has_many] days
People (in different categories) can sign up for event days, giving the following binding results:
category [has_many] customers [has_many] orders [has_many] days
[belongs_to] event
Now, I'd like to have the total number of 'events' for one customer, or for all customers in a certain category, and I'm stuck. AFAIK, there's no way of performing a simple 'find' through an array of objects, correct? So, what would be the solution; nested loops, and a collect method to get the 'events' from the 'days' in orders?
Please let me know if I'm unclear.
Thanks for your help!
I would personally do this using a MySQL statement. I don't know for sure, but I think it is a lot faster then the other examples (using the rails provided association methods).
That means that in the Customer model you could do something like:
(Note that I'm assuming you are using the default association keys: 'model_name_id')
class Customer
def events
Event.find_by_sql("SELECT DISTINCT e.* FROM events e, days d, orders o, customers c WHERE c.id=o.customer_id AND o.id=d.order_id AND e.id=d.event_id")
end
end
That will return all the events associated with the user, and no duplicated (the 'DISTINCT' keyword makes sure of that). You will, as with the example above, lose information about what days exactly the user signed up for. If you need that information, please say so.
Also, I haven't included an example for your Category model, because I assumed you could adapt my example yourself. If not, just let me know.
EDIT:
I just read you just want to count the events. That can be done even faster (or at least, less memory intensive) using the count statement. To use that, just use the following function:
def event_count
Event.count_by_sql(SELECT DISTINCT COUNT(e.*) FROM ... ... ...
end
Your models probably look like this:
class Category
has_many :customers
class Customer
has_many :orders
has_many :days, :through => :orders # I added this
belongs_to :category
class Order
has_many :days
belongs_to :customer
class Day
belongs_to :event
belongs_to :order
class Event
has_many :days
With this you can count events for customer:
events = customer.days.count(:group => 'event_id')
It will return OrderedHash like this:
#<OrderedHash {1=>5, 2=>13, 3=>0}>
You can get events count by:
events[event_id]
events[1] # => 5
events[2] # => 13
etc.
If you want total number of uniq events:
events.size # => 3
In case of counting events for all customers in category I'd do something like this:
events = {}
category.customers.each {|c| events.merge!(c.days.count(:group => 'event_id') ) }
events.size # => 9 - it should be total number of events
But in this case you lose information how many times each event appeared.
I didn't test this, so there could be some mistakes.

Resources