ruby-on-railsrubyrubygemsruby-grape

Ruby on Rails: how to efficiently save nested attributes (recursively)?


I'm using Rails 6.1.3.2 and trying to make saving of nested attributes more efficient. The database used is MySQL.

Code basis

BoardSet.rb

class BoardSet < ApplicationRecord
   has_many :boards, inverse_of: :board_set, foreign_key: 'board_set_id', dependent: :destroy
   accepts_nested_attributes_for :boards

Board.rb

class Board < ApplicationRecord
   has_many :cells, inverse_of: :board, foreign_key: 'board_id', dependent: :destroy
   accepts_nested_attributes_for :cells

Cell.rb

class Cell < ApplicationRecord
   belongs_to :board, inverse_of: :cells, foreign_key: 'board_id'

There is Grape::API endpoint like this:

params do
  requires :name, type: String, desc: 'BoardSet name'
  optional :boards, as: :boards_attributes, type: Array[JSON] do
    # more params
    optional :cells, as: :cells_attributes, type: Array[JSON] do
      # more params
    end
  end
end
post do
  board_set = BoardSet.new(declared(params, include_missing: false))
  board_set.save!
end

Functionality

Summary of code basis: a BoardSet contains Boards which contain Cells. There is a Grape::API endpoint, where data can be passed to via PUT, which is then saved within the database.

Problem

Saving with board_set.save! is very inefficient. A BoardSet typically contains 40 Boards, where each contain maybe 20 Cells. So in total there are more than 40 * 20 = 800 SQL INSERT statements (a single one for each Cell).

Solution attempts

I've tried these attempts for improvements:


Solution

  • Instead of saving all the objects with one line try these 3 steps:

    1. Save the board set with only its name i.e. without nested attributes.
    2. Save all boards without nested attributes
    3. Save all cells

    After rails 6 we can do insert all

    board_set = BoardSet.new(declared(params[:name], include_missing: false))