Rails upsert возвращаемое значение не будет работать для создания экземпляра | ActiveRecord::AssociationTypeMismatch

#ruby-on-rails #activerecord #upsert

#ruby-на-рельсах #activerecord #апсерт

Вопрос:

Пытаясь использовать выборку с Rails для сохранения данных формы в базе данных Postgre, я получаю одну загадочную ошибку за другой, и я совершенно не понимаю, как это решить. Все, что я пытаюсь сделать, это взять входные значения и сохранить их в двух разных таблицах.

Сервер выдает мне следующее:

   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'
 

Соответствующие строки включают upsert и create :

 # 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)
 

Ответ №1:

В 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 . Контроллер вернет идентификатор вновь созданного тега в теле ответа, и вы можете использовать элемент формы в форме, такой как флажки или выбор, для хранения идентификаторов тегов или набора скрытых входных данных (или пользовательских элементов / компонентов).

Механизм rails для назначения существующих записей чрезвычайно прост. Просто передайте массив _ids= установщику:

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

Комментарии:

1. Это невероятно полезно, спасибо. Я попытался использовать вложенные атрибуты, однако теперь ничего не сохраняется (ни проект, ни теги). В журнале сервера отображается ОТКАТ (см. Выше).

2. Я на самом деле не собираюсь пытаться помочь вам спасти это — я действительно не думаю, что этот код можно исправить, поскольку вы не рассмотрели такие проблемы, как работа с недопустимыми тегами или проблемы со сбоями при вставке тега или соединении строк. Весь подход просто неправильный. Если вы хотите создать несколько подобных записей, вам нужно подумать о таких вещах, как транзакции, и это на самом деле немного сложнее, чем вы себе представляете.

3. Я думаю, что должен быть более простой способ создать простую форму, которая сохраняется в двух разных таблицах. У вас есть статья, которая направила бы меня в правильном направлении?