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