diff --git a/README.md b/README.md index 7808fd2..17e6e94 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,37 @@ ActiveAdmin.register Post do end ``` -For databases that support upserts you can use `:on_duplicate_key_update` instead. +On databases that support upserts (MySQL, PostgreSQL 9.5+, SQLite 3.24+) you can +update colliding rows and insert new ones in a single pass with +`:on_duplicate_key_update` — no `delete_all` required: + +```ruby +ActiveAdmin.register Author do + # PostgreSQL / SQLite + active_admin_import validate: false, + on_duplicate_key_update: { + conflict_target: [:id], + columns: %i[name last_name birthday] + } +end +``` + +Notes: + +* The option shape is **adapter-specific**, since it is passed straight to + `activerecord-import`: + * PostgreSQL / SQLite need an explicit `:conflict_target` — the unique + column(s) used to detect a collision (`[:id]` for the primary key). + * MySQL infers the conflicting key, so pass just the column list and omit + `:conflict_target` (passing it raises `Unknown column 'conflict_target'`): + + ```ruby + on_duplicate_key_update: %i[name last_name birthday] + ``` +* Turn `validate` off for id-based upserts. `activerecord-import` runs + uniqueness validations against the very rows the upsert is about to overwrite, + so a model-level `validates_uniqueness_of` would otherwise reject the update. +* Active Record callbacks are not fired for bulk imports. ##### Tune batch size diff --git a/spec/import_spec.rb b/spec/import_spec.rb index 2c75df8..595acfb 100644 --- a/spec/import_spec.rb +++ b/spec/import_spec.rb @@ -217,6 +217,51 @@ def upload_file!(name, ext = 'csv') expect(Author.find(2).name).to eq('Jane') end end + + # Issue #187: update existing records by id without a delete_all workaround. + # On databases that support upserts, :on_duplicate_key_update lets a single + # import update colliding rows and insert new ones in one pass. + context 'upserting authors by id via :on_duplicate_key_update' do + before do + # Existing row shares its id with the first CSV row but carries a stale + # birthday; the second CSV row (id 2) has no match and must be inserted. + Author.delete_all + Author.create!(id: 1, name: 'John', last_name: 'Doe', birthday: '1900-01-01') + + # The option shape is adapter-specific: MySQL infers the conflicting key + # and only wants the column list, while PostgreSQL/SQLite need an explicit + # :conflict_target (see README). + on_duplicate_key_update = + if ActiveRecord::Base.connection.adapter_name.match?(/mysql/i) + %i[name last_name birthday] + else + { conflict_target: [:id], columns: %i[name last_name birthday] } + end + + add_author_resource( + # Uniqueness validation runs against the rows the upsert is about to + # overwrite, so it must be off for an id-based upsert (see README). + validate: false, + on_duplicate_key_update: on_duplicate_key_update + ) + visit '/admin/authors/import' + upload_file!(:authors_with_ids) + end + + it 'reports every row as imported' do + expect(page).to have_content 'Successfully imported 2 authors' + end + + it 'updates the existing author instead of duplicating it' do + expect(Author.count).to eq(2) + expect(Author.find(1).birthday).to eq(Date.new(1986, 5, 1)) + end + + it 'inserts the non-colliding author' do + expect(Author.find(2).name).to eq('Jane') + expect(Author.find(2).last_name).to eq('Roe') + end + end end context 'with valid options' do