Using find_or_initialize on Rails 5

Wasabi Developer picture Wasabi Developer · Jul 25, 2016 · Viewed 8.3k times · Source

I'm playing with Rails 5's find_or_initialize in my Blog app that has Posts and Categories entered into input text fields.

Post model

# == Schema Information
#
# Table name: posts
#
#  id           :integer          not null, primary key
#  title        :string           default(""), not null
#  body         :text             default(""), not null
#  created_at   :datetime         not null
#  updated_at   :datetime         not null
#  category_id  :integer


class Post < ApplicationRecord
  validates :title, :body, :category, presence: true

  has_one :category
  accepts_nested_attributes_for :category
end

Category model

# == Schema Information
#
# Table name: category
#
#  id         :integer          not null, primary key
#  name       :string           default(""), not null
#  created_at :datetime         not null
#  updated_at :datetime         not null

class Category < ApplicationRecord
  validates :name, presence: true
  validates :name, length: { in: 3..80 }

  has_many :posts
end

In the console I can get the following to work:

post = Post.new(title: "Hello Stack Overflow", body: "How are you today?")
post.category = Category.find_or_initialize_by(name: "Greetings")
post.save

post.last
=> #<Post:0x007fd34fa21a23
 id: 42,
 title: "Hello Stack Overflow",
 body: "How are you today?",
 created_at: Mon, 25 Jul 2016 12:56:39 UTC +00:00,
 updated_at: Mon, 25 Jul 2016 12:56:39 UTC +00:00,
 category_id: 3>

The Post form looks like this:

<%= form_for @post do |f| %>
  <fieldset>
    <%= f.label "Title" %>
    <%= f.text_field :body %>
  </fieldset>

  <fieldset>
    <%= f.fields_for :category do |category_fields|
       <%= f.label "Category" %>
       <%= category_fields.text_field :name %>
    <% end %>
  </fieldset>   
<% end %>

My trouble is trying to get what is entered in the category fields_for input into the Post model to use the find_or_initialize.

I tried the following:

class Post < ApplicationRecord
  before_save :generate_category?

  has_one :category
  accepts_nested_attributes_for :category

  private

   def generate_category?
     self.category = Category.find_or_initialize_by(name: name)
   end
end

This fails and I'm getting an NameError error:

NameError in PostController#create
undefined local variable or method `name' for #<Listing:0x007fc3d9b48a48>

My question is:

  1. How do I access the category attributes-is there a way to access the nested_attributes in the model?
  2. Am I using the right callback with before_save
  3. Should I be handling this in the controller somehow?

Any tips on how I can code this would be much appreciated.

Answer

agmcleod picture agmcleod · Jul 25, 2016

name is not a method on the post model, but on the category one. Since category is not defined yet, you can't really rely on calling: self.category.name either. You'll need to define the name value in some other form. Now if you're planning to use accepts_nested_attributes_for you should be able to forgo that method entirely, and just have the hash of data contain the category name:

{ title: 'Post Title', category: { name: 'blog' } }

If you're not going that route, you should probably setup a traditional public method to create the category by passing arguments to this method, instead of using an active record callback. Also since find_or_initialize() will return an object, using a predicate method doesn't make a lot of sense. generate_category? should become generate_category