Rails嵌套属性不是从隐藏表单输入中的JSON字符串创建对象

时间:2014-10-20 20:29:57

标签: json ruby-on-rails-4 nested-attributes strong-parameters

您好我有一个表格包,我填写表格并使用参数。此表有一个FK到另一个表位置,用于保存包的lat,lng和地址。 Location表使用GeoKit。

我的表单包含包的字段和允许用户输入位置名称的字段。 Google地图可帮助用户使用自动填充功能填写详细信息,并将结果以json的形式保存在隐藏字段中。

我正在尝试使用强大的参数

private
def package_params
  params.require(:package).permit( :state, :delivery_date, :length, :height, :width, :weight, destination: [:id, :address, :lat, :lng], origin: [:id, :address, :lat, :lng] )
end

我也试过

private
def package_params
  params.require(:package).permit( :state, :delivery_date, :length, :height, :width, :weight, destination_attributes: [:id, :address, :lat, :lng], origin_attributes: [:id, :address, :lat, :lng] )
end

但是起源&目标_attributes不再在params的包对象中传递。

包模型

class Package < ActiveRecord::Base
  belongs_to :user
  has_many :bids, dependent: :destroy

  belongs_to :origin, :class_name => 'Location', :foreign_key => 'origin'
  belongs_to :destination, :class_name => 'Location', :foreign_key => 'destination'
  has_many :locations, autosave: true

  accepts_nested_attributes_for :origin, :destination
  ....
end

位置模型是

class Location < ActiveRecord::Base
    acts_as_mappable

    validates :address, presence: true
    validates :lat, presence: true
    validates :lng, presence: true
end

create方法是

def create
    @package = current_user.packages.build(package_params)
    if @package.save
......
end

package.save失败了。这是我收到的错误。

  

PackagesController #create中的ActiveRecord :: AssociationTypeMismatch   位置(#70350522152300)预计,得到字符串(#70350507797560)

我可以想到几个解决方法,但我想让它工作,所以我从中学习。我已经尝试过阅读rails api并在谷歌上搜索了几天,但我还是没能把它弄得太干净了。

帖子数据是

  Parameters: {
      "utf8"=>"✓", 
      "authenticity_token"=>"ZYkfpQBu6fvX7ZmzRw2bjkU+3i6mH0M7JLeqG4b99WI=",
      "origin_input"=>"Kimmage, Dublin, Ireland", 
      "package"=>{
          "origin"=>"{
                      \"address\":\"Kimmage, Dublin, Ireland\",
                      \"lat\":53.32064159999999,
                      \"lng\":-6.298185999999987}",
          "destination"=>"{
                           \"address\":\"Lucan, Ireland\",
                           \"lat\":53.3572085,
                           \"lng\":-6.449848800000041}", 
          "length"=>"22", 
          "width"=>"222", 
          "height"=>"22", 
          "weight"=>"0 -> 5", 
          "delivery_date"=>"2014-10-31"}, 
      "destination_input"=>"Lucan, Ireland", 
      "commit"=>"Post"}

我知道原点和目的地没有被反序列化,但我不知道它们为什么不是。我是否必须手动反序列化字符串,我可以在package_params中执行此操作吗?

创建此内容的表单如下

<%= form_for(@package, :html => {:class => "form-horizontal", :role => 'form'}) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="form-group">
    <input type="text" name="origin_input" placeholder="From" onFocus="geolocate(); autocompleteLocation(this,package_origin)" class="form-control" />
    <%= f.hidden_field :origin, class: "form-control" %>
  </div>
  <div class="form-group">
    <input type="text" name="destination_input" placeholder="Destination" onFocus="geolocate(); autocompleteLocation(this,package_destination)" class="form-control" />
    <%= f.hidden_field :destination, class: "form-control" %>
  </div>

  <div class="form-inline form-group">
    <div class="input-group col-md-3">
        <%= f.text_field :length, placeholder: "L", class: "form-control" %>
        <span class="input-group-addon">cm</span>
    </div>

  <div class="input-group col-md-3">
    <%= f.text_field :width, placeholder: "W", class: "form-control" %>
    <span class="input-group-addon">cm</span>
  </div>

  <div class="input-group col-md-3">
    <%= f.text_field :height, placeholder: "H", class: "form-control" %>
    <span class="input-group-addon">cm</span>
  </div>
