Using find_or_create_by with accepts_nested_attributes_for
I have a Book which has_many :authors, through: :book_authors
class Book < ApplicationRecordhas_many :book_authors
has_many :authors, through: :book_authors
belongs_to :useraccepts_nested_attributes_for :authors, allow_destroy: true
end
author.rb
class Author < ApplicationRecord
has_many :book_authors
has_many :books, through: :book_authors
end
book_author.rb
class BookAuthor < ApplicationRecord
belongs_to :book
belongs_to :author
end
books_controller.rb
class BooksController < ApplicationController
before_action :authenticate_user!def new
@book = Book.new
@book.authors.new
enddef create
@book = current_user.books.create(book_params)
@book.authors.each {|author| author.user_id = current_user.id}if @book.save redirect_to book_path(@book) else render :new endend
private
def book_params
params.require(:book).permit(:title, authors_attributes: [:id, :name, :_destroy])
end
end
new.html.erb
<%= simple_form_for @book do |f| %>
<%= f.input :title %>
<div id='authors'>
<%= f.simple_fields_for :authors do |author| %>
<%= render 'author_fields', :f => author %>
<% end %>
<div class='links'>
<br>
<%= link_to_add_association 'Add another author', f, :authors %>
</div>
</div>
<% end %>
_author_fields.html.erb
<div class="nested-fields">
<%= f.input :name, label: "Author(s)", collection: @authors, value_method: :name, input_html: {value: @authors, class: 'new-author'} %>
<%= link_to_remove_association "Remove this author", f %>
</div>
I haven't tested this, but I believe this should work for you on your create method:
@book = current_user.books.create(book_params) author = Author.find_or_create_by(name: "foo") do |author| # do stuff if author is new end @book.book_authors.create(author_id: author.id)
find_or_create_by will either create and return a new object or return the found object. So once you have the author object, just create the @book.book_authors record directly.
That's been a massive help, so thanks. I just have one small problem now.
When I save the Book and then do
@book.authors
I have two records saved. One is the correct record that was retrieved by the find_or_create_by method. The second record is new and I believe this is being created by the first line of code:
@book = current_user.books.create(book_params)
Is there a way to skip the saving of the new record?
If this were my project, right or wrong, I'd make two sets of params, one for author and one for book. So something like:
def book_params params.require(:book).permit(:title) end
def author_params params.require(:book).permit(authors_attributes: [:id, :name, :_destroy]) end
This way you can create your book without the rails magic also creating the association when you create the book when passing book_params.
There could be a better way to handle this scenario that rails provides, but I haven't found it so I usually resort to this sort of setup to get the job done.