如何为 rails 中具有 2 belongs_to 关系的嵌套资源更新脚手架生成的 MVC + 路由

How to update scaffold generated MVC + routes for a nested resource with 2 belongs_to relationships in rails

我正在 Rails 中创建一个虚拟库,当我试图为嵌套资源上的基本 CRUD 创建视图时,我 运行 遇到了错误。我最好的猜测是我没有在视图中正确传递变量 and/or 我生成的控制器没有正确更新。或者模型本身未正确设置为使用外键。我遵循了本教程:

Walk-through to update scaffold generated MVC to use nested resource

主要区别是我的嵌套资源 (checkout_log) 有 2 个外键(user_id 和 book_id),而不是示例中的 1 个。也许这就是答案,我无法创建仅依赖于 2 个外键中的 1 个的路由?例如book/1/checkout_logs/1

当前错误是:(控制器线 7)

undefined method `checkout_logs' for nil:NilClass

我仍在获取活动记录,因此我尝试了多种方法。索引应显示 @book 的所有 checkout_logs,它应该设置为 get_book.

非常感谢任何帮助,谢谢。如果我没有提供足够的信息,请告诉我。

checkout_log.rb

class CheckoutLog < ApplicationRecord
  belongs_to :user
  belongs_to :book
end

book.rb

class Book < ApplicationRecord
    has_many :checkout_logs, dependent: :destroy
end

user.rb

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :checkout_logs
end

schema.rb

ActiveRecord::Schema.define(version: 2020_12_30_171415) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "books", force: :cascade do |t|
    t.string "title"
    t.string "author"
    t.string "genre"
    t.string "subgenre"
    t.integer "pages"
    t.string "publisher"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.integer "book_number"
  end

  create_table "checkout_logs", force: :cascade do |t|
    t.datetime "checkout_date"
    t.datetime "due_date"
    t.datetime "returned_date"
    t.bigint "user_id", null: false
    t.bigint "book_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["book_id"], name: "index_checkout_logs_on_book_id"
    t.index ["user_id"], name: "index_checkout_logs_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.boolean "admin", default: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

  add_foreign_key "checkout_logs", "books"
  add_foreign_key "checkout_logs", "users"
end

routes.rb(现在主要关注书籍和 checkout_log)

Rails.application.routes.draw do    
  devise_for :users
  
  get 'my_books/index'
  get 'my_books/borrow'
  get 'my_books/return'
  get 'books/listing'
  get 'books/borrow'
  
  get 'home/index'
  
  resources :books do    
    resources :checkout_logs
  end

  root to: "home#index"
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

checkout_logs_controller.rb

    class CheckoutLogsController < ApplicationController
  before_action :get_book, :set_checkout_log, only: [:show, :edit, :update, :destroy]

  # GET /checkout_logs
  # GET /checkout_logs.json
  def index
    @checkout_logs = @book.checkout_logs
  end

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

  # GET /checkout_logs/new
  def new
    @checkout_log = @book.checkout_logs.build
  end

  # GET /checkout_logs/1/edit
  def edit
  end

  # POST /checkout_logs
  # POST /checkout_logs.json
  def create
    @checkout_log = CheckoutLog.new(checkout_log_params)

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

  # PATCH/PUT /checkout_logs/1
  # PATCH/PUT /checkout_logs/1.json
  def update
    respond_to do |format|
      if @checkout_log.update(checkout_log_params)
        format.html { redirect_to @checkout_log, notice: 'Checkout log was successfully updated.' }
        format.json { render :show, status: :ok, location: @checkout_log }
      else
        format.html { render :edit }
        format.json { render json: @checkout_log.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /checkout_logs/1
  # DELETE /checkout_logs/1.json
  def destroy
    @checkout_log.destroy
    respond_to do |format|
      format.html { redirect_to checkout_logs_url, notice: 'Checkout log was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    #get book associated with this checkout_log
    def get_book
      @book = Book.find(params[:book_id])
    end

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

    # Only allow a list of trusted parameters through.
    def checkout_log_params
      params.require(:checkout_log).permit(:checkout_date, :due_date, :returned_date, :user_id, :book_id)
    end
end

index.html.erb (checkout_log 指数)

<p id="notice"><%= notice %></p>

<h1>Checkout Logs</h1>

<table>
  <thead>
    <tr>
      <th>User</th>
      <th>Book</th>
      <th>CheckoutDate</th>
      <th>DueDate</th>
      <th>ReturnedDate</th>      
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% Array(@checkout_logs).each do |checkout_log| %>
      <tr>
        <td><%= checkout_log.user_id %></td>
        <td><%= checkout_log.book_id %></td>
        <td><%= checkout_log.checkout_date %></td>
        <td><%= checkout_log.due_date %></td>
        <td><%= checkout_log.returned_date %></td>        
        <td><%= link_to 'Show', book_checkout_log_path(checkout_log) %></td>
        <td><%= link_to 'Edit', edit_book_checkout_log_path(checkout_log) %></td>
        <td><%= link_to 'Destroy', book_checkout_log_path(checkout_log), method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Checkout Log', new_book_checkout_log_path %>

<%= link_to 'Back', books_path %>

_form.html.erb(checkout_log表格部分,我只改了第一行加书)

<%= form_with(model: [@book, @checkout_log], local: true) do |form| %>
  <% if checkout_log.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(checkout_log.errors.count, "error") %> prohibited this checkout_log from being saved:</h2>

      <ul>
        <% checkout_log.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :UserId %>
    <%= form.number_field :UserId %>
  </div>

  <div class="field">
    <%= form.label :BookId %>
    <%= form.number_field :BookId %>
  </div>

  <div class="field">
    <%= form.label :checkout_date %>
    <%= form.datetime_select :checkout_date %>
  </div>

  <div class="field">
    <%= form.label :due_date %>
    <%= form.datetime_select :due_date %>
  </div>

  <div class="field">
    <%= form.label :returned_date %>
    <%= form.datetime_select :returned_date %>
  </div>

  <div class="field">
    <%= form.label :user_id %>
    <%= form.text_field :user_id %>
  </div>

  <div class="field">
    <%= form.label :book_id %>
    <%= form.text_field :book_id %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

在 checkout_logs_controller 中,我需要将 before_action 分两行。我想出于某种原因我可以把它们放在一个列表中。

 before_action :get_book
 before_action :set_checkout_log, only: [:show, :edit, :update, :destroy]