Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions spec/import_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading