#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. Я думаю, что должен быть более простой способ создать простую форму, которая сохраняется в двух разных таблицах. У вас есть статья, которая направила бы меня в правильном направлении?