abstract_importer 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/.travis.yml +5 -3
- data/abstract_importer.gemspec +2 -1
- data/lib/abstract_importer/base.rb +63 -75
- data/lib/abstract_importer/collection_importer.rb +34 -33
- data/lib/abstract_importer/id_map.rb +22 -14
- data/lib/abstract_importer/import_options.rb +2 -0
- data/lib/abstract_importer/import_plan.rb +4 -4
- data/lib/abstract_importer/reporters/base_reporter.rb +22 -22
- data/lib/abstract_importer/reporters/debug_reporter.rb +36 -36
- data/lib/abstract_importer/reporters/null_reporter.rb +5 -5
- data/lib/abstract_importer/reporters/performance_reporter.rb +17 -17
- data/lib/abstract_importer/strategies.rb +1 -0
- data/lib/abstract_importer/strategies/base.rb +12 -0
- data/lib/abstract_importer/strategies/default_strategy.rb +1 -7
- data/lib/abstract_importer/strategies/insert_strategy.rb +58 -0
- data/lib/abstract_importer/summary.rb +3 -3
- data/lib/abstract_importer/version.rb +1 -1
- data/test/callback_test.rb +29 -29
- data/test/{importer_test.rb → default_strategy_test.rb} +49 -77
- data/test/insert_strategy_test.rb +82 -0
- data/test/replace_strategy_test.rb +29 -0
- data/test/support/mock_data_source.rb +10 -10
- data/test/support/mock_objects.rb +1 -1
- data/test/support/schema.rb +7 -6
- data/test/test_helper.rb +10 -10
- metadata +27 -7
@@ -11,6 +11,7 @@ module AbstractImporter
|
|
11
11
|
:id_map,
|
12
12
|
:scope,
|
13
13
|
:reporter,
|
14
|
+
:association_attrs,
|
14
15
|
to: :collection
|
15
16
|
|
16
17
|
def initialize(collection)
|
@@ -25,6 +26,17 @@ module AbstractImporter
|
|
25
26
|
id_map.contains? collection.table_name, hash[:id]
|
26
27
|
end
|
27
28
|
|
29
|
+
def flush
|
30
|
+
end
|
31
|
+
|
32
|
+
def prepare_attributes(hash)
|
33
|
+
hash = invoke_callback(:before_build, hash) || hash
|
34
|
+
|
35
|
+
legacy_id = hash.delete(:id)
|
36
|
+
|
37
|
+
hash.merge(legacy_id: legacy_id).merge(association_attrs)
|
38
|
+
end
|
39
|
+
|
28
40
|
end
|
29
41
|
end
|
30
42
|
end
|
@@ -58,13 +58,7 @@ module AbstractImporter
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def build_record(hash)
|
61
|
-
|
62
|
-
|
63
|
-
legacy_id = hash.delete(:id)
|
64
|
-
|
65
|
-
collection.model.new(hash
|
66
|
-
.merge(legacy_id: legacy_id)
|
67
|
-
.merge(collection.association_attrs))
|
61
|
+
collection.model.new prepare_attributes(hash)
|
68
62
|
end
|
69
63
|
|
70
64
|
def clean_record(record)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "abstract_importer/strategies/base"
|
2
|
+
require "activerecord/insert_many"
|
3
|
+
|
4
|
+
module AbstractImporter
|
5
|
+
module Strategies
|
6
|
+
class InsertStrategy < Base
|
7
|
+
|
8
|
+
def initialize(collection)
|
9
|
+
super
|
10
|
+
@batch = []
|
11
|
+
@batch_size = 250
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def process_record(hash)
|
16
|
+
summary.total += 1
|
17
|
+
|
18
|
+
if already_imported?(hash)
|
19
|
+
summary.already_imported += 1
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
remap_foreign_keys!(hash)
|
24
|
+
|
25
|
+
if redundant_record?(hash)
|
26
|
+
summary.redundant += 1
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
@batch << prepare_attributes(hash)
|
31
|
+
flush if @batch.length >= @batch_size
|
32
|
+
|
33
|
+
rescue ::AbstractImporter::Skip
|
34
|
+
summary.skipped += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def flush
|
39
|
+
invoke_callback(:before_batch, @batch)
|
40
|
+
|
41
|
+
begin
|
42
|
+
tries = (tries || 0) + 1
|
43
|
+
collection.scope.insert_many(@batch)
|
44
|
+
rescue
|
45
|
+
raise if tries > 1
|
46
|
+
invoke_callback(:rescue_batch, @batch)
|
47
|
+
retry
|
48
|
+
end
|
49
|
+
|
50
|
+
id_map.merge! collection.table_name, collection.scope
|
51
|
+
.where(legacy_id: @batch.map { |hash| hash[:legacy_id] })
|
52
|
+
@batch = []
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module AbstractImporter
|
2
2
|
class Summary < Struct.new(:total, :redundant, :created, :already_imported, :invalid, :ms, :skipped)
|
3
|
-
|
3
|
+
|
4
4
|
def initialize
|
5
5
|
super(0,0,0,0,0,0,0)
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
def average_ms
|
9
9
|
return nil if total == 0
|
10
10
|
ms / total
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
end
|
14
14
|
end
|
data/test/callback_test.rb
CHANGED
@@ -2,9 +2,9 @@ require "test_helper"
|
|
2
2
|
|
3
3
|
|
4
4
|
class CallbackTest < ActiveSupport::TestCase
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
|
6
|
+
|
7
|
+
|
8
8
|
context "before_build" do
|
9
9
|
setup do
|
10
10
|
plan do |import|
|
@@ -13,12 +13,12 @@ class CallbackTest < ActiveSupport::TestCase
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
should "should be invoked on the incoming attributes" do
|
18
18
|
import!
|
19
19
|
assert_equal ["Harry", "Ron", "Hermione"], account.students.pluck(:name)
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
should "allow you to skip certain records" do
|
23
23
|
plan do |import|
|
24
24
|
import.students do |options|
|
@@ -27,15 +27,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
import!
|
32
32
|
assert_equal ["Ron Weasley", "Hermione Granger"], account.students.pluck(:name)
|
33
33
|
assert_equal 1, results[:students].skipped
|
34
34
|
end
|
35
35
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
|
37
|
+
|
38
|
+
|
39
39
|
context "before_create" do
|
40
40
|
setup do
|
41
41
|
plan do |import|
|
@@ -44,15 +44,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
should "should be invoked on imported records before they are saved" do
|
49
49
|
import!
|
50
50
|
assert_equal ["Gryffindor"], account.students.pluck(:house).uniq
|
51
51
|
end
|
52
52
|
end
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
|
54
|
+
|
55
|
+
|
56
56
|
context "after_create" do
|
57
57
|
setup do
|
58
58
|
plan do |import|
|
@@ -61,7 +61,7 @@ class CallbackTest < ActiveSupport::TestCase
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
should "should be invoked after the record is created" do
|
66
66
|
mock(importer).callback({name: "Harry Potter"}, satisfy(&:persisted?)).once
|
67
67
|
mock(importer).callback({name: "Ron Weasley"}, satisfy(&:persisted?)).once
|
@@ -69,9 +69,9 @@ class CallbackTest < ActiveSupport::TestCase
|
|
69
69
|
import!
|
70
70
|
end
|
71
71
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
|
73
|
+
|
74
|
+
|
75
75
|
context "rescue" do
|
76
76
|
setup do
|
77
77
|
plan do |import|
|
@@ -80,15 +80,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
80
80
|
end
|
81
81
|
end
|
82
82
|
end
|
83
|
-
|
83
|
+
|
84
84
|
should "should be given a chance to amend an invalid record" do
|
85
85
|
import!
|
86
86
|
assert_equal ["godrics-hollow", "azkaban"], account.locations.pluck(:slug)
|
87
87
|
end
|
88
88
|
end
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
|
90
|
+
|
91
|
+
|
92
92
|
context "before_all" do
|
93
93
|
setup do
|
94
94
|
plan do |import|
|
@@ -97,15 +97,15 @@ class CallbackTest < ActiveSupport::TestCase
|
|
97
97
|
end
|
98
98
|
end
|
99
99
|
end
|
100
|
-
|
100
|
+
|
101
101
|
should "should be invoked before the collection has been imported" do
|
102
102
|
mock(importer).callback.once
|
103
103
|
import!
|
104
104
|
end
|
105
105
|
end
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
|
107
|
+
|
108
|
+
|
109
109
|
context "after_all" do
|
110
110
|
setup do
|
111
111
|
plan do |import|
|
@@ -114,13 +114,13 @@ class CallbackTest < ActiveSupport::TestCase
|
|
114
114
|
end
|
115
115
|
end
|
116
116
|
end
|
117
|
-
|
117
|
+
|
118
118
|
should "should be invoked after the collection has been imported" do
|
119
119
|
mock(importer).callback.once
|
120
120
|
import!
|
121
121
|
end
|
122
122
|
end
|
123
|
-
|
124
|
-
|
125
|
-
|
123
|
+
|
124
|
+
|
125
|
+
|
126
126
|
end
|
@@ -1,30 +1,30 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class DefaultStrategyTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
|
7
|
+
|
8
8
|
context "with a simple data source" do
|
9
9
|
setup do
|
10
10
|
plan do |import|
|
11
11
|
import.students
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
should "import the given records" do
|
16
16
|
import!
|
17
17
|
assert_equal ["Harry Potter", "Ron Weasley", "Hermione Granger"], account.students.pluck(:name)
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
should "record their legacy_id" do
|
21
21
|
import!
|
22
22
|
assert_equal [456, 457, 458], account.students.pluck(:legacy_id)
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
|
26
|
+
|
27
|
+
|
28
28
|
context "with a complex data source" do
|
29
29
|
setup do
|
30
30
|
plan do |import|
|
@@ -32,19 +32,19 @@ class ImporterTest < ActiveSupport::TestCase
|
|
32
32
|
import.parents
|
33
33
|
end
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
should "preserve mappings" do
|
37
37
|
import!
|
38
38
|
harry = account.students.find_by_name("Harry Potter")
|
39
39
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
should "preserve mappings even when a record was previously imported" do
|
43
43
|
harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
|
44
44
|
import!
|
45
45
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
context "when {atomic: true}" do
|
49
49
|
should "rollback the whole import if an part fails" do
|
50
50
|
mock(importer).atomic? { true }
|
@@ -54,7 +54,7 @@ class ImporterTest < ActiveSupport::TestCase
|
|
54
54
|
assert_equal 0, account.students.count, "Expected students to have been rolled back"
|
55
55
|
end
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
context "when {atomic: false}" do
|
59
59
|
should "not rollback the whole import if an part fails" do
|
60
60
|
mock(importer).atomic? { false }
|
@@ -65,9 +65,9 @@ class ImporterTest < ActiveSupport::TestCase
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
68
|
-
|
69
|
-
|
70
|
-
|
68
|
+
|
69
|
+
|
70
|
+
|
71
71
|
context "with a dependency" do
|
72
72
|
setup do
|
73
73
|
depends_on :students
|
@@ -75,16 +75,16 @@ class ImporterTest < ActiveSupport::TestCase
|
|
75
75
|
import.parents
|
76
76
|
end
|
77
77
|
end
|
78
|
-
|
78
|
+
|
79
79
|
should "preserve mappings when a dependency was imported by another importer" do
|
80
80
|
harry = account.students.create!(name: "Harry Potter", legacy_id: 456)
|
81
81
|
import!
|
82
82
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
83
83
|
end
|
84
84
|
end
|
85
|
-
|
86
|
-
|
87
|
-
|
85
|
+
|
86
|
+
|
87
|
+
|
88
88
|
context "when a finder is specified" do
|
89
89
|
setup do
|
90
90
|
plan do |import|
|
@@ -94,22 +94,22 @@ class ImporterTest < ActiveSupport::TestCase
|
|
94
94
|
import.parents
|
95
95
|
end
|
96
96
|
end
|
97
|
-
|
97
|
+
|
98
98
|
should "not import redundant records" do
|
99
99
|
account.students.create!(name: "Ron Weasley", legacy_id: nil)
|
100
100
|
import!
|
101
101
|
assert_equal 3, account.students.count
|
102
102
|
end
|
103
|
-
|
103
|
+
|
104
104
|
should "preserve mappings" do
|
105
105
|
harry = account.students.create!(name: "Harry Potter", legacy_id: nil)
|
106
106
|
import!
|
107
107
|
assert_equal ["James Potter", "Lily Potter"], harry.parents.pluck(:name)
|
108
108
|
end
|
109
109
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
110
|
+
|
111
|
+
|
112
|
+
|
113
113
|
context "with a more complex data source" do
|
114
114
|
setup do
|
115
115
|
plan do |import|
|
@@ -124,16 +124,16 @@ class ImporterTest < ActiveSupport::TestCase
|
|
124
124
|
import.grades
|
125
125
|
end
|
126
126
|
end
|
127
|
-
|
127
|
+
|
128
128
|
should "preserve mappings" do
|
129
129
|
import!
|
130
130
|
ron = account.students.find_by_name "Ron Weasley"
|
131
131
|
assert_equal ["Advanced Potions: Acceptable", "History of Magic: Troll"], ron.report_card
|
132
132
|
end
|
133
133
|
end
|
134
|
-
|
135
|
-
|
136
|
-
|
134
|
+
|
135
|
+
|
136
|
+
|
137
137
|
context "with polymorphic associations" do
|
138
138
|
setup do
|
139
139
|
plan do |import|
|
@@ -142,7 +142,7 @@ class ImporterTest < ActiveSupport::TestCase
|
|
142
142
|
import.students
|
143
143
|
end
|
144
144
|
end
|
145
|
-
|
145
|
+
|
146
146
|
should "preserve mappings" do
|
147
147
|
import!
|
148
148
|
assert_equal 2, account.students.map(&:pet).compact.count, "Expected two students to still be linked to their pets upon import"
|
@@ -150,30 +150,25 @@ class ImporterTest < ActiveSupport::TestCase
|
|
150
150
|
assert_kind_of Cat, account.students.find_by_name("Hermione Granger").pet, "Expected Harry's pet to be a Cat"
|
151
151
|
end
|
152
152
|
end
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
context "When
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
context "When records already exist" do
|
157
157
|
setup do
|
158
158
|
plan do |import|
|
159
159
|
import.students
|
160
160
|
end
|
161
|
+
account.students.create!(name: "Ron Weasley", legacy_id: 457)
|
161
162
|
end
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
end
|
167
|
-
|
168
|
-
should "not import existing records twice" do
|
169
|
-
import!
|
170
|
-
assert_equal 3, account.students.count
|
171
|
-
end
|
163
|
+
|
164
|
+
should "not import existing records twice" do
|
165
|
+
import!
|
166
|
+
assert_equal 3, account.students.count
|
172
167
|
end
|
173
168
|
end
|
174
|
-
|
175
|
-
|
176
|
-
|
169
|
+
|
170
|
+
|
171
|
+
|
177
172
|
context "When we specify collections to skip" do
|
178
173
|
setup do
|
179
174
|
plan do |import|
|
@@ -181,24 +176,24 @@ class ImporterTest < ActiveSupport::TestCase
|
|
181
176
|
import.parents
|
182
177
|
end
|
183
178
|
end
|
184
|
-
|
179
|
+
|
185
180
|
context "using :skip" do
|
186
181
|
setup do
|
187
182
|
options.merge!(skip: :parents)
|
188
183
|
end
|
189
|
-
|
184
|
+
|
190
185
|
should "not import the named collections" do
|
191
186
|
import!
|
192
187
|
assert_equal 3, account.students.length
|
193
188
|
assert_equal 0, account.parents.length
|
194
189
|
end
|
195
190
|
end
|
196
|
-
|
191
|
+
|
197
192
|
context "using :only" do
|
198
193
|
setup do
|
199
194
|
options.merge!(only: [:students])
|
200
195
|
end
|
201
|
-
|
196
|
+
|
202
197
|
should "import only the named collections" do
|
203
198
|
import!
|
204
199
|
assert_equal 3, account.students.length
|
@@ -206,30 +201,7 @@ class ImporterTest < ActiveSupport::TestCase
|
|
206
201
|
end
|
207
202
|
end
|
208
203
|
end
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
context "When we use the :replace strategy" do
|
213
|
-
setup do
|
214
|
-
options.merge!(strategy: {students: :replace})
|
215
|
-
plan do |import|
|
216
|
-
import.students
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
context "and records already exist" do
|
221
|
-
setup do
|
222
|
-
account.students.create!(name: "Ron Weasley", legacy_id: 457)
|
223
|
-
end
|
224
|
-
|
225
|
-
should "reimport the existing records" do
|
226
|
-
import!
|
227
|
-
assert_equal "Gryffindor", account.students.find_by_name("Ron Weasley").house,
|
228
|
-
"Expected Ron's record to have been replaced with one that has a house"
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
|
234
|
-
|
204
|
+
|
205
|
+
|
206
|
+
|
235
207
|
end
|