diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index cb07ddd..a940f0b 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -200,14 +200,45 @@ class TagsController < ApplicationController .find_by(id: params[:id]) return head :not_found unless tag + rows = normalise_deerjikist_rows(tag) + return if performed? + ApplicationRecord.transaction do - tag.deerjikists = [] - params[:_json].each.with_index do |item, i| - platform = item[:platform] - code = normalise_deerjikist_code(platform, item[:code]) - deerjikist = Deerjikist.find_or_initialize_by(platform:, code:) - deerjikist.tag = tag - render_deerjikist_form_record_invalid(deerjikist, i) unless deerjikist.save + tag.lock! + + requested_keys = rows.map { |row| [row[:platform], row[:code]] }.uniq + row_indexes_by_key = rows_by_key(rows) + locked_deerjikists = lock_deerjikists_for_tag_update(tag.id, requested_keys) + current_deerjikists = locked_deerjikists.filter { |deerjikist| + deerjikist.tag_id == tag.id + } + requested_deerjikists = locked_deerjikists.filter { |deerjikist| + row_indexes_by_key.key?([deerjikist.platform, deerjikist.code]) + } + + render_deerjikist_conflicts(requested_deerjikists, row_indexes_by_key, tag) + raise ActiveRecord::Rollback if performed? + + requested_keys_set = requested_keys.to_set + current_deerjikists.each do |deerjikist| + key = [deerjikist.platform, deerjikist.code] + deerjikist.destroy! unless requested_keys_set.include?(key) + end + + existing_keys = requested_deerjikists.to_h { |deerjikist| + [[deerjikist.platform, deerjikist.code], true] + } + requested_keys.each do |platform, code| + next if existing_keys[[platform, code]] + + deerjikist = Deerjikist.new(platform:, code:, tag:) + row_index = row_indexes_by_key[[platform, code]].first + begin + render_deerjikist_form_record_invalid(deerjikist, row_index) unless deerjikist.save + rescue ActiveRecord::RecordNotUnique + conflicts = lock_deerjikists_for_tag_update(tag.id, [[platform, code]]) + render_deerjikist_conflicts(conflicts, row_indexes_by_key, tag) + end raise ActiveRecord::Rollback if performed? end end @@ -653,6 +684,7 @@ class TagsController < ApplicationController end def normalise_deerjikist_code platform, code + code = code.to_s return code if platform != 'youtube' || code[0] != '@' url = "https://www.youtube.com/#{ code }" @@ -669,6 +701,82 @@ class TagsController < ApplicationController nil end + def normalise_deerjikist_rows tag + rows = [] + + params[:_json].each.with_index do |item, index| + platform = item[:platform] + unless Deerjikist.platforms.key?(platform) + render_deerjikist_platform_invalid(index) + return rows + end + + code = normalise_deerjikist_code(platform, item[:code]) + deerjikist = Deerjikist.new(platform:, code:, tag:) + unless deerjikist.valid? + render_deerjikist_form_record_invalid(deerjikist, index) + return rows + end + + rows << { index:, platform:, code: } + end + + rows + end + + def lock_deerjikists_for_tag_update tag_id, keys + clauses = ['tag_id = ?'] + values = [tag_id] + + keys.each do |platform, code| + clauses << '(platform = ? AND code = ?)' + values << platform << code + end + + Deerjikist + .where(clauses.join(' OR '), *values) + .order(:platform, :code) + .lock + .to_a + end + + def render_deerjikist_conflicts deerjikists, row_indexes_by_key, tag + conflicts = deerjikists.filter { |deerjikist| deerjikist.tag_id != tag.id } + return if conflicts.empty? + + tag_names_by_id = Tag + .joins(:tag_name) + .where(id: conflicts.map(&:tag_id).uniq) + .pluck('tags.id', 'tag_names.name') + .to_h + fields = { } + + conflicts.each do |deerjikist| + message = "この情報は既に「#{ tag_names_by_id[deerjikist.tag_id] }」に紐づいてゐます." + row_indexes_by_key[[deerjikist.platform, deerjikist.code]].each do |index| + field = :"deerjikists.#{ index }.code" + fields[field] ||= [] + fields[field] << message + end + end + + render_validation_error fields: + end + + def render_deerjikist_platform_invalid index + render_validation_error fields: { + :"deerjikists.#{ index }.platform" => ['値が不正です.'], + } + end + + def rows_by_key rows + rows.each_with_object({ }) do |row, result| + key = [row[:platform], row[:code]] + result[key] ||= [] + result[key] << row[:index] + end + end + def render_deerjikist_form_record_invalid deerjikist, index fields = { }