0

I use rails 4 and define one-one relationship between model book and isbn, but can not access the nested attribute. Below is the code

book.rb

 class Book < ActiveRecord::Base
   has_one :isbn
   accepts_nested_attributes_for :isbn
 end

isbn.rb

 class Isbn < ActiveRecord::Base
   belongs_to :book
 end

books_controller.rb

  class BooksController < ApplicationController
  before_action :set_book, only: [:show, :edit, :update, :destroy]

  # GET /books
  # GET /books.json
  def index
    @books = Book.find_by_sql(["select * from books where price > ?", 20])
   end

  # GET /books/1
  # GET /books/1.json
  def show
  end

  # GET /books/new
  def new
    @book = Book.new
  end

  # GET /books/1/edit
  def edit
  end

  # POST /books
  # POST /books.json
  def create
    @book = Book.new(book_params)

    respond_to do |format|
      if @book.save
        format.html { redirect_to @book, notice: 'Book was successfully created.' }
        format.json { render action: 'show', status: :created, location: @book }
      else
        format.html { render action: 'new' }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /books/1
  # PATCH/PUT /books/1.json
  def update
    respond_to do |format|
      if @book.update(book_params)
        format.html { redirect_to @book, notice: 'Book was successfully updated.' }
        format.json { head :no_content }
      else
        format.html { render action: 'edit' }
        format.json { render json: @book.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /books/1
  # DELETE /books/1.json
  def destroy
    @book.destroy
    respond_to do |format|
      format.html { redirect_to books_url }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_book
      @book = Book.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def book_params
      params.require(:book).permit(:name, :author, :price, isbn_attributes: [:number])
    end
end

books\index.html.erb

<table>
  <tr>
    <th>#</th>
    <th>name</th>
    <th>author</th>
    <th>price</th>
    <th>ISBN</th>
    <th colspan="3">operation</th>
  </tr>
<% @books.each do |book| %>
  <tr>
    <td><%= book.id %></td>
    <td><%= book.name %></td>
    <td><%= book.author %></td>
    <td><%= book.price %></td>
    <td><%= book.isbn.number %></td>
    <td><%= link_to 'detail', book %></td>
    <td><%= link_to 'edit', edit_book_path(book) %></td>
    <td><%= link_to 'del', book, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
</table>

but the "<%= book.isbn.number %>" throw exception: undefined method `number' for nil:NilClass.

How to fix this issue? Thanks!

user3003466
  • 323
  • 2
  • 5
  • 15

2 Answers2

2

The simplest way is to check for isbn.nil? before accessing its attributes.

But there are better ways of doing this.

  1. Using delegate so that don't violate the Law of Demeter.

    class Book < ActiveRecord::Base
      has_one :isbn
      accepts_nested_attributes_for :isbn
      delegate :number, to: :isbn, allow_nil: true, prefix: true
    end
    
    # then in your view
    book.isbn_number
    
  2. Using decorators, draper being the most popular gem.

    class BookDecorator < Draper::Decorator
      delegate_all
    
      def isbn_number
        # view can be more complex
        isbn.number unless isbn.nil?
      end
    end
    
    # don't forget to wrap your book instance in controller
    

Decorators are used to accumulate view logic. Use them when you need something more complex than a simple check for existence. Also don't make make your models too fat with a lot of delegators/methods, use decorators instead.

In rare cases you may need your associations always built/created when parent was. You can do it in your controller. Also you can use ActiveRecord callbacks and build associations on parent initialisation which is not recommended because there are some side effects, in tests primarily.

Community
  • 1
  • 1
bashmish
  • 86
  • 3
1

This happened because for that particular book no isbn exists, to handle this add a condition to <%= book.isbn.number %>

<td><%= book.isbn.number unless book.isbn.blank? %></td>
Rajdeep Singh
  • 17,621
  • 6
  • 53
  • 78