</div>

   <div class="form-group input-group">
    <p>Please select the weight range of your package, Weights are in kg</p>
    <% options = options_from_collection_for_select(@weights, 'weight', 'weight') %>
    <%= f.select :weight,  options, class: "form-control dropdown" %>
   </div>

   <div class="form-group">
    <%= f.date_field :delivery_date, class: "form-control" %>
   </div>
   <%= f.submit "Post", class: "btn btn-large btn-primary", id: "package_post" %>
<% end %>
<%= render 'shared/places_autocomplete' %>

1 个答案:

答案 0 :(得分:4)

问题

您收到的错误AssociationTypeMismatch是由于您在strong_params中添加了origin:destination:。 Rails认为你试图关联对象就像你做@post.comment = @comment一样。

即使正确的序列化&amp;你的params反序列化这种方法不起作用。 Rails会看到你正在尝试使用strong_params:

# Not deserialized
@package.origin = '{ \"address\":\"Kimmage, Dulbin, Ireland\", ... }'

# Deserialized. However, this still won't work.
@package.origin = { address: "Kimmage, Dublin, Ireland", ...}

Rails在两种情况下都想要一个对象。您可以使用正确反序列化的案例进入控制台进行测试:

$ rails c

irb(main): p = Package.new
irb(main): p.destination = { address: "Kimmage, Dublin, Ireland" } # => Throws ActiveRecord::AssociationTypeMismatch.

那么,为什么它不起作用?因为Rails不是将它传递给实际对象,而是将您传递的内容解释为字符串或散列。为了通过strong_params关联对象,Rails会查找并使用accepts_nested_attributes方法(您已经尝试过)。但是,如下所述,这不会为您解决问题。

此处的问题是您尝试关联数据的方式。使用接受嵌套属性是通过父对象关联和保存子对象。在您的情况下,您尝试使用accepts_nested_attributes_for方法通过子对象(包)关联并保存两个父对象(origin和amp; destination)。 Rails不会以这种方式工作。

来自docs的第一行(强调我的):

  

嵌套属性允许您通过父保存关联记录的属性。

在您的代码中,您尝试通过孩子关联并保存/更新


解决方案

解决方案1 ​​

您需要的是表单中的origin_idlocation_id,不包括模型中的accepts_nested_attributes,因为您不再需要它,然后使用ID&#39; S:

params.require(:package).permit(:width, :length, :height, :whatever_else, :origin_id, :location_id)

然后,在提交表单之前使用AJAX请求,将这两个位置的origin_iddestination_id插入隐藏字段。如果它们不存在,您可以使用find_or_create_by方法在检索时创建这些位置。

解决方案2

  • 在控制器的before_action中查找或创建父资源@destination & @origin
  • @origin@destination@package
  • 相关联

你不需要accept_nested_attributes_for任何东西。您可以像平常一样保存包(确保修改package_params)。


class PackagesController < ApplicationController
  before_action :set_origin, only: [:create]
  before_action :set_destination, only: [:create]

  def create
    @package = current_user.packages.build(package_params)
    @package.destination = @destination
    @package.origin = @origin
    if @package.save
      # Do whatever you need
    else
      # Do whatever you need
    end
  end

private
  # Create the package like you normally would
  def package_params
    params.require(:package).permit( :state, :delivery_date, :length, :height, :width, :weight)
  end

  def set_origin
    # You can use Location.create if you don't need to find a previously stored origin
    @origin = Location.find_or_create_by(
      address: params[:package][:origin][:address],
      lat: params[:package][:origin][:lat],
      lng: params[:package][:origin][:lng],
    )
  end

  def set_destination
    # You can use Location.create if you don't need to find a previously stored destination
    @destination = Location.find_or_create_by(
      address: params[:package][:destination][:address],
      lat: params[:package][:destination][:lat],
      lng: params[:package][:destination][:lng],
    )
  end
end

要确保您拥有包含有效来源和目的地的套餐,请在模型中验证:

class Package < ActiveRecord::Base
  validates :origin, presence: true
  validates :destination, presence: true

  validates_associated :origin, :destination
end