首页 > 解决方案 > Rails upsert 返回值无法创建实例 | ActiveRecord::AssociationType 不匹配

问题描述

尝试使用 fetch 和 Rails 将表单数据保存到 Postgre DB,我在下一个之后被抛出一个神秘的错误,我完全不知道如何解决这个问题。我要做的就是获取输入值并将它们保存到两个不同的表中。

服务器向我抛出以下内容:

  Tag Upsert (4.7ms)  INSERT INTO "tags" ("category","name") VALUES ('topic', 'abc') ON CONFLICT ("id") DO UPDATE SET "category"=excluded."category","name"=excluded."name" RETURNING "id"
  ↳ app/controllers/projects_controller.rb:36:in `block in create'
Completed 500 Internal Server Error in 180ms (ActiveRecord: 54.1ms | Allocations: 28146)



ActiveRecord::AssociationTypeMismatch (Tag(#70111841352580) expected, got #<ActiveRecord::Result:0x00007f8861ca8410 @columns=["id"], @rows=[[12]], @hash_rows=nil, @column_types={"id"=>#<ActiveModel::Type::Integer:0x00007f885e5608e0 @precision=nil, @scale=nil, @limit=8, @range=-9223372036854775808...9223372036854775808>}> which is an instance of ActiveRecord::Result(#70111841508420)):

app/controllers/projects_controller.rb:40:in `block in create'
app/controllers/projects_controller.rb:31:in `each'
app/controllers/projects_controller.rb:31:in `create'

相关线路涉及upsertcreate

# projects_controller.rb
...
def create
   @project = Project.new(project_params)
    if @project.save
      params[:tags].each do |tag|
         @tag = Tag.upsert({
                            category: 'topic',
                            name: tag
                          })
        ProjectTag.create(tag: @tag, project: @project)
      end
      respond_to do |format|
        format.json { render json: { "message": "success!", status: :ok } }
      end
    else
      respond_to do |format|
        format.json { render json: { "errors": "Missing entries." } }
      end
    end
end
...

这是表单提交时启动的获取请求:

fetch(`../projects/`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Accept': 'application/json',
              'X-Transaction': 'POST Example',
              'X-Requested-With': 'XMLHttpRequest',
              'X-CSRF-Token': document.querySelector("[name='csrf-token']").content, // $('meta[name="csrf-token"]').attr('content'),
            },
            body: JSON.stringify({project: project, tags: tags}),
            credentials: 'include'
          })
          .then(response => {
            if (!response.ok) {
              throw response;
            }
            return response.json();
          })
          .then(data => {
            if (data.errors) {
              alert(`${data.errors}`);
            } else {
              console.log('Success:', data);
              alert('Saved.');
            }
          })
          .catch(error => {
            console.error('Error:', error);
            alert('error', data.errors);
          });

标签模型如下所示:

# tag.rb
class Tag < ApplicationRecord
  has_many :project_tags
  has_many :issue_tags

  validates :name, uniqueness: true
end

更新以响应最大值

