Search code examples
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:

  • use the gem activerecord-import which seems to be able to do bulk inserts for such situations. However I think I would need something like BoardSet.import [board_set], recursive: true, but recursive is only supported for PostgeSQL, so not for MySQL, which I'm using.
  • first save board_set including boards with empty cells, then transferring the ID of the boards to the cells and save them in bulk. However I didn't manage to remove the cells, I've tried to use cloned_board = board.dup and cloned_board.cells = [] for saving empty copies, but it still saves all cells at cloned_board_set.save! (where cloned_board_set contains cloned boards with emtpy cells).
  • I've tried to remove accepts_nested_attributes_for :cells from Board.rb in order to prevent board_set.save! from automatically saving all the cells. However this results in an exception in BoardSet.new(declared(params, include_missing: false)) since the parameter cells_attributes it not known.

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))