【问题标题】:Rails - Why does my nested model form not require 'accepts_nested_attributes_for'Rails - 为什么我的嵌套模型表单不需要'accepts_nested_attributes_for'
【发布时间】:2016-02-09 03:57:27
【问题描述】:

我有两个型号 itemuser_itemItem 有很多 user_itemsuser_items 属于 item。我有一个表单,用户可以在其中创建新项目。在表单中,用户应该包含一张图片。名称、描述和标签被保存到一个新的项目对象中。图片应保存为同时创建的 user_item 对象的属性。

我一直在阅读有关嵌套模型形式和强参数以及使用accepts_nested_attributes_for 的信息。我的表单似乎正在运行,但我不明白为什么我不需要accepts_nested_attributes_for。虽然看起来很有效,但我这样做的方式有问题吗?

表格

<%= simple_form_for @item, url: items_path, method: :post do |item_builder| %>
  <div class="well">
  <%= item_builder.input :name, required: false, error: false, label: "Item name" %>
  <%= item_builder.input :description, as: :text, required: false, error: false, label: "Describe item" %>
  <%= item_builder.input :tag_list, required: false, label: "Tags" %>
  <%= item_builder.simple_fields_for @item, @item.user_items.build do |user_item_builder| %>
    <%= user_item_builder.input :picture, as: :file, required: false, label: "Picture of you with this item" %>
  <% end %>
  </div>
  <div class="clearfix">
    <%= item_builder.submit 'Submit new item', class: "btn btn-primary pull-right inherit-width" %>
  </div>
<% end %>

Items_controller

def create
    Item.transaction do

      @item = Item.create(name: item_params[:name],
                                        description: item_params[:description],
                                        tag_list: item_params[:tag_list], 
                                        created_by: current_user.id,
                                        status: Item::STATUS[:pending])

      if item_params[:item] == nil
        @item.errors.add(:picture, "is required")
      end

      if @item.errors.empty?
        @user_item = @item.user_items.build(user_id: current_user.id, picture: item_params[:item][:picture])

        if @user_item.save
          flash[:notice] = "Thank you for your item submission."
          redirect_to items_path
        else
          render :new
          raise ActiveRecord::Rollback, "Useritem create failed"
        end

      else
        render :new
        raise ActiveRecord::Rollback, "no picture"
      end
    end
  end

  private

    def item_params
      params.require(:item).permit(:name, :description, :tag_list,
                                                                item: :picture)
    end

编辑:

我已经根据下面理查德的回答进行了更改,但我现在遇到了几个问题。这是我当前在 items_controller 中的创建操作

  def create
    @item= Item.new item_params
    @item.status = Item::STATUS[:pending]
    @item.created_by = current_user.id
    @item.user_items.first.user_id = current_user.id
    if @item.save
      redirect_to items_path, notice: "Thank you for your item request!
    else
      render :new
    end
  end
  1. 验证失败时,我收到了缺少模板的错误,因此我添加了 elserender :new

  2. 现在验证失败时,会呈现新模板,但不显示图片的 fields_for 输入。

  3. 我在 user_item.rb 中有 validates_presence_of :picture,我有 validates_associated :user_items 在 item.rb 中,但我仍然可以提交没有图片的表单。

  4. 正如您在有效提交中看到的那样,我需要将项目的状态属性设置为待处理,并将 created_by 属性设置为当前用户 ID。我还需要将 user_item 的 user_id 属性设置为当前用户 ID。我在控制器中设置了这个。我想知道像我在创建操作中这样设置是否是正确的方法。

【问题讨论】:

  • 从这里开始很难弄清楚你在做什么。你有一个强大的参数方法,但你没有使用它。 simple_fields_for @item, @item.user_items.build 很奇怪 - 只需将您要为其创建字段的关联名称传递给它。另请阅读将嵌套参数列入白名单的指南:guides.rubyonrails.org/…

