Обеспечение уникальности записи в базе данных Rails при обновлении без прерывания процесса обновления

Ruby 2.3.0, Rails 4.2.4 и на самом деле с использованием postgreSQL, а не SQLite

Обновлено для ясности

У меня есть большой файл csv (обновляется извне и загружается ежедневно), и я написал метод обновления таблицы базы данных Rails. Я не хочу, чтобы метод добавлял все строки в базу данных без проверки уникальности, поэтому я использую это отличное решение (Как сделать столбец уникальным и проиндексировать его при миграции Ruby on Rails?) с помощью add_index.

Я использую rake-файл для хранения исполняемого кода обновления и ввожу $ rake update_task в свой терминал (который работает, ЕСЛИ в таблице нет дубликатов с импортированными строками csv). Проблема заключается в том, что база данных ВЫКЛЮЧАЕТ (rake aborted!) грабли, когда встречает первую повторяющуюся запись (ERROR: duplicate key value violates unique constraint).

Что я могу сделать, чтобы удалить / не сохранять дубликаты, избегая при этом прерывания / сбоя? Я не могу просто отбрасывать таблицу базы данных и перезагружать ее каждый день. Вот схема:

ActiveRecord::Schema.define(version: 20160117172450) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

  create_table "tablename", force: :cascade do |t|
    t.string   "attr1"
    t.string   "attr2"
    t.string   "attr3"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_index "tablename", ["attr1", "attr2", "attr3"], name: "index_tablename_on_attr1_and_attr2_and_attr3", unique: true, using: :btree

end

и моя задача rake в содержимом lib / tasks / file_name.rake:

desc "Download data and update database table"

task :update_task => :environment do
  u = CorrectClassName.new
  u.perform_this
end

и CorrectClassName находится в файле .rb в app / directory1:

class CorrectClassName

  def perform_this
    something = ClassWithUpdateCode.new
    something.update_database
  end

end

и ClassWithUpdateCode находится в файле .rb в app / directory2:

require 'csv'

class ClassWithUpdateCode

  def update_database
    csv_update = File.read(Rails.root.join('lib', 'assets', "file_name.csv"))
    options = {:headers => true}

    csv = CSV.parse(csv_update, options)
    csv.each do |row|
        tm = TableModel.new

        tm.attr1 = row[0]
        tm.attr2 = row[1]
        tm.attr3 = row[2]
        tm.save # maybe I can use a different method or if statement here?
    end
  end

end

Обновление: решение @Kristan работает ниже, но вот где разместить обработку начала / восстановления / завершения:

В файле .rb в app / directory2:

require 'csv'

class ClassWithUpdateCode

  def update_database
    csv_update = File.read(Rails.root.join('lib', 'assets', "file_name.csv"))
    options = {:headers => true}

    csv = CSV.parse(csv_update, options)
    csv.each do |row|
        tm = TableModel.new
        begin
          tm.attr1 = row[0]
          tm.attr2 = row[1]
          tm.attr3 = row[2]
          tm.save
        rescue ActiveRecord::RecordNotUnique
        end
    end
  end

end

person JHFirestarter    schedule 23.01.2016    source источник
comment
Какую версию PostgreSQL вы используете? 9.5 добавляет поддержку. INSERT ... ON CONFLICT DO NOTHING   -  person Thomas Walpole    schedule 23.01.2016
comment
Это может сработать (я использую 9.5), Том ... Я просто не пробовал раньше использовать чистый SQL! В какой-то момент в будущем мне нужно будет ускориться, не используя синтаксический анализ csv и, вероятно, иметь / copy from и, возможно, ваше предложение SQL здесь.   -  person JHFirestarter    schedule 24.01.2016


Ответы (1)


rake выходит из строя, потому что возникает исключение, когда вы пытаетесь сохранить запись, которая нарушает ограничение уникальности вашей таблицы. Самый простой способ предотвратить это - перехватить и проигнорировать исключение. Я предполагаю, что ваша запись создана во время u.perform_this.

task :update_task => :environment do
  u = CorrectClassName.new
  begin
    u.perform_this
  rescue ActiveRecord::RecordNotUnique
    # move on
  end
end

Другой вариант - добавить проверку уникальности в вашу модель Rails, затем либо проверьте valid? перед сохранением, либо вызовите create (не create!), что не вызывает исключений проверки.

class CorrectClassName < ActiveRecord::Base
  validates_uniqueness_of :attr1, scope: [:attr2, :attr3]
end
task :update_task => :environment do
  u = CorrectClassName.new(data)
  u.perform_this if u.valid?
end
person Kristján    schedule 23.01.2016
comment
Я, вероятно, был слишком многословен и также должен был включить базовый метод в perform_this. Решение должно (а) учитывать проверку уникальности без (б) создания исключений и прерывания / сбоя. Когда я попробовал начать / спасти / закончить решение, (б) был решен, но не (а) - теперь в таблице есть дубликаты. То же самое с if u.valid? (даже если u.valid? используется вместе с методом perform_this .save ... который я только что обновил, если есть решение). - person JHFirestarter; 23.01.2016
comment
Если у вас есть уникальный индекс базы данных для этих атрибутов, невозможно получить дубликаты. Вы уверены, что правильно настроили? - person Kristján; 24.01.2016
comment
Ах, интересно ... Мне нужно было удалить attr3 как ограничение уникальности - спасибо, что помог мне это найти! Тем не менее, без оскорбительного ограничения attr3 обработка begin / rescue / end устраняет возникающие исключения ... но таблица не обновляется (так что задача rake каким-то образом прерывается). Есть ли эквивалентный переход к следующему синтаксису, который я должен использовать между rescue и end? - person JHFirestarter; 24.01.2016
comment
Кроме того, по-прежнему существует ошибка решения .valid?. Мой собственный класс CorrectClassName не распознается при наследовании ActiveRecord :: Base ... Я получаю PG::UndefinedTable: ERROR: relation "file_name" does not exist (где имя_файла - это .rb в app / directory1 в моем коде выше, НЕ в app / models). - person JHFirestarter; 24.01.2016
comment
Ага! Хорошо, когда я пытался быть кратким в вопросе, я усложнил для вас ответ. Ключ в том, что вы сказали, что я предполагаю, что ваша запись создана во время u.perform_this. Приношу свои извинения, запись фактически создается внутри метода update_database (в классе ClassWithUpdateCode выше). Итак, begin rescue end работает, когда я помещаю его в цикл do этого метода. - person JHFirestarter; 24.01.2016