Rails中has_many through多对多关联在MVC场景下的实操问题
问题1:是否需要为Checkouts创建CRUD脚手架?
不需要全量生成CRUD脚手架,你仅需要create(借阅)、destroy(归还)两个关联操作,脚手架生成的编辑、列表、详情等功能对你的场景基本无用,会产生大量冗余代码。如果需要单独维护借阅相关逻辑,完全可以手动创建仅包含必要动作的Checkouts控制器,无需走全量脚手架生成流程。
控制器识别关联对象的逻辑非常简单:
- 已登录的
User对象直接通过Devise提供的current_user方法获取,不需要额外传参 - 对应的
Book对象通过嵌套路由携带的book_id参数查询获取
具体实现步骤
第一步:确认模型关联配置正确
# app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :checkouts has_many :books, through: :checkouts end
# app/models/book.rb class Book < ApplicationRecord has_many :checkouts has_many :users, through: :checkouts end
# app/models/checkout.rb class Checkout < ApplicationRecord belongs_to :user belongs_to :book # 可选:添加唯一校验,避免同一用户重复借阅同一本未归还的书 validates :user_id, uniqueness: { scope: :book_id, message: "你已经借阅过这本书" } end
第二步:配置嵌套路由
# config/routes.rb Rails.application.routes.draw do devise_for :users resources :books do # 仅保留需要的借阅、归还动作路由 resources :checkouts, only: [:create, :destroy] end end
第三步:编写Checkouts控制器
# app/controllers/checkouts_controller.rb class CheckoutsController < ApplicationController # 借阅/归还必须要求用户登录 before_action :authenticate_user! # 两个动作都需要先查询对应图书 before_action :find_book def create # 通过current_user的关联创建记录,自动写入user_id字段 @checkout = current_user.checkouts.build(book: @book) if @checkout.save redirect_to @book, notice: "借阅成功" else redirect_to @book, alert: @checkout.errors.full_messages.join(", ") end end def destroy # 仅查询当前用户对该本书的借阅记录,避免越权操作 @checkout = current_user.checkouts.find_by(book_id: @book.id) if @checkout @checkout.destroy redirect_to @book, notice: "归还成功" else redirect_to @book, alert: "你没有借阅这本书" end end private def find_book @book = Book.find(params[:book_id]) end end
第四步:视图层添加操作按钮
在图书详情页添加借阅/归还按钮即可:
<!-- app/views/books/show.html.erb --> <% if user_signed_in? %> <% if current_user.books.include?(@book) %> <%= button_to "归还图书", book_checkout_path(@book, @book.checkouts.find_by(user_id: current_user.id)), method: :delete %> <% else %> <%= button_to "借阅图书", book_checkouts_path(@book), method: :post %> <% end %> <% end %>
问题2:不创建Checkouts控制器的实现方式
如果不想新增控制器,也可以直接把借阅/归还逻辑写到已有的BooksController中,通过自定义动作实现:
路由配置
# config/routes.rb resources :books do member do post :checkout # 借阅动作 delete :return # 归还动作 end end
控制器实现
在原有BooksController中新增两个动作即可:
# app/controllers/books_controller.rb class BooksController < ApplicationController # 原有CRUD动作保持不变,新增以下内容 before_action :authenticate_user!, only: [:checkout, :return] before_action :set_book, only: [:show, :edit, :update, :destroy, :checkout, :return] def checkout @checkout = current_user.checkouts.build(book: @book) if @checkout.save redirect_to @book, notice: "借阅成功" else redirect_to @book, alert: @checkout.errors.full_messages.join(", ") end end def return @checkout = current_user.checkouts.find_by(book_id: @book.id) if @checkout @checkout.destroy redirect_to @book, notice: "归还成功" else redirect_to @book, alert: "你没有借阅这本书" end end # 原有set_book等私有方法不用修改 end
视图层按钮
<% if user_signed_in? %> <% if current_user.books.include?(@book) %> <%= button_to "归还图书", return_book_path(@book), method: :delete %> <% else %> <%= button_to "借阅图书", checkout_book_path(@book), method: :post %> <% end %> <% end %>
内容的提问来源于stack exchange,提问作者Jeff
相关产品推荐
相关产品推荐

