Как я могу определить, нарушает ли мой объект ActiveRecord уникальный ключ / индекс базы данных? - PullRequest
6 голосов
/ 07 апреля 2009

ActiveRecord validates_uniqueness_of является уязвимым для условий гонки . Чтобы действительно обеспечить уникальность, требуются дополнительные гарантии. Одним из предложений ActiveRecord RDocs является создание уникального индекса в базе данных, например, путем включения в ваши миграции:

add_index :recipes, :name, :unique => true

Это обеспечит на уровне базы данных, что имя уникально. Но недостатком этого подхода является то, что исключение ActiveRecord::StatementInvalid, возвращаемое при попытке сохранить дубликат, не очень полезно. При отлове этого исключения нельзя быть уверенным, что ошибка возникла из-за повторяющейся записи, а не только из-за неработающего SQL.

Одним из решений, как предлагают RDocs, является анализ сообщения, которое приходит с исключением, и попытка обнаружить слова, такие как «дубликаты» или «уникальные», но это глупо, и сообщение является специфическим для базы данных. Для SqlLite3 мое понимание состоит в том, что сообщение является полностью общим и не может быть проанализировано таким образом.

Учитывая, что это фундаментальная проблема для пользователей ActiveRecord, было бы неплохо узнать, существует ли какой-либо стандартный подход к обработке этих исключений. Я предложу свое предложение ниже; пожалуйста, прокомментируйте или предоставьте альтернативы; спасибо!

Ответы [ 2 ]

7 голосов
/ 07 апреля 2009

Синтаксический анализ сообщения об ошибке не так уж и плох, но выглядит глупо. Предложение, с которым я столкнулся (не помню, где), которое кажется привлекательным, состоит в том, что в блоке спасения вы можете проверить базу данных, чтобы увидеть, есть ли на самом деле дублирующая запись. Если есть, то есть вероятность, что StatementInvalid из-за дубликата, и вы можете обработать его соответствующим образом. Если нет, то StatementInvalid должен быть из чего-то другого, и вы должны обращаться с ним по-другому.

Итак, основная идея, предполагая уникальный индекс для recipe.name, как указано выше:

begin
  recipe.save!
rescue ActiveRecord::StatementInvalid
  if Recipe.count(:conditions => {:name => recipe.name}) > 0
    # It's a duplicate
  else
    # Not a duplicate; something else went wrong
  end
end

Я попытался автоматизировать эту проверку с помощью следующего:

class ActiveRecord::Base
  def violates_unique_index?(opts={})
    raise unless connection
    unique_indexes = connection.indexes(self.class.table_name).select{|i|i.unique}
    unique_indexes.each do |ui|
      conditions = {}
      ui.columns.each do |col|
        conditions[col] = send(col)
      end
      next if conditions.values.any?{|c|c.nil?} and !opts[:unique_includes_nil]
      return true if self.class.count(:conditions => conditions) > 0
    end
    return false
  end
end

Так что теперь вы сможете использовать generic_record.violates_unique_index? в своем блоке спасения, чтобы решить, как обрабатывать StatementInvalid.

Надеюсь, это полезно! Другие подходы?

2 голосов
/ 07 апреля 2009

Это действительно такая большая проблема?

Если вы используете уникальный индекс вместе с ограничением validates_uniqueness_of, то

  • Сохранение целостности данных
  • Вы будете в худшем случае только получить ошибку, когда два отдельных запросы попробуйте вставить неуникальный грести одновременно

Так что, если у вас нет приложения, которое выполняет много потенциальных дубликатов вставок (в этом случае я бы посмотрел на его переработку), я вижу, что на практике это редко является проблемой.

...