2

I have a query* that results in the following:

#<ActiveRecord::Relation [
    #<BookRank id: 2, book_id: 2, list_edition_id: 1, rank_world: 5, rank_europe: 1>,
    #<BookRank id: 3, book_id: 1, list_edition_id: 1, rank_world: 6, rank_europe: 2>,
    #<BookRank id: 8, book_id: 2, list_edition_id: 3, rank_world: 1, rank_europe: 1>,
    #<BookRank id: 9, book_id: 1, list_edition_id: 3, rank_world: 2, rank_europe: 2
]>

What I am trying to get is a hash like this:

{
    book_id => {
        list_edition_id => {
            "rank_world" => value,
            "rank_europe" => value
        }
    }
}

(The cherry on top would be to order the hash by the rank_world value for the lowest list_edition_id, but that may be too complex perhaps.)

ranks_relation.group_by(&:book_id) gives me a hash where the book_ids are keys, but then the ranks data is still in arrays:

{
    2 => [
            #<BookRank id: 2, book_id: 2, list_edition_id: 1, rank_world: 5, rank_europe: 1>,
            #<BookRank id: 8, book_id: 2, list_edition_id: 3, rank_world: 1, rank_europe: 1>
    ],
    1 => [
            #<BookRank id: 3, book_id: 1, list_edition_id: 1, rank_world: 6, rank_europe: 2>
            #<BookRank id: 9, book_id: 1, list_edition_id: 3, rank_world: 2, rank_europe: 2>
    ]
}

How should I proceed?

*EDIT: This is the model structure and query. Another user asked for it:

class Book < ActiveRecord::Base
  has_many :book_ranks, dependent: :destroy
end

class List < ActiveRecord::Base
  has_many :list_editions, dependent: :destroy
end

class ListEdition < ActiveRecord::Base
  belongs_to :list
  has_many :book_ranks, dependent: :destroy
end

class BookRank < ActiveRecord::Base
  belongs_to :book
  belongs_to :list_edition

  has_one :list, through: :list_edition
end

For the query, I already use two arrays with the relevant IDs for Book and ListEdition:

BookRank.where(:book_id => book_ids, :list_edition_id => list_edition_ids)
gibihmruby
  • 41
  • 7
  • "then the ranks data is still in arrays" - what if you group those arrays too? – Sergio Tulentsev Jul 04 '18 at 10:36
  • I solved similar problems by chaining multiple `group_by`s with `map` (to transform the values in a suitable format like another hash), `to_h` and `sort`/`sort_by`. I am not aware of a more elegant solution but curious to see one too. But as long as it works (and your query does not produce data which is too large to be processed inplace by ruby) this could be a way. – Jay Schneider Jul 04 '18 at 10:56
  • I guess theoretically I could group by list_edition_id in the second step, but I don't know how to properly nest the statements. @JaySchneider: It would usually be up to ~300 BookRanks handled per request. Not ideal, would rather get this done in the DB, but it could work. – gibihmruby Jul 04 '18 at 10:58
  • Can you please add your query in your question? – Kinjal Shah Jul 04 '18 at 11:03

3 Answers3

2

Try this

record = your_record

hash = {}

record.each do |record|
  hash[record.book_id] ||= {}
  hash[record.book_id][record.list_edition_id] = {
    'rank_world' => record.rank_world,
    'rank_europe' => record.rank_europe
  }
end
# hash will then be {2=>{1=>{"rank_world"=>5, "rank_europe"=>1}, 3=>{"rank_world"=>1, "rank_europe"=>1}}, 1=>{1=>{"rank_world"=>6, "rank_europe"=>2}, 3=>{"rank_world"=>2, "rank_europe"=>2}}}

This will iterate through record only once.

Rashid D R
  • 162
  • 6
  • 2
    This solution is quite nice. You could be able to improve it by writing `hash[record.book_id][record.list_edition_id] = record.slice(:rank_world, :rank_europe)` which makes it really short. As far as I understood it, I don't think the keys being strings or symbols matters here. – Jay Schneider Jul 04 '18 at 11:45
  • @JaySchneider Yea if strings or symbols doesn't matter, then we can use slice – Rashid D R Jul 04 '18 at 11:56
1

Hey @gibihmruby (nice name btw),

so since you asked in the comments for a more specific description of my ugly approach using just group_by and friends, here is my proposal:

rel.group_by(&:book_id).map do |k, v| 
  [k, v.group_by(&:list_edition_id)]
end.to_h

would yield a structure like

{2=>
  {1=>
    [#<struct BookRank
      id=2,
      book_id=2,
      list_edition_id=1,
      rank_world=5,
      rank_europe=1>],
   3=>
    [#<struct BookRank
      id=8,
      book_id=2,
      list_edition_id=3,
      rank_world=1,
      rank_europe=1>]},
 1=>
  {1=>
    [#<struct BookRank
      id=3,
      book_id=1,
      list_edition_id=1,
      rank_world=6,
      rank_europe=2>],
   3=>
    [#<struct BookRank
      id=9,
      book_id=1,
      list_edition_id=3,
      rank_world=2,
      rank_europe=2>]}}

You then would have to map the most inner object to the attributes you want. If you are sure that the combination of book_id and list_edition_id is unique, you can get rid of the array wrapping and then map to the required attributes. You can use slice for ActiveRecord objects. The mapping would then be

rel.group_by(&:book_id).map do |book_id, grouped_by_book_id| 
  [
    book_id, 
    grouped_by_book_id.group_by(&:list_edition_id).map do |list_ed_id, grouped|
      [list_ed_id, grouped.first.slice(:rank_world, :rank_europe)]
    end.to_h
  ]
end.to_h

Since I didn't create a model but simply used structs (as you can see in my example above), I didn't really test the last bit by myself. But it should work like this, please comment if you found a mistake or have more questions. I still hope someone comes up with a better solution since I was looking for one myself way too often now.

Cheers :)

edit: minor corrections

Jay Schneider
  • 325
  • 1
  • 7
0
ranks_relation.
  group_by(&:book_id).
  map do |id, books|
    [id, books.map do |book|
           [
             book.list_edition_id,
             {
               "rank_world" => book.rank_world,
               "rank_europe" => book.rank_europe
             }
           ]
         end.sort_by { |_, hash| hash["rank_world"] }.to_h
    ]
  end.to_h
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160