Hash mapping

I am new to Rails and Ruby and am having a difficult time building the data needed for a Highchart. I am struggling with the syntax to map a pair of hashes to an array. Let me try and explain:

I have a table of "initiatives" which contains basically an ID and a Name.

Then I have a table "trainings" that has the initiative_id field to map to the initiative table

What I am trying to do is create a method that pulls the Initiative name and the count of records from the trainings, and returns an array with [[name,count],[name,count]]

What I have been trying is this

def training_count_by_initiative()   initiative_name = Initiative.all.select("id,name")   training_count = Training.group(:initiative_id).count   initiative_name.map do |initiative|     ??? This is the part I am struggling with   end end

I tried following Railscasts #223 but can't seem to get the mapping to work.

I know it is just a Ruby syntax issue that I am not understanding so if someone can explain it to me I would greatly appreciate it.

John

Hi,

I am new to Rails and Ruby and am having a difficult time building the data needed for a Highchart. I am struggling with the syntax to map a pair of hashes to an array. Let me try and explain:

I have a table of "initiatives" which contains basically an ID and a Name.

Then I have a table "trainings" that has the initiative_id field to map to the initiative table

What I am trying to do is create a method that pulls the Initiative name and the count of records from the trainings, and returns an array with [[name,count],[name,count]]

What I have been trying is this

def training_count_by_initiative()   initiative_name = Initiative.all.select("id,name")   training_count = Training.group(:initiative_id).count   initiative_name.map do |initiative|     ??? This is the part I am struggling with   end end

I tried following Railscasts #223 but can't seem to get the mapping to work.

You want the map to return an array with content of [name, count]

So you need to get name and count from inside the map.

`initiative` variable already contains the name so the first part of array is simple, it's just `initiative.name` (`[initiative.name, ...]`).

And as for the count, it's inside `training_count`. The query you have should return a hash with structure `{:id => :count}`. To access count of each id, it's just `training_count[<insert_id>]`. In same spirit with previous part, `training_count[initiative.id]` should do the trick.

Combined, all you need is one line of `[initiative.name, training_count[initiative.id]]`.

You don't need the first two lines, you just need something like Initiative.all.map {|initiative| [initiative.name, initiative.trainings.count] }

Colin

That'll be slow. The alternative is to include `:trainings` but then it'll transfer a bunch of unnecessary data as well.

Who cares? Always initially write the simplest code that will do the job, do not indulge in premature optimisation. The simplest code is less likely to be buggy which is much more important than saving a couple of milliseconds. Later if it is shown that there are throughput issues then one can work out where the bottlenecks are and refactor. Several decades of s/w development have shown me that the bottlenecks in the code are most unlikely to be in the areas you expect (unless you are much better at it than I am) so it is much better to adopt the KISS principle at first development, that way you will get something working more quickly and with fewer bugs.

Colin

I am trying to make this as efficient as possible which is why I was trying to do the group call.

I was doing it in the Highchart definition which worked however it does many queries depending on how many initiatives there are.

series: [{   name: "Trainings",   data: [   <% Initiative.all.each do |initiative| %>       ['<%= initiative.name %>',<%= Training.where("initiative_id = ?", initiative.id).count %>],   <% end %>   ] }],

One of the issues is that there may not be any record count for a particular initiative so I also need to return a zero but still map the initiative.

The example from the Railscast is like this and is similar to what I was trying to do however this example was based on a day mapping.

def orders_chart_series(orders, start_time)     orders_by_day = orders.where(:purchased_at => start_time.beginning_of_day..Time.zone.now.end_of_day). group("date(purchased_at)").select("purchased_at, sum(total_price) as total_price")

    (start_time.to_date..Date.today).map do |date|       order = orders_by_day.detect { |order| order.purchased_at.to_date == date }       order && order.total_price.to_f || 0     end.inspect end

I will try the examples you have given me so far to see if I can accomplish what I need.

I appreciate the fast responses...

I am trying to make this as efficient as possible which is why I was trying to do the group call.

Why?

I was doing it in the Highchart definition which worked however it does many queries depending on how many initiatives there are.

series: [{   name: "Trainings",   data: [   <% Initiative.all.each do |initiative| %>       ['<%= initiative.name %>',<%= Training.where("initiative_id = ?", initiative.id).count %>],   <% end %>   ] }],

One of the issues is that there may not be any record count for a particular initiative so I also need to return a zero but still map the initiative.

initiative.trainings.count will return zero if there are no trainings.

Colin

In order to be most efficient I would first ensure that you have the possessive relationships are in place. If so you would be able to include Training when Calling Initiative. Initiative.all.includes(:training).each do |initiative| puts initiative.training.size end This will utilize eager loading to reduce this run to 2 queries. The issue with this would be dataset size, imagine if you have 1000 initiatives with a total of 300,000 trainings between them. You will need to be able to hold all of those trainings in memory to run this. Not efficient. Likewise your methodology is not efficient as it essentially is an n+1 anitpattern, meaning that you will be doing n queries to get the trainings and another 1 for the initiatives. So if you have 1000 initiatives that will be 1001 queries. So the better way to do this would be to have a counter cache of trainings on the initiative model. This would involve adding a trainings_count field to initiative and then setting the relationship on the training model to something like belongs_to :initiative, counter_cache: true Then when you call initiative.training.size, rails would look to the data in that field before trying to pull the data from the database, so you would be able to use Initiative.all.each do |initiative| puts initiative.training.size end Note that, when adding counter_caching to an existing model set you will need to run a process (pref in the migration) to reset the counters for the model. This could be your migration file. Though using a pure sql statement might be more efficient. add_column :initiatives, :trainings_count, null: false, default: 0 Initiative.all.each do |i| Initiative.reset_counters(i.id, :trainings) end hope this helps

I thought of a counter cache however one of the problems is I need to be able to query by a date range as well in some of my charts and a counter cache wouldn't work.

I almost have it working however I am getting a &quot; returned around each string which I believe is a javascript problem.

My code is

def training_count_by_initiative(startdate, enddate)   initiative_names = Initiative.all.select("id,name")   training_count = Training.by_date(startdate,enddate).group(:initiative_id).count   initiative_name.map do |initiative|     [initiative.name, training_count[initiative.id] || 0]   end end

Got it. Just needed to put raw in front of the ruby call...

Woo Hoo