Using find_or_create_by with accepts_nested_attributes_for
Hi gang...
I have a Book which has_many :authors, through: :book_authors
I have a Book which has_many :authors, through: :book_authors
Instead of creating a new Author each time I create a book I'd like to use find_or_create_by on the author's name attribute. Really struggling to work out where this code should go. Using cocoon gem for the form.
Here's what I currently have that works.
book.rb
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 end
end
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>
Hey Nino,
I haven't tested this, but I believe this should work for you on your create method:
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.
Hi Jacob,
That's been a massive help, so thanks. I just have one small problem now.
When I save the Book and then do
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?
Oh, duh I didn't pay attention to your book_params, it includes authors_attirbutes.
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:
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.