标签: ruby-on-rails ruby-on-rails-4


【解决方案1】:

你不需要它,因为你的模式是错误的:

@user_item = @item.user_items.build(user_id: current_user.id, picture: item_params[:item][:picture])

使用上面的代码,您可以从每个 @item 创建单独的 user_items。这不需要accepts_nested_attributes_for,但是很麻烦,反对convention并且非常hacky。


应该这样做:

模型

#app/models/item.rb
class Item < ActiveRecord::Base
  has_many :user_items
  has_many :users, through: :user_items

  accepts_nested_attributes_for :user_items
end

#app/models/user_item.rb
class UserItem < ActiveRecord::Base
  belongs_to :user
  belongs_to :item
end

控制器

#app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def new
    @item = Item.new
    @item.user_items.build
  end

  def create
    @item = Item.new item_params
    redirect_to @item, notice: "Thank you for your item submission." if @item.save
  end

  private

  def item_params
    params.require(:item).permit(:name, :description, :tag_list, user_items_attributes: [:picture])
  end
end

观看次数

#app/views/items/new.html.erb
<%= simple_form_for @item do |item_builder| %>
  <%= item_builder.input :name, required: false, error: false, label: "Item name" %>
  <%= item_builder.input :description, as: :text, required: false, error: false, label: "Describe item" %>
  <%= item_builder.input :tag_list, required: false, label: "Tags" %>
  <%= item_builder.simple_fields_for :user_items do |user_item_builder| %>
    <%= user_item_builder.input :picture, as: :file, required: false, label: "Picture of you with this item" %>
  <% end %>
  <%= item_builder.submit 'Submit new item', class: "btn btn-primary pull-right inherit-width" %>
<% end %>

我的做法有问题吗

从技术上讲,没有

但是,如果您渴望成为一名专业人士或类似水平的人,那么您编写的代码不会走得太远。

使用 Rails 之类的框架可以让您访问前所未有的一系列预置功能。最好的代码不是您编写的代码,而是经过多年生产使用编译和测试的库代码。

虽然您的代码有效,但效率不高,extensible

--

你写的东西是否“正确”的最终关键是你是否会为再次看到它而感到自豪。如果没有,你可能是最好的重构。


更新

如果你想给 Item 一个 status,你会想看看 ActiveRecord 模型的 enum 模块:

#app/models/item.rb
class Item < ActiveRecord::Base
  enum status: [:active, :pending, :declined]
end

这是一个非常有趣的方法,因为它提供了一系列的类方法(作用域)和实例方法:

@item = Item.find x

@item.active?   #-> true
@item.pending?  #-> false
@item.declined? #-> false

Item.active     #-> collection of "active" items
Item.pending    #-> collection of "pending" items
Item.declined   #-> collection of "declined" items

要保存项目的status,您可以使用collection_select

<%= form_for @item do |f| %>
  <%= f.collection_select :status, Item.statuses, :first, :first %>
  <%= f.submit %>
<% end %> 

更新

您的代码可以大大改进:

def create
    @item= Item.new item_params #-> this line should do ALL the heavy lifting.

    if @item.save
      redirect_to items_path, notice: "Thank you for your item request!"
    else
      render :new
    end
end

private

def item_params
    params.require(:item).permit(:name, :description, :tag_list, user_items_attributes: [:picture]).merge(created_by: current_user.id)
end

如果您有验证问题,file 字段将不会再次填充;这是一个操作系统问题,而不是 Rails(操作系统如何知道您的文件与您第一次提交时所在的位置相同?)。

您需要使您的create 代码尽可能简洁;显式声明属性通常是一个坏主意。您应该尽可能多地将其放入Item 模型(enum 等)。