Started POST "/projects/" for ::1 at 2021-01-04 00:42:41 +0100
Processing by ProjectsController#create as JSON
  Parameters: {"project"=>{"name"=>"Project 7", "tags_attributes"=>[{"name"=>"career_planning", "category"=>"topic"}, {"name"=>"angular", "category"=>"topic"}], "language"=>"Angular", "slogan"=>"xyz", "target"=>nil, "pain"=>nil, "solution"=>nil, "originality"=>nil, "vision"=>nil, "db_design_url"=>nil, "repo_url"=>nil, "proto_url"=>nil}, "tags"=>["career_planning", "angular"]}
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 1], ["LIMIT", 1]]
   (0.2ms)  BEGIN
  ↳ app/controllers/projects_controller.rb:35:in `block (2 levels) in create'
  Tag Exists? (0.4ms)  SELECT 1 AS one FROM "tags" WHERE "tags"."name" = $1 LIMIT $2  [["name", "career_planning"], ["LIMIT", 1]]
  ↳ app/controllers/projects_controller.rb:35:in `block (2 levels) in create'
  Tag Exists? (0.3ms)  SELECT 1 AS one FROM "tags" WHERE "tags"."name" = $1 LIMIT $2  [["name", "angular"], ["LIMIT", 1]]
  ↳ app/controllers/projects_controller.rb:35:in `block (2 levels) in create'
  Project Exists? (0.2ms)  SELECT 1 AS one FROM "projects" WHERE "projects"."name" = $1 LIMIT $2  [["name", "Project 7"], ["LIMIT", 1]]
  ↳ app/controllers/projects_controller.rb:35:in `block (2 levels) in create'
   (0.2ms)  ROLLBACK
  ↳ app/controllers/projects_controller.rb:35:in `block (2 levels) in create'
Completed 200 OK in 16ms (Views: 0.2ms | ActiveRecord: 1.7ms | Allocations: 9221)

标签: ruby-on-railsactiverecordupsert

解决方案


在 Rails 中,如果你想在同一个请求中创建记录和嵌套记录,你可以使用嵌套属性

class Project < ApplicationRecord
  has_many :tags
  accepts_nested_attributes_for :tags
end

这将允许您通过简单地传递一组属性来创建项目和标签:

Project.new(
  name: 'Learn Nested Attributes',
  tags_attributes: [
    { name: 'Ruby' },
    { name: 'Ruby On Rails' }
  ]
)

然后,当您插入父项时,它将有效地创建单个插入查询而不是 n+1 查询问题,并且它还允许您以理智的方式处理嵌套记录中的验证错误。您可以使用该reject_if:选项或自定义设置器来处理现有记录。

您的控制器应该看起来像:

def create
  @project = Project.new(project_params)   
  respond_to do |format|
    format.json do 
      # Check if the record is actually persisted! Not just `.valid?`
      if @project.save
        # You should return meaningful response codes instead of 
        # just using the "json messages" anti-pattern
        render json: { "message": "success!" }, status: :created }
      else
        format.json { render json: { "errors": "Missing entries." }, status: :unprocessable_entity }
      end
    end
  end
end

private

def project_params
  params.require(:project)
        .permit(:foo, :bar, tags_attributes: [:name] )
end

在 Rails 中,控制器不是复杂的地方,因为它们很难测试。

然而,嵌套属性实际上并不是最好的 UX 解决方案。如果您改为创建单独的端点来创建标签并发送单独的 AJAX 请求,您可以在用户输入标签名称时提供直接用户反馈,或者自动完成现有标签,或者在标签无效时提供直接验证反馈。

resources :tags, only: [:index, :create]
class TagsController < ApplicationController
  # GET /tags
  # GET /tags?search=foobarbaz
  def index
    @tags = if params[:search].present? 
        Tag.where('tags.name LIKE ?', "%#{params[:search]}%")
      else
        Tag.all
      end
    end
    render json: @tags
  end

  # POST /tags
  def create
    @tag = Tag.new(tag_params)
    if @tag.save
      render json: @tag, 
             status: :created
    else
      render json: { errors: @tag.errors.full_messages }, 
             status: :unprocessable_entity
    end
  end

  private
  def tag_params
    params.require(:tag)
          .permit(:name)
  end
end

然后,您可以编写一个 AJAX 处理程序*,将请求发送/tags?search=foobarbaz到自动完成并通过向POST /tags. 控制器将在响应正文中返回新创建的标签的 id,您可以使用表单中的表单元素,例如复选框或选择来存储标签 id 或一组隐藏的输入(或自定义元素/组件)。

分配现有记录的 rails 机制非常简单。只需将数组传递给_ids=setter:

def project_params
  params.require(:project)
        .permit(:foo, :bar, tag_ids: [])
end

推荐阅读