activerecord-import 0.27.0 → 0.28.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.markdown +498 -3
- data/lib/activerecord-import/import.rb +59 -23
- data/lib/activerecord-import/version.rb +1 -1
- data/test/import_test.rb +40 -2
- data/test/models/user_token.rb +1 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +20 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbd7badbb2e98c6725ddaf5edbb713c4f79bfc01
|
4
|
+
data.tar.gz: 0912df3959070820db94063d0172a43f4b027a9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22fb70781c58df1795c41e5b8e954b8a89e6c85be0dbf33f2c2fb9265847778f99f414cbd7fe9ea945d6aa43f89d7557ea41541f938fa357a7bbcb2c0e6114df
|
7
|
+
data.tar.gz: b480446464f626518ac128b2e79d4e4dbb2b50e8e52d6781812a0ee1594e5328430214ad03be9590c09dcfd07661ae54130b53ccde1b9b7d2131d2f40c80a633
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
## Changes in 0.28.1
|
2
|
+
|
3
|
+
### Fixes
|
4
|
+
|
5
|
+
* Fix issue where ActiveRecord presence validations were being mutated.
|
6
|
+
Limited custom presence validation to bulk imports.
|
7
|
+
|
8
|
+
## Changes in 0.28.0
|
9
|
+
|
10
|
+
### New Features
|
11
|
+
|
12
|
+
* Allow updated timestamps to be manually set.Thanks to @Rob117, @jkowens via \#570.
|
13
|
+
|
14
|
+
### Fixes
|
15
|
+
|
16
|
+
* Fix validating presence of belongs_to associations. Existence
|
17
|
+
of the parent record is not validated, but the foreign key field
|
18
|
+
cannot be empty. Thanks to @Rob117, @jkowens via \#575.
|
19
|
+
|
1
20
|
## Changes in 0.27.0
|
2
21
|
|
3
22
|
### New Features
|
data/README.markdown
CHANGED
@@ -21,12 +21,401 @@ and then the reviews:
|
|
21
21
|
That would be about 4M SQL insert statements vs 3, which results in vastly improved performance. In our case, it converted
|
22
22
|
an 18 hour batch process to <2 hrs.
|
23
23
|
|
24
|
+
The gem provides the following high-level features:
|
25
|
+
|
26
|
+
* activerecord-import can work with raw columns and arrays of values (fastest)
|
27
|
+
* activerecord-import works with model objects (faster)
|
28
|
+
* activerecord-import can perform validations (fast)
|
29
|
+
* activerecord-import can perform on duplicate key updates (requires MySQL or Postgres 9.5+)
|
30
|
+
|
24
31
|
## Table of Contents
|
25
32
|
|
33
|
+
* [Examples](#examples)
|
34
|
+
* [Introduction](#introduction)
|
35
|
+
* [Columns and Arrays](#columns-and-arrays)
|
36
|
+
* [Hashes](#hashes)
|
37
|
+
* [ActiveRecord Models](#activerecord-models)
|
38
|
+
* [Batching](#batching)
|
39
|
+
* [Recursive](#recursive)
|
40
|
+
* [Options](#options)
|
41
|
+
* [Duplicate Key Ignore](#duplicate-key-ignore)
|
42
|
+
* [Duplicate Key Update](#duplicate-key-update)
|
43
|
+
* [Return Info](#return-info)
|
44
|
+
* [Counter Cache](#counter-cache)
|
45
|
+
* [ActiveRecord Timestamps](#activerecord-timestamps)
|
26
46
|
* [Callbacks](#callbacks)
|
47
|
+
* [Supported Adapters](#supported-adapters)
|
27
48
|
* [Additional Adapters](#additional-adapters)
|
49
|
+
* [Requiring](#requiring)
|
50
|
+
* [Autoloading via Bundler](#autoloading-via-bundler)
|
51
|
+
* [Manually Loading](#manually-loading)
|
28
52
|
* [Load Path Setup](#load-path-setup)
|
53
|
+
* [Conflicts With Other Gems](#conflicts-with-other-gems)
|
29
54
|
* [More Information](#more-information)
|
55
|
+
* [Contributing](#contributing)
|
56
|
+
* [Running Tests](#running-tests)
|
57
|
+
|
58
|
+
### Examples
|
59
|
+
|
60
|
+
#### Introduction
|
61
|
+
|
62
|
+
Without `activerecord-import`, you'd write something like this:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
10.times do |i|
|
66
|
+
Book.create! :name => "book #{i}"
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
This would end up making 10 SQL calls. YUCK! With `activerecord-import`, you can instead do this:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
books = []
|
74
|
+
10.times do |i|
|
75
|
+
books << Book.new(:name => "book #{i}")
|
76
|
+
end
|
77
|
+
Book.import books # or use import!
|
78
|
+
```
|
79
|
+
|
80
|
+
and only have 1 SQL call. Much better!
|
81
|
+
|
82
|
+
#### Columns and Arrays
|
83
|
+
|
84
|
+
The `import` method can take an array of column names (string or symbols) and an array of arrays. Each child array represents an individual record and its list of values in the same order as the columns. This is the fastest import mechanism and also the most primitive.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
columns = [ :title, :author ]
|
88
|
+
values = [ ['Book1', 'FooManChu'], ['Book2', 'Bob Jones'] ]
|
89
|
+
|
90
|
+
# Importing without model validations
|
91
|
+
Book.import columns, values, :validate => false
|
92
|
+
|
93
|
+
# Import with model validations
|
94
|
+
Book.import columns, values, :validate => true
|
95
|
+
|
96
|
+
# when not specified :validate defaults to true
|
97
|
+
Book.import columns, values
|
98
|
+
```
|
99
|
+
|
100
|
+
#### Hashes
|
101
|
+
|
102
|
+
The `import` method can take an array of hashes. The keys map to the column names in the database.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
values = [{ title: 'Book1', author: 'FooManChu' }, { title: 'Book2', author: 'Bob Jones'}]
|
106
|
+
|
107
|
+
# Importing without model validations
|
108
|
+
Book.import values, validate: false
|
109
|
+
|
110
|
+
# Import with model validations
|
111
|
+
Book.import values, validate: true
|
112
|
+
|
113
|
+
# when not specified :validate defaults to true
|
114
|
+
Book.import values
|
115
|
+
```
|
116
|
+
h2. Import Using Hashes and Explicit Column Names
|
117
|
+
|
118
|
+
The `import` method can take an array of column names and an array of hash objects. The column names are used to determine what fields of data should be imported. The following example will only import books with the `title` field:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
books = [
|
122
|
+
{ title: "Book 1", author: "FooManChu" },
|
123
|
+
{ title: "Book 2", author: "Bob Jones" }
|
124
|
+
]
|
125
|
+
columns = [ :title ]
|
126
|
+
|
127
|
+
# without validations
|
128
|
+
Book.import columns, books, validate: false
|
129
|
+
|
130
|
+
# with validations
|
131
|
+
Book.import columns, books, validate: true
|
132
|
+
|
133
|
+
# when not specified :validate defaults to true
|
134
|
+
Book.import columns, books
|
135
|
+
|
136
|
+
# result in table books
|
137
|
+
# title | author
|
138
|
+
#--------|--------
|
139
|
+
# Book 1 | NULL
|
140
|
+
# Book 2 | NULL
|
141
|
+
|
142
|
+
```
|
143
|
+
|
144
|
+
Using hashes will only work if the columns are consistent in every hash of the array. If this does not hold, an exception will be raised. There are two workarounds: use the array to instantiate an array of ActiveRecord objects and then pass that into `import` or divide the array into multiple ones with consistent columns and import each one separately.
|
145
|
+
|
146
|
+
See https://github.com/zdennis/activerecord-import/issues/507 for discussion.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
arr = [
|
150
|
+
{ bar: 'abc' },
|
151
|
+
{ baz: 'xyz' },
|
152
|
+
{ bar: '123', baz: '456' }
|
153
|
+
]
|
154
|
+
|
155
|
+
# An exception will be raised
|
156
|
+
Foo.import arr
|
157
|
+
|
158
|
+
# better
|
159
|
+
arr.map! { |args| Foo.new(args) }
|
160
|
+
Foo.import arr
|
161
|
+
|
162
|
+
# better
|
163
|
+
arr.group_by(&:keys).each_value do |v|
|
164
|
+
Foo.import v
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
#### ActiveRecord Models
|
169
|
+
|
170
|
+
The `import` method can take an array of models. The attributes will be pulled off from each model by looking at the columns available on the model.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
books = [
|
174
|
+
Book.new(:title => "Book 1", :author => "FooManChu"),
|
175
|
+
Book.new(:title => "Book 2", :author => "Bob Jones")
|
176
|
+
]
|
177
|
+
|
178
|
+
# without validations
|
179
|
+
Book.import books, :validate => false
|
180
|
+
|
181
|
+
# with validations
|
182
|
+
Book.import books, :validate => true
|
183
|
+
|
184
|
+
# when not specified :validate defaults to true
|
185
|
+
Book.import books
|
186
|
+
```
|
187
|
+
|
188
|
+
The `import` method can take an array of column names and an array of models. The column names are used to determine what fields of data should be imported. The following example will only import books with the `title` field:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
books = [
|
192
|
+
Book.new(:title => "Book 1", :author => "FooManChu"),
|
193
|
+
Book.new(:title => "Book 2", :author => "Bob Jones")
|
194
|
+
]
|
195
|
+
columns = [ :title ]
|
196
|
+
|
197
|
+
# without validations
|
198
|
+
Book.import columns, books, :validate => false
|
199
|
+
|
200
|
+
# with validations
|
201
|
+
Book.import columns, books, :validate => true
|
202
|
+
|
203
|
+
# when not specified :validate defaults to true
|
204
|
+
Book.import columns, books
|
205
|
+
|
206
|
+
# result in table books
|
207
|
+
# title | author
|
208
|
+
#--------|--------
|
209
|
+
# Book 1 | NULL
|
210
|
+
# Book 2 | NULL
|
211
|
+
|
212
|
+
```
|
213
|
+
|
214
|
+
#### Batching
|
215
|
+
|
216
|
+
The `import` method can take a `batch_size` option to control the number of rows to insert per INSERT statement. The default is the total number of records being inserted so there is a single INSERT statement.
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
books = [
|
220
|
+
Book.new(:title => "Book 1", :author => "FooManChu"),
|
221
|
+
Book.new(:title => "Book 2", :author => "Bob Jones"),
|
222
|
+
Book.new(:title => "Book 1", :author => "John Doe"),
|
223
|
+
Book.new(:title => "Book 2", :author => "Richard Wright")
|
224
|
+
]
|
225
|
+
columns = [ :title ]
|
226
|
+
|
227
|
+
# 2 INSERT statements for 4 records
|
228
|
+
Book.import columns, books, :batch_size => 2
|
229
|
+
```
|
230
|
+
|
231
|
+
#### Recursive
|
232
|
+
|
233
|
+
NOTE: This only works with PostgreSQL.
|
234
|
+
|
235
|
+
Assume that Books <code>has_many</code> Reviews.
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
books = []
|
239
|
+
10.times do |i|
|
240
|
+
book = Book.new(:name => "book #{i}")
|
241
|
+
book.reviews.build(:title => "Excellent")
|
242
|
+
books << book
|
243
|
+
end
|
244
|
+
Book.import books, recursive: true
|
245
|
+
```
|
246
|
+
|
247
|
+
### Options
|
248
|
+
|
249
|
+
Key | Options | Default | Description
|
250
|
+
----------------------- | --------------------- | ------------------ | -----------
|
251
|
+
:validate | `true`/`false` | `true` | Whether or not to run `ActiveRecord` validations (uniqueness skipped).
|
252
|
+
:validate_uniqueness | `true`/`false` | `false` | Whether or not to run uniqueness validations, has potential pitfalls, use with caution (requires `>= v0.27.0`).
|
253
|
+
:on_duplicate_key_ignore| `true`/`false` | `false` | Allows skipping records with duplicate keys. See [here](https://github.com/zdennis/activerecord-import/#duplicate-key-ignore) for more details.
|
254
|
+
:ignore | `true`/`false` | `false` | Alias for :on_duplicate_key_ignore.
|
255
|
+
:on_duplicate_key_update| :all, `Array`, `Hash` | N/A | Allows upsert logic to be used. See [here](https://github.com/zdennis/activerecord-import/#duplicate-key-update) for more details.
|
256
|
+
:synchronize | `Array` | N/A | An array of ActiveRecord instances. This synchronizes existing instances in memory with updates from the import.
|
257
|
+
:timestamps | `true`/`false` | `true` | Enables/disables timestamps on imported records.
|
258
|
+
:recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL only).
|
259
|
+
:batch_size | `Integer` | total # of records | Max number of records to insert per import
|
260
|
+
:raise_error | `true`/`false` | `false` | Throws an exception if there are invalid records. `import!` is a shortcut for this.
|
261
|
+
|
262
|
+
|
263
|
+
#### Duplicate Key Ignore
|
264
|
+
|
265
|
+
[MySQL](http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html), [SQLite](https://www.sqlite.org/lang_insert.html), and [PostgreSQL](https://www.postgresql.org/docs/current/static/sql-insert.html#SQL-ON-CONFLICT) (9.5+) support `on_duplicate_key_ignore` which allows you to skip records if a primary or unique key constraint is violated.
|
266
|
+
|
267
|
+
For Postgres 9.5+ it adds `ON CONFLICT DO NOTHING`, for MySQL it uses `INSERT IGNORE`, and for SQLite it uses `INSERT OR IGNORE`. Cannot be enabled on a recursive import. For database adapters that normally support setting primary keys on imported objects, this option prevents that from occurring.
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
book = Book.create! title: "Book1", author: "FooManChu"
|
271
|
+
book.title = "Updated Book Title"
|
272
|
+
book.author = "Bob Barker"
|
273
|
+
|
274
|
+
Book.import [book], on_duplicate_key_ignore: true
|
275
|
+
|
276
|
+
book.reload.title # => "Book1" (stayed the same)
|
277
|
+
book.reload.author # => "FooManChu" (stayed the same)
|
278
|
+
```
|
279
|
+
|
280
|
+
The option `:on_duplicate_key_ignore` is bypassed when `:recursive` is enabled for [PostgreSQL imports](https://github.com/zdennis/activerecord-import/wiki#recursive-example-postgresql-only).
|
281
|
+
|
282
|
+
#### Duplicate Key Update
|
283
|
+
|
284
|
+
MySQL, PostgreSQL (9.5+), and SQLite (3.24.0+) support `on duplicate key update` (also known as "upsert") which allows you to specify fields whose values should be updated if a primary or unique key constraint is violated.
|
285
|
+
|
286
|
+
One big difference between MySQL and PostgreSQL support is that MySQL will handle any conflict that happens, but PostgreSQL requires that you specify which columns the conflict would occur over. SQLite models its upsert support after PostgreSQL.
|
287
|
+
|
288
|
+
This will use MySQL's `ON DUPLICATE KEY UPDATE` or Postgres/SQLite `ON CONFLICT DO UPDATE` to do upsert.
|
289
|
+
|
290
|
+
Basic Update
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
book = Book.create! title: "Book1", author: "FooManChu"
|
294
|
+
book.title = "Updated Book Title"
|
295
|
+
book.author = "Bob Barker"
|
296
|
+
|
297
|
+
# MySQL version
|
298
|
+
Book.import [book], on_duplicate_key_update: [:title]
|
299
|
+
|
300
|
+
# PostgreSQL version
|
301
|
+
Book.import [book], on_duplicate_key_update: {conflict_target: [:id], columns: [:title]}
|
302
|
+
|
303
|
+
# PostgreSQL shorthand version (conflict target must be primary key)
|
304
|
+
Book.import [book], on_duplicate_key_update: [:title]
|
305
|
+
|
306
|
+
book.reload.title # => "Updated Book Title" (changed)
|
307
|
+
book.reload.author # => "FooManChu" (stayed the same)
|
308
|
+
```
|
309
|
+
|
310
|
+
Using the value from another column
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
book = Book.create! title: "Book1", author: "FooManChu"
|
314
|
+
book.title = "Updated Book Title"
|
315
|
+
|
316
|
+
# MySQL version
|
317
|
+
Book.import [book], on_duplicate_key_update: {author: :title}
|
318
|
+
|
319
|
+
# PostgreSQL version (no shorthand version)
|
320
|
+
Book.import [book], on_duplicate_key_update: {
|
321
|
+
conflict_target: [:id], columns: {author: :title}
|
322
|
+
}
|
323
|
+
|
324
|
+
book.reload.title # => "Book1" (stayed the same)
|
325
|
+
book.reload.author # => "Updated Book Title" (changed)
|
326
|
+
```
|
327
|
+
|
328
|
+
Using Custom SQL
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
book = Book.create! title: "Book1", author: "FooManChu"
|
332
|
+
book.author = "Bob Barker"
|
333
|
+
|
334
|
+
# MySQL version
|
335
|
+
Book.import [book], on_duplicate_key_update: "author = values(author)"
|
336
|
+
|
337
|
+
# PostgreSQL version
|
338
|
+
Book.import [book], on_duplicate_key_update: {
|
339
|
+
conflict_target: [:id], columns: "author = excluded.author"
|
340
|
+
}
|
341
|
+
|
342
|
+
# PostgreSQL shorthand version (conflict target must be primary key)
|
343
|
+
Book.import [book], on_duplicate_key_update: "author = excluded.author"
|
344
|
+
|
345
|
+
book.reload.title # => "Book1" (stayed the same)
|
346
|
+
book.reload.author # => "Bob Barker" (changed)
|
347
|
+
```
|
348
|
+
|
349
|
+
PostgreSQL Using constraints
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
book = Book.create! title: "Book1", author: "FooManChu", edition: 3, published_at: nil
|
353
|
+
book.published_at = Time.now
|
354
|
+
|
355
|
+
# in migration
|
356
|
+
execute <<-SQL
|
357
|
+
ALTER TABLE books
|
358
|
+
ADD CONSTRAINT for_upsert UNIQUE (title, author, edition);
|
359
|
+
SQL
|
360
|
+
|
361
|
+
# PostgreSQL version
|
362
|
+
Book.import [book], on_duplicate_key_update: {constraint_name: :for_upsert, columns: [:published_at]}
|
363
|
+
|
364
|
+
|
365
|
+
book.reload.title # => "Book1" (stayed the same)
|
366
|
+
book.reload.author # => "FooManChu" (stayed the same)
|
367
|
+
book.reload.edition # => 3 (stayed the same)
|
368
|
+
book.reload.published_at # => 2017-10-09 (changed)
|
369
|
+
```
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
Book.import books, validate_uniqueness: true
|
373
|
+
```
|
374
|
+
|
375
|
+
### Return Info
|
376
|
+
|
377
|
+
The `import` method returns a `Result` object that responds to `failed_instances` and `num_inserts`. Additionally, for users of Postgres, there will be two arrays `ids` and `results` that can be accessed`.
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
articles = [
|
381
|
+
Article.new(author_id: 1, title: 'First Article', content: 'This is the first article'),
|
382
|
+
Article.new(author_id: 2, title: 'Second Article', content: ''),
|
383
|
+
Article.new(author_id: 3, content: '')
|
384
|
+
]
|
385
|
+
|
386
|
+
demo = Article.import(articles), returning: :title # => #<struct ActiveRecord::Import::Result
|
387
|
+
|
388
|
+
demo.failed_instances
|
389
|
+
=> [#<Article id: 3, author_id: 3, title: nil, content: "", created_at: nil, updated_at: nil>]
|
390
|
+
|
391
|
+
demo.num_inserts
|
392
|
+
=> 1,
|
393
|
+
|
394
|
+
demo.ids
|
395
|
+
=> ["1", "2"] # for Postgres
|
396
|
+
=> [] # for other DBs
|
397
|
+
|
398
|
+
demo.results
|
399
|
+
=> ["First Article", "Second Article"] # for Postgres
|
400
|
+
=> [] for other DBs
|
401
|
+
```
|
402
|
+
|
403
|
+
### Counter Cache
|
404
|
+
|
405
|
+
When running `import`, `activerecord-import` does not automatically update counter cache columns. To update these columns, you will need to do one of the following:
|
406
|
+
|
407
|
+
* Provide values to the column as an argument on your object that is passed in.
|
408
|
+
* Manually update the column after the record has been imported.
|
409
|
+
|
410
|
+
### ActiveRecord Timestamps
|
411
|
+
|
412
|
+
If you're familiar with ActiveRecord you're probably familiar with its timestamp columns: created_at, created_on, updated_at, updated_on, etc. When importing data the timestamp fields will continue to work as expected and each timestamp column will be set.
|
413
|
+
|
414
|
+
Should you wish to specify those columns, you may use the option `timestamps: false`.
|
415
|
+
|
416
|
+
However, it is also possible to set just `:created_at` in specific records. In this case despite using `timestamps: true`, `:created_at` will be updated only in records where that field is `nil`. Same rule applies for record associations when enabling the option `recursive: true`.
|
417
|
+
|
418
|
+
If you are using custom time zones, these will be respected when performing imports as well as long as `ActiveRecord::Base.default_timezone` is set, which for practically all Rails apps it is
|
30
419
|
|
31
420
|
### Callbacks
|
32
421
|
|
@@ -34,7 +423,7 @@ ActiveRecord callbacks related to [creating](http://guides.rubyonrails.org/activ
|
|
34
423
|
|
35
424
|
If you do have a collection of in-memory ActiveRecord objects you can do something like this:
|
36
425
|
|
37
|
-
```
|
426
|
+
```ruby
|
38
427
|
books.each do |book|
|
39
428
|
book.run_callbacks(:save) { false }
|
40
429
|
book.run_callbacks(:create) { false }
|
@@ -46,7 +435,7 @@ This will run before_create and before_save callbacks on each item. The `false`
|
|
46
435
|
|
47
436
|
If that is an issue, another possible approach is to loop through your models first to do validations and then only run callbacks on and import the valid models.
|
48
437
|
|
49
|
-
```
|
438
|
+
```ruby
|
50
439
|
valid_books = []
|
51
440
|
invalid_books = []
|
52
441
|
|
@@ -66,6 +455,24 @@ end
|
|
66
455
|
Book.import valid_books, validate: false
|
67
456
|
```
|
68
457
|
|
458
|
+
### Supported Adapters
|
459
|
+
|
460
|
+
The following database adapters are currently supported:
|
461
|
+
|
462
|
+
* MySQL - supports core import functionality plus on duplicate key update support (included in activerecord-import 0.1.0 and higher)
|
463
|
+
* MySQL2 - supports core import functionality plus on duplicate key update support (included in activerecord-import 0.2.0 and higher)
|
464
|
+
* PostgreSQL - supports core import functionality (included in activerecord-import 0.1.0 and higher)
|
465
|
+
* SQLite3 - supports core import functionality (included in activerecord-import 0.1.0 and higher)
|
466
|
+
* Oracle - supports core import functionality through DML trigger (available as an external gem: [activerecord-import-oracle_enhanced](https://github.com/keeguon/activerecord-import-oracle_enhanced)
|
467
|
+
* SQL Server - supports core import functionality (available as an external gem: [activerecord-import-sqlserver](https://github.com/keeguon/activerecord-import-sqlserver)
|
468
|
+
|
469
|
+
If your adapter isn't listed here, please consider creating an external gem as described in the README to provide support. If you do, feel free to update this wiki to include a link to the new adapter's repository!
|
470
|
+
|
471
|
+
To test which features are supported by your adapter, use the following methods on a model class:
|
472
|
+
* `supports_import?(*args)`
|
473
|
+
* `supports_on_duplicate_key_update?`
|
474
|
+
* `supports_setting_primary_key_of_imported_objects?`
|
475
|
+
|
69
476
|
### Additional Adapters
|
70
477
|
|
71
478
|
Additional adapters can be provided by gems external to activerecord-import by providing an adapter that matches the naming convention setup by activerecord-import (and subsequently activerecord) for dynamically loading adapters. This involves also providing a folder on the load path that follows the activerecord-import naming convention to allow activerecord-import to dynamically load the file.
|
@@ -73,17 +480,54 @@ Additional adapters can be provided by gems external to activerecord-import by p
|
|
73
480
|
When `ActiveRecord::Import.require_adapter("fake_name")` is called the require will be:
|
74
481
|
|
75
482
|
```ruby
|
76
|
-
|
483
|
+
require 'activerecord-import/active_record/adapters/fake_name_adapter'
|
77
484
|
```
|
78
485
|
|
79
486
|
This allows an external gem to dynamically add an adapter without the need to add any file/code to the core activerecord-import gem.
|
80
487
|
|
488
|
+
### Requiring
|
489
|
+
|
490
|
+
Note: These instructions will only work if you are using version 0.2.0 or higher.
|
491
|
+
|
492
|
+
#### Autoloading via Bundler
|
493
|
+
|
494
|
+
If you are using Rails or otherwise autoload your dependencies via Bundler, all you need to do add the gem to your `Gemfile` like so:
|
495
|
+
|
496
|
+
```ruby
|
497
|
+
gem 'activerecord-import'
|
498
|
+
```
|
499
|
+
|
500
|
+
#### Manually Loading
|
501
|
+
|
502
|
+
You may want to manually load activerecord-import for one reason or another. First, add the `require: false` argument like so:
|
503
|
+
|
504
|
+
```ruby
|
505
|
+
gem 'activerecord-import', require: false
|
506
|
+
```
|
507
|
+
|
508
|
+
This will allow you to load up activerecord-import in the file or files where you are using it and only load the parts you need.
|
509
|
+
If you are doing this within Rails and ActiveRecord has established a database connection (such as within a controller), you will need to do extra initialization work:
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
require 'activerecord-import/base'
|
513
|
+
# load the appropriate database adapter (postgresql, mysql2, sqlite3, etc)
|
514
|
+
require 'activerecord-import/active_record/adapters/postgresql_adapter'
|
515
|
+
```
|
516
|
+
|
517
|
+
If your gem dependencies aren’t autoloaded, and your script will be establishing a database connection, then simply require activerecord-import after ActiveRecord has been loaded, i.e.:
|
518
|
+
|
519
|
+
```ruby
|
520
|
+
require 'active_record'
|
521
|
+
require 'activerecord-import'
|
522
|
+
```
|
523
|
+
|
81
524
|
### Load Path Setup
|
82
525
|
To understand how rubygems loads code you can reference the following:
|
83
526
|
|
84
527
|
http://guides.rubygems.org/patterns/#loading_code
|
85
528
|
|
86
529
|
And an example of how active_record dynamically load adapters:
|
530
|
+
|
87
531
|
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/connection_specification.rb
|
88
532
|
|
89
533
|
In summary, when a gem is loaded rubygems adds the `lib` folder of the gem to the global load path `$LOAD_PATH` so that all `require` lookups will not propagate through all of the folders on the load path. When a `require` is issued each folder on the `$LOAD_PATH` is checked for the file and/or folder referenced. This allows a gem (like activerecord-import) to define push the activerecord-import folder (or namespace) on the `$LOAD_PATH` and any adapters provided by activerecord-import will be found by rubygems when the require is issued.
|
@@ -105,12 +549,62 @@ activerecord-import-fake_name/
|
|
105
549
|
|
106
550
|
When rubygems pushes the `lib` folder onto the load path a `require` will now find `activerecord-import/active_record/adapters/fake_name_adapter` as it runs through the lookup process for a ruby file under that path in `$LOAD_PATH`
|
107
551
|
|
552
|
+
|
553
|
+
### Conflicts With Other Gems
|
554
|
+
|
555
|
+
`activerecord-import` adds the `.import` method onto `ActiveRecord::Base`. There are other gems, such as `elasticsearch-rails`, that do the same thing. In conflicts such as this, there is an aliased method named `.bulk_import` that can be used interchangeably.
|
556
|
+
|
557
|
+
If you are using the `apartment` gem, there is a weird triple interaction between that gem, `activerecord-import`, and `activerecord` involving caching of the `sequence_name` of a model. This can be worked around by explcitly setting this value within the model. For example:
|
558
|
+
|
559
|
+
```ruby
|
560
|
+
class Post < ActiveRecord::Base
|
561
|
+
self.sequence_name = "posts_seq"
|
562
|
+
end
|
563
|
+
```
|
564
|
+
|
565
|
+
Another way to work around the issue is to call `.reset_sequence_name` on the model. For example:
|
566
|
+
|
567
|
+
```ruby
|
568
|
+
schemas.all.each do |schema|
|
569
|
+
Apartment::Tenant.switch! schema.name
|
570
|
+
ActiveRecord::Base.transaction do
|
571
|
+
Post.reset_sequence_name
|
572
|
+
|
573
|
+
Post.import posts
|
574
|
+
end
|
575
|
+
end
|
576
|
+
```
|
577
|
+
|
578
|
+
See https://github.com/zdennis/activerecord-import/issues/233 for further discussion.
|
579
|
+
|
108
580
|
### More Information
|
109
581
|
|
110
582
|
For more information on activerecord-import please see its wiki: https://github.com/zdennis/activerecord-import/wiki
|
111
583
|
|
112
584
|
To document new information, please add to the README instead of the wiki. See https://github.com/zdennis/activerecord-import/issues/397 for discussion.
|
113
585
|
|
586
|
+
### Contributing
|
587
|
+
|
588
|
+
#### Running Tests
|
589
|
+
|
590
|
+
The first thing you need to do is set up your database(s):
|
591
|
+
|
592
|
+
* copy `test/database.yml.sample` to `test/database.yml`
|
593
|
+
* modify `test/database.yml` for your database settings
|
594
|
+
* create databases as needed
|
595
|
+
|
596
|
+
After that, you can run the tests. They run against multiple tests and ActiveRecord versions.
|
597
|
+
|
598
|
+
This is one example of how to run the tests:
|
599
|
+
|
600
|
+
```ruby
|
601
|
+
rm Gemfile.lock
|
602
|
+
AR_VERSION=4.2 bundle install
|
603
|
+
AR_VERSION=4.2 bundle exec rake test:postgresql test:sqlite3 test:mysql2
|
604
|
+
```
|
605
|
+
|
606
|
+
Once you have pushed up your changes, you can find your CI results [here](https://travis-ci.org/zdennis/activerecord-import/).
|
607
|
+
|
114
608
|
# License
|
115
609
|
|
116
610
|
This is licensed under the ruby license.
|
@@ -131,3 +625,4 @@ Zach Dennis (zach.dennis@gmail.com)
|
|
131
625
|
* Thibaud Guillaume-Gentil
|
132
626
|
* Mark Van Holstyn
|
133
627
|
* Victor Costan
|
628
|
+
* Dillon Welch
|
@@ -24,33 +24,57 @@ module ActiveRecord::Import #:nodoc:
|
|
24
24
|
end
|
25
25
|
|
26
26
|
class Validator
|
27
|
-
def initialize(options = {})
|
27
|
+
def initialize(klass, options = {})
|
28
28
|
@options = options
|
29
|
+
init_validations(klass)
|
30
|
+
end
|
31
|
+
|
32
|
+
def init_validations(klass)
|
33
|
+
@validate_callbacks = klass._validate_callbacks.dup
|
34
|
+
|
35
|
+
klass._validate_callbacks.each_with_index do |callback, i|
|
36
|
+
filter = callback.raw_filter
|
37
|
+
|
38
|
+
if filter.class.name =~ /Validations::PresenceValidator/
|
39
|
+
callback = callback.dup
|
40
|
+
filter = filter.dup
|
41
|
+
associations = klass.reflect_on_all_associations(:belongs_to)
|
42
|
+
attrs = filter.instance_variable_get(:@attributes).dup
|
43
|
+
associations.each do |assoc|
|
44
|
+
if (index = attrs.index(assoc.name))
|
45
|
+
key = assoc.foreign_key.to_sym
|
46
|
+
attrs[index] = key unless attrs.include?(key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
filter.instance_variable_set(:@attributes, attrs)
|
50
|
+
if @validate_callbacks.respond_to?(:chain, true)
|
51
|
+
@validate_callbacks.send(:chain).tap do |chain|
|
52
|
+
callback.instance_variable_set(:@filter, filter)
|
53
|
+
chain[i] = callback
|
54
|
+
end
|
55
|
+
else
|
56
|
+
callback.raw_filter = filter
|
57
|
+
callback.filter = callback.send(:_compile_filter, filter)
|
58
|
+
@validate_callbacks[i] = callback
|
59
|
+
end
|
60
|
+
elsif !@options[:validate_uniqueness] && filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
61
|
+
@validate_callbacks.delete(callback)
|
62
|
+
end
|
63
|
+
end
|
29
64
|
end
|
30
65
|
|
31
66
|
def valid_model?(model)
|
32
67
|
validation_context = @options[:validate_with_context]
|
33
68
|
validation_context ||= (model.new_record? ? :create : :update)
|
34
|
-
|
35
69
|
current_context = model.send(:validation_context)
|
70
|
+
|
36
71
|
begin
|
37
72
|
model.send(:validation_context=, validation_context)
|
38
73
|
model.errors.clear
|
39
74
|
|
40
|
-
validate_callbacks = model._validate_callbacks.dup
|
41
|
-
associations = model.class.reflect_on_all_associations(:belongs_to).map(&:name)
|
42
|
-
|
43
|
-
model._validate_callbacks.each do |callback|
|
44
|
-
filter = callback.raw_filter
|
45
|
-
if (!@options[:validate_uniqueness] && filter.is_a?(ActiveRecord::Validations::UniquenessValidator)) ||
|
46
|
-
(defined?(ActiveRecord::Validations::PresenceValidator) && filter.is_a?(ActiveRecord::Validations::PresenceValidator) && associations.include?(filter.attributes.first))
|
47
|
-
validate_callbacks.delete(callback)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
75
|
model.run_callbacks(:validation) do
|
52
76
|
if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
|
53
|
-
runner = validate_callbacks.compile
|
77
|
+
runner = @validate_callbacks.compile
|
54
78
|
env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
|
55
79
|
if runner.respond_to?(:call) # ActiveRecord < 5.1
|
56
80
|
runner.call(env)
|
@@ -69,10 +93,10 @@ module ActiveRecord::Import #:nodoc:
|
|
69
93
|
runner.invoke_before(env)
|
70
94
|
runner.invoke_after(env)
|
71
95
|
end
|
72
|
-
elsif validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
73
|
-
model.instance_eval validate_callbacks.compile
|
96
|
+
elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
|
97
|
+
model.instance_eval @validate_callbacks.compile
|
74
98
|
else # ActiveRecord 3.x
|
75
|
-
model.instance_eval validate_callbacks.compile(nil, model)
|
99
|
+
model.instance_eval @validate_callbacks.compile(nil, model)
|
76
100
|
end
|
77
101
|
end
|
78
102
|
|
@@ -520,7 +544,7 @@ class ActiveRecord::Base
|
|
520
544
|
options[:locking_column] = locking_column if attribute_names.include?(locking_column)
|
521
545
|
|
522
546
|
is_validating = options[:validate_with_context].present? ? true : options[:validate]
|
523
|
-
validator = ActiveRecord::Import::Validator.new(options)
|
547
|
+
validator = ActiveRecord::Import::Validator.new(self, options)
|
524
548
|
|
525
549
|
# assume array of model objects
|
526
550
|
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
@@ -553,6 +577,14 @@ class ActiveRecord::Base
|
|
553
577
|
serialized_attributes
|
554
578
|
end
|
555
579
|
|
580
|
+
update_attrs = if record_timestamps && options[:timestamps]
|
581
|
+
if respond_to?(:timestamp_attributes_for_update, true)
|
582
|
+
send(:timestamp_attributes_for_update).map(&:to_sym)
|
583
|
+
else
|
584
|
+
new.send(:timestamp_attributes_for_update_in_model)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
556
588
|
array_of_attributes = []
|
557
589
|
|
558
590
|
models.each do |model|
|
@@ -566,9 +598,13 @@ class ActiveRecord::Base
|
|
566
598
|
end
|
567
599
|
|
568
600
|
array_of_attributes << column_names.map do |name|
|
569
|
-
if
|
570
|
-
|
571
|
-
|
601
|
+
if model.persisted? &&
|
602
|
+
update_attrs && update_attrs.include?(name.to_sym) &&
|
603
|
+
!model.send("#{name}_changed?")
|
604
|
+
nil
|
605
|
+
elsif stored_attrs.key?(name.to_sym) ||
|
606
|
+
serialized_attrs.key?(name.to_s) ||
|
607
|
+
default_values[name.to_s]
|
572
608
|
model.read_attribute(name.to_s)
|
573
609
|
else
|
574
610
|
model.read_attribute_before_type_cast(name.to_s)
|
@@ -655,7 +691,7 @@ class ActiveRecord::Base
|
|
655
691
|
timestamps = {}
|
656
692
|
|
657
693
|
# record timestamps unless disabled in ActiveRecord::Base
|
658
|
-
if record_timestamps && options
|
694
|
+
if record_timestamps && options[:timestamps]
|
659
695
|
timestamps = add_special_rails_stamps column_names, array_of_attributes, options
|
660
696
|
end
|
661
697
|
|
@@ -961,7 +997,7 @@ class ActiveRecord::Base
|
|
961
997
|
index = column_names.index(column) || column_names.index(column.to_sym)
|
962
998
|
if index
|
963
999
|
# replace every instance of the array of attributes with our value
|
964
|
-
array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil?
|
1000
|
+
array_of_attributes.each { |arr| arr[index] = timestamp if arr[index].nil? }
|
965
1001
|
else
|
966
1002
|
column_names << column
|
967
1003
|
array_of_attributes.each { |arr| arr << timestamp }
|
data/test/import_test.rb
CHANGED
@@ -303,6 +303,27 @@ describe "#import" do
|
|
303
303
|
end
|
304
304
|
end
|
305
305
|
end
|
306
|
+
|
307
|
+
context "when validatoring presence of belongs_to association" do
|
308
|
+
it "should not import records without foreign key" do
|
309
|
+
assert_no_difference "UserToken.count" do
|
310
|
+
UserToken.import [:token], [['12345abcdef67890']]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
it "should import records with foreign key" do
|
315
|
+
assert_difference "UserToken.count", +1 do
|
316
|
+
UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
it "should not mutate the defined validations" do
|
321
|
+
UserToken.import [:user_name, :token], [%w("Bob", "12345abcdef67890")]
|
322
|
+
ut = UserToken.new
|
323
|
+
ut.valid?
|
324
|
+
assert_includes ut.errors.messages, :user
|
325
|
+
end
|
326
|
+
end
|
306
327
|
end
|
307
328
|
|
308
329
|
context "without :validation option" do
|
@@ -499,11 +520,11 @@ describe "#import" do
|
|
499
520
|
|
500
521
|
context "when the timestamps columns are present" do
|
501
522
|
setup do
|
502
|
-
@existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc)
|
523
|
+
@existing_book = Book.create(title: "Fell", author_name: "Curry", publisher: "Bayer", created_at: 2.years.ago.utc, created_on: 2.years.ago.utc, updated_at: 2.years.ago.utc, updated_on: 2.years.ago.utc)
|
503
524
|
ActiveRecord::Base.default_timezone = :utc
|
504
525
|
Timecop.freeze(time) do
|
505
526
|
assert_difference "Book.count", +2 do
|
506
|
-
Book.import %w(title author_name publisher created_at created_on), [["LDAP", "Big Bird", "Del Rey", nil, nil], [@existing_book.title, @existing_book.author_name, @existing_book.publisher, @existing_book.created_at, @existing_book.created_on]]
|
527
|
+
Book.import %w(title author_name publisher created_at created_on updated_at updated_on), [["LDAP", "Big Bird", "Del Rey", nil, nil, nil, nil], [@existing_book.title, @existing_book.author_name, @existing_book.publisher, @existing_book.created_at, @existing_book.created_on, @existing_book.updated_at, @existing_book.updated_on]]
|
507
528
|
end
|
508
529
|
end
|
509
530
|
@new_book, @existing_book = Book.last 2
|
@@ -532,6 +553,23 @@ describe "#import" do
|
|
532
553
|
it "should set the updated_on column for new records" do
|
533
554
|
assert_in_delta time.to_i, @new_book.updated_on.to_i, 1.second
|
534
555
|
end
|
556
|
+
|
557
|
+
it "should not set the updated_at column for existing records" do
|
558
|
+
assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_at.strftime("%Y:%d")
|
559
|
+
end
|
560
|
+
|
561
|
+
it "should not set the updated_on column for existing records" do
|
562
|
+
assert_equal 2.years.ago.utc.strftime("%Y:%d"), @existing_book.updated_on.strftime("%Y:%d")
|
563
|
+
end
|
564
|
+
|
565
|
+
it "should not set the updated_at column on models if changed" do
|
566
|
+
timestamp = Time.now.utc
|
567
|
+
books = [
|
568
|
+
Book.new(author_name: "Foo", title: "Baz", created_at: timestamp, updated_at: timestamp)
|
569
|
+
]
|
570
|
+
Book.import books
|
571
|
+
assert_equal timestamp.strftime("%Y:%d"), Book.last.updated_at.strftime("%Y:%d")
|
572
|
+
end
|
535
573
|
end
|
536
574
|
|
537
575
|
context "when a custom time zone is set" do
|
data/test/models/user_token.rb
CHANGED
@@ -246,6 +246,26 @@ def should_support_basic_on_duplicate_key_update
|
|
246
246
|
end
|
247
247
|
end
|
248
248
|
|
249
|
+
context "with timestamps enabled" do
|
250
|
+
let(:time) { Chronic.parse("5 minutes from now") }
|
251
|
+
|
252
|
+
it 'should not overwrite changed updated_at with current timestamp' do
|
253
|
+
topic = Topic.create(author_name: "Jane Doe", title: "Book")
|
254
|
+
timestamp = Time.now.utc
|
255
|
+
topic.updated_at = timestamp
|
256
|
+
Topic.import [topic], on_duplicate_key_update: :all, validate: false
|
257
|
+
assert_equal timestamp.to_s, Topic.last.updated_at.to_s
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'should update updated_at with current timestamp' do
|
261
|
+
topic = Topic.create(author_name: "Jane Doe", title: "Book")
|
262
|
+
Timecop.freeze(time) do
|
263
|
+
Topic.import [topic], on_duplicate_key_update: [:updated_at], validate: false
|
264
|
+
assert_in_delta time.to_i, topic.reload.updated_at.to_i, 1.second
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
249
269
|
context "with validation checks turned off" do
|
250
270
|
asssertion_group(:should_support_on_duplicate_key_update) do
|
251
271
|
should_not_update_fields_not_mentioned
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-import
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.28.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zach Dennis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|