【讨论】:

  • 好答案。虽然来自 OP UserItem 只是 1-1 而不是连接表,或者他只是没有考虑到足够的域。这让has_many :items, through: :user_items 有点奇怪,因为它会使模型自我加入。
  • 这个“引以为豪”的东西对我来说从来没有用过。从中我必须得出结论,我不是专业人士。 :)
  • 我不是有意暗示不尊重;这家伙问他的代码是否合法——在 Rails 的上下文中——不是。他显然想要改进,所以我意思是如果他把它带到雇主那里,它就不会像那些经历过足够了解框架以了解复杂性等的人那样受到重视。种类就像穿着短裤参加面试
  • 别担心,我是在开玩笑 :)
  • 是的,我会的。请参阅我上面的编辑。感谢您的帮助。
【解决方案2】:

虽然它似乎工作正常,但我的方式有问题吗 干吗?

它很好用——但是它确实有一种代码气味。您的控制器有很多属于模型层的业务逻辑,您将不得不在更新操作中通过参数检查重复整个混乱。

让我们看看Rails 方式

class Item
  enum status: [:pending, :approved, :awesome] # or whatever statuses you have
  has_many :user_items
  belongs_to :creator, class_name: 'User' # or author or whatever
  validates_associated :user_items
  accepts_nested_attributes_for :user_items, reject_if: :all_blank?
end

class UserItem
  belongs_to :item
  validates :picture, presence: true
end

class User
  has_many :items, source: :creator, inverse_of: :creator
end

这里我们将 item 设置为 accepts_nested_attributes_for :user_items 并为 UserItem 设置验证,validates_associated 将导致 Item 验证失败,除非所有关联的 UserItem 都有效。

ActiveRecord::Enum 是一种不那么老套的方式,我相信你正在使用 item.status

处理完我们的模型后,我们可以继续使用控制器:

class ItemsController

  before_action :set_item, only: [:show, :edit, :update, :destroy]

  def new
    @item = Item.new
    @item.user_items.new # seeds the form
  end

  def edit
    @item.user_items.new unless @item.user_items.any? # seeds the form
  end

  def create
    @item = Item.new(item_params) do |item|
      item.status = :pending
      item.creator = current_user
    end
    if @item.save
      redirect_to items_path, success: 'Oh yes'
    else
      render :new, error: 'Oh noes'
    end
  end

  def update
    if @item.update(item_params)
      redirect_to @item, success: 'Item updated'
    else
      render :edit, error: 'Oh noes'
    end
  end

  private 
    def set_item
      @item = Item.includes(:user_items).find(params[:id])
    end

    def item_params
      params.require(:item)
            .permit(:name, :description, :tag_list, user_items_attributes: [:picture])
    end
end

这是 MVC Rails 方式 - 模型负责验证和关联。我们使用强参数以可重用的方式将表单输入映射到模型。

最后让我们清理表单。我们将首先将表单提取到部分中,以便可以重复使用。

<% # views/items/_form.html.erb %>
<%= simple_form_for @item do |item_builder| %>
  <div class="well">
    <%= item_builder.input :name, require: false, label: "Item name" %>
    <%= item_builder.input :description, require: false, as: :text, label: "Describe item" %>
    <%= item_builder.input :tag_list, label: "Tags", require: false %>
    <%= item_builder.simple_fields_for :user_items do |user_item_builder| %>
    <%= user_item_builder.input :picture, as: :file, label: "Picture of you with this item" %>
  <% end %>
  </div>
  <div class="clearfix">
    <%= item_builder.submit %>
  </div>
<% end %>

然后我们可以将此表单用于newedit

<h1>Create a new item</h1>
<%= render partial: '_form' %>

<h1>Edit <%= @item.name %></h1>
<%= render partial: '_form' %>

【讨论】:

  • 请注意,如果您希望其他用户那么您需要重新考虑一下,然后创建该项目的用户才能使用项目提交他们自己的图片。这将需要不同的关系、路由和控制器。
猜你喜欢
  • 2011-10-07
  • 2011-08-21
  • 1970-01-01
  • 2015-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多