Ask A Question

Notifications

You’re not receiving notifications from this thread.

Using find_or_create_by with accepts_nested_attributes_for

Nino Rosella asked in Rails

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>

Reply
Don't suppose anyone could lend a hand before this drives me insane..? :s
Reply
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.
Reply
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?
Reply
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.
Reply
Jacob, you nailed it.

Thanks so much for your help!
Reply
Woohoo, glad that worked for you!

Good luck!
Reply

Good luck!

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 83,453+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.