Skip to main content

Using find_or_create_by with accepts_nested_attributes_for

Rails • Asked by Nino Rosella

Hi gang...

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

has_many :book_authors
has_many :authors, through: :book_authors
belongs_to :user

accepts_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
end

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

Don't suppose anyone could lend a hand before this drives me insane..? :s

Hey Nino, 

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

@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:

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.

Jacob, you nailed it.

Thanks so much for your help!

Woohoo, glad that worked for you!

Good luck!

Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 22,346+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.