Избегайте исключения при попытке добавить запись с повторяющимся индексом в rails

#ruby-on-rails

#ruby-on-rails

Вопрос:

У меня есть 3 модели:

  • Пользователь
  • Головоломка
  • Решение

Решение представляет собой первый раз, когда пользователь решил данную головоломку. Я хочу записать только в первый раз — последующие попытки не учитываются. Решение имеет индекс для комбинации user-puzzle.

Моя миграция выглядит так:

 class CreateSolutions < ActiveRecord::Migration[6.0]
  def change
    create_table :solutions do |t|
      t.integer :time
      t.integer :attempts
      t.references :user
      t.references :puzzle

      t.timestamps
    end
    add_index :solutions, [:user_id, :puzzle_id], unique: true
  end
end
  

Я использую MySQL. Модели Puzzle и User имеют: has_many :solutions .

В solutions_controller.rb , у меня есть:

   def create
    @solution = Solution.new(solution_params)
    @solution.user = current_user
    @solution.puzzle = Puzzle.find(params["puzzle"])

    respond_to do |format|
      if @solution.save
        format.json { render :show, status: :created }
      else
        format.json { render json: @solution.errors, status: :unprocessable_entity }
      end
    end
  end
  

Проблема в том, что во второй раз, когда я пытаюсь сохранить головоломку, я получаю исключение:

 ActiveRecord::RecordNotUnique in SolutionsController#create
Mysql2::Error: Duplicate entry '28-13' for key 'index_solutions_on_user_id_and_puzzle_id'
  

Цель: я бы хотел, чтобы он просто молча игнорировал последующие сохранения, а не создавал исключение.

Вот что я пробовал — все в solutions_controller.rb:

  • if @solution.save(validate: false)...
  • if @solution.save(:validate => false)...
  • if @solution.save(false)...

В идеале я хочу пропустить проверку только для повторяющегося индекса — я бы хотел перехватить другие ошибки.

Ответ №1:

В модели решения вы можете добавить

 validates_uniqueness_of :user_id, scope: :puzzle_id
  

Он будет запускать запрос select каждый раз, когда вы пытаетесь сохранить запись решения. Если запись существует для решения запроса select.сохранение выдаст ошибку.

Или вы могли бы использовать first_or_initialize

 Solution.where(user_id: current_user.id, puzzle_id: params[:puzzle][:id]).first_or_initialize(solution_params)
  

Это проверит, существует ли запись для данного запроса, если она не существует, она присвоит атрибуты в условии where и solution_params .

Обновить

 def create
    @solution = Solution.where(user_id: current_user.id, puzzle_id: params[:puzzle][:id]).first_or_initialize(solution_params)

    respond_to do |format|
      if @solution.save
        format.json { render :show, status: :created }
      else
        format.json { render json: @solution.errors, status: :unprocessable_entity }
      end
    end
  end
  

если вы сделаете это таким образом, нет необходимости проверять сообщение об ошибке, если запись уже существует в таблице, она будет присвоена @solution, а данные в параметрах решения не будут присвоены. Если запись не существует, @solution будет присвоен с данными в where condition и solution_params(значения user_id и puzzle_id будут взяты из того, что указано в where condition, а другие — из solution_params). Если для атрибута данные указаны в where condition и solution_params, то будет принято значение solution_params.

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

1. Спасибо — первый validates_uniqueness_of подход, похоже, улавливает ошибку, но он может сработать. Я мог бы проверить @solution.errors.messages[:user_id] , «ли это уже принято», и игнорировать только этот случай.

2. Второй подход тоже работает, но меня это смущает. Это работает, только если я все еще инициализирую модель с @solution = Solution.new(solution_params) помощью etc. в контроллере. Но я не вижу, где я на самом деле использую @solution то, что я инициализировал. Мой блок if теперь выглядит так: if Solution.where(user_id: current_user.id, puzzle_id: @solution.puzzle.id).first_or_initialize(solution_params) ... — Я @solution.save больше не звоню.

3. Я обновил свой ответ, пожалуйста, проверьте его. Я полагаю, вы не хотите обновлять запись, если она уже существует правильно.

4. Теперь я вижу — еще раз спасибо. Единственное, что мне пришлось сделать иначе, чем в вашем примере, — указать puzzle_id: params[:puzzle] вместо params[:puzzle][:id] . params это просто параметры POST, поэтому они представляют собой простые пары имя / значение. Если нет способа, которым объект головоломки должен расширяться, которого мне не хватает?

5. моя ошибка запуталась, в идеале она должна быть отправлена в объект решения как puzzle_id. поскольку он назван puzzle, я подумал, что это может быть объект puzzle, исходящий из параметров.