EpixRu EpixRu - 7 months ago 8
Ruby Question

Dynamically generate values and create multiple records based on user input with Ruby on Rails

In my Ruby on Rails application I am trying to dynamically create n number of database records based on a number that the user enters on a form. To keep things simple,I will use the analogy of a book and its chapters, to illustrate what I am trying to do. Below is my model, attributes, and form to create a book.

Models:

class Book < ActiveRecord::Base
has_many :chapters
end

class Chapter < ActiveRecord::Base
belongs_to :book
end


Book Attributes:

id
,
book_title
,
number_of_chapters
,
created_at
,
updated_at


Chapter Attributes:

id
,
chapter_title
,
contents
,
book_id
,
created_at
,
updated_at


Form to create new Book:

<%= form_for(@book) do |f| %>
<%= f.text_field :book_title %>
<%= f.text_field :number_of_chapters %>

<%= f.submit %>
<% end %>


When a user creates a book, they can enter in the book's title and the number of chapters the book has. If the user enters in 5 chapters, we then need to be able to dynamically insert 5 records into the Chapter table each with a unique generated chapter title name.

The generated chapter titles could be as simple as:

chapter1
chapter2
chapter3
chapter4
chapter5


The closest solution I have been able to find in my research is to create a nested model form. But a nested model form is not the ideal solution, for a couple of reasons. I cannot predict the number of records that will need to be inserted into the second model, it could be a few or as high as 10,000 records. Plus manually data entering field values for that many records is not practical.

I have a few bits and pieces of what I think needs to be done, but I need help tying it all together.

My first thought is to use an
after_save
callback to trigger a method that will create the chapters. I would add the following to the Book model.

after_save Chapter.generate_chapter_titles


But I am questioning if
after_save
is the best way to do this, from a performance standpoint, since we could potentially be creating several thousand records at a time.

On the Chapter model I was thinking of using the following method to create the
chapter_titles
values based on the
number_of_chapters
.

def generate_chapter_titles
1.upto(book.number_of_chapters) do |chapter|
print "chapter%d" % chapter
end


But now I am stuck, I haven't quite figured how to take the output from
generate_chapter_titles
and create the individual Chapter db records. While making sure that the newly generated chapters are associated with the book it was created from.

Any help on getting this functionality working is greatly appreciated. I have tried to research this before posting here, but answer still eludes me. I am a programming and Ruby noob, so I am willing to admit that some of the information on the Ruby and Ruby on Rails API documentation goes over my head. I would greatly appreciate any help on breaking down and understanding the added functionally that I need to make this work.

Update - Final Solution
Thanks to @margo, all I had to do was update the
create
method in the
books_controller.rb
with the code below. Now the chapters are generated automatically based on the value entered in for
number_of_chapters
.

class BooksController < ApplicationController

[...]

def create
@book = Book.new(book_params)

if @book.save
ActiveRecord::Base.transaction do
@book.number_of_chapters.times do |n|
Chapter.create!(book_id: @book.id, title: "Chapter #{n+1}")
end
end
redirect_to @book, notice: "Book created successfully."
else
render 'new'
end
end

[...]

end

Answer

First of all, you will want to wrap the creation of the book's chapters in a transaction. Transactions will ensure that all the chapters get saved or rolls back any commits if there is an error before it finishes.

You can create all the chapters in the create method in the books_controller. Below is an example, you'll have to fill in the pieces.

In your books_controller

def create
  @book = Book.new(book_params)
  if @book.save 
    generate_chapters(@book)
  end
end

private
def generate_chapters(book)
  ActiveRecord::Base.transaction do
    book.number_of_chapters.times do |n|
      Chapter.create!(book_id: book.id, title: "Chapter #{n+1})"
    end
  end
end

I would suggest that you start with this approach and get it working. If you're going to be creating thousands of records at once, then you might want to consider moving this to a background job, but that's a separate question.