Search code examples
ruby-on-railsrubyruby-on-rails-5

How can you group and list only certain associated objects of the father object?


I have a simple has_many association between the two models:

class Author < ActiveRecord::Base
  has_many :books
end

class Book < ActiveRecord::Base
  belongs_to :author
end

Suppose I have an array of books objects.

[#<Book:0x00007fbe329ff6f8
  id: 10,
  name: "book_15"
  author_id: 1>,
 #<Book:0x00007fbe329ff6f9
  id: 15,
  name: "Bible"
  author_id: nil>,
 #<Book:0x00007fbe329ff6f1
  id: 17
  name: "book_45"
  author_id: 1>]

How can I convert this array of objects to an array of json objects, so that the top level shows the authors with their associated books as an array and books with no authors are also on the 1st level independently.

Like so:

[
  {
    author_id,
    author_name,
    books: [
      { ...book json... },
      { ...book json... },
    ]
  },
  { ...book json ... },
  { ...book json ... }
]

In my particular example, the result would be something like below:

[
  {
    id: 1,
    name: "Dexter Willis",
    books: [
      { id: 10, name: "book_15" },
      { id: 17, name: "book_45" }
    ]
  },
  { id: 15, name: "Bible" }
]

How can this be done? Thanks in advance.


Solution

  • Assuming books is an array of books objects:

     books_with_author, books_without_author = books.partition { |book| book.author_id }
     books_grouped_by_author = books_with_author.group_by(&:author_id)
     # to avoid N+1
     authors = Author.where(id: books_grouped_by_author.keys)
    
     books_with_author = books_grouped_by_author.map do |author_id, author_books|
       author = authors.detect { |a| a.id == author_id }
       
       {
         id: author.id,
         name: author.name,
         books: author_books.map { |book| { id: book.id, name: book.name } }
       }
     end
    
     books_without_author = books_without_author.map { |book| { id: book.id, name: book.name } }
    
     books_with_author.push(*books_without_author)