activerecord-import 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/lib/activerecord-import/base.rb +1 -0
- data/lib/activerecord-import/import.rb +10 -1
- data/lib/activerecord-import/synchronize.rb +55 -0
- data/test/import_test.rb +34 -1
- data/test/support/mysql/import_examples.rb +26 -0
- data/test/synchronize_test.rb +22 -0
- metadata +7 -6
- data/README.textile +0 -62
data/Rakefile
CHANGED
@@ -41,6 +41,8 @@ ADAPTERS.each do |adapter|
|
|
41
41
|
namespace :test do
|
42
42
|
desc "Runs #{adapter} database tests."
|
43
43
|
Rake::TestTask.new(adapter) do |t|
|
44
|
+
# FactoryGirl has an issue with warnings, so turn off, so noisy
|
45
|
+
# t.warning = true
|
44
46
|
t.test_files = FileList["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/active_record/*_test.rb", "test/#{adapter}/**/*_test.rb"]
|
45
47
|
end
|
46
48
|
task adapter
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.6
|
@@ -129,6 +129,11 @@ class ActiveRecord::Base
|
|
129
129
|
# BlogPost.import posts, :synchronize=>[ post ]
|
130
130
|
# puts post.author_name # => 'yoda'
|
131
131
|
#
|
132
|
+
# # Example synchronizing unsaved/new instances in memory by using a uniqued imported field
|
133
|
+
# posts = [BlogPost.new(:title => "Foo"), BlogPost.new(:title => "Bar")]
|
134
|
+
# BlogPost.import posts, :synchronize => posts
|
135
|
+
# puts posts.first.new_record? # => false
|
136
|
+
#
|
132
137
|
# == On Duplicate Key Update (MySQL only)
|
133
138
|
#
|
134
139
|
# The :on_duplicate_key_update option can be either an Array or a Hash.
|
@@ -178,6 +183,9 @@ class ActiveRecord::Base
|
|
178
183
|
end
|
179
184
|
# end
|
180
185
|
end
|
186
|
+
# supports empty array
|
187
|
+
elsif args.last.is_a?( Array ) and args.last.empty?
|
188
|
+
return ActiveRecord::Import::Result.new([], 0) if args.last.empty?
|
181
189
|
# supports 2-element array and array
|
182
190
|
elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
|
183
191
|
column_names, array_of_attributes = args
|
@@ -209,7 +217,8 @@ class ActiveRecord::Base
|
|
209
217
|
end
|
210
218
|
|
211
219
|
if options[:synchronize]
|
212
|
-
|
220
|
+
sync_keys = options[:synchronize_keys] || [self.primary_key]
|
221
|
+
synchronize( options[:synchronize], sync_keys)
|
213
222
|
end
|
214
223
|
|
215
224
|
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ActiveRecord # :nodoc:
|
2
|
+
class Base # :nodoc:
|
3
|
+
|
4
|
+
# Synchronizes the passed in ActiveRecord instances with data
|
5
|
+
# from the database. This is like calling reload on an individual
|
6
|
+
# ActiveRecord instance but it is intended for use on multiple instances.
|
7
|
+
#
|
8
|
+
# This uses one query for all instance updates and then updates existing
|
9
|
+
# instances rather sending one query for each instance
|
10
|
+
#
|
11
|
+
# == Examples
|
12
|
+
# # Synchronizing existing models by matching on the primary key field
|
13
|
+
# posts = Post.find_by_author("Zach")
|
14
|
+
# <.. out of system changes occur to change author name from Zach to Zachary..>
|
15
|
+
# Post.synchronize posts
|
16
|
+
# posts.first.author # => "Zachary" instead of Zach
|
17
|
+
#
|
18
|
+
# # Synchronizing using custom key fields
|
19
|
+
# posts = Post.find_by_author("Zach")
|
20
|
+
# <.. out of system changes occur to change the address of author 'Zach' to 1245 Foo Ln ..>
|
21
|
+
# Post.synchronize posts, [:name] # queries on the :name column and not the :id column
|
22
|
+
# posts.first.address # => "1245 Foo Ln" instead of whatever it was
|
23
|
+
#
|
24
|
+
def self.synchronize(instances, keys=[self.primary_key])
|
25
|
+
return if instances.empty?
|
26
|
+
|
27
|
+
conditions = {}
|
28
|
+
order = ""
|
29
|
+
|
30
|
+
key_values = keys.map { |key| instances.map(&"#{key}".to_sym) }
|
31
|
+
keys.zip(key_values).each { |key, values| conditions[key] = values }
|
32
|
+
order = keys.map{ |key| "#{key} ASC" }.join(",")
|
33
|
+
|
34
|
+
klass = instances.first.class
|
35
|
+
|
36
|
+
fresh_instances = klass.find( :all, :conditions=>conditions, :order=>order )
|
37
|
+
instances.each do |instance|
|
38
|
+
matched_instance = fresh_instances.detect do |fresh_instance|
|
39
|
+
keys.all?{ |key| fresh_instance.send(key) == instance.send(key) }
|
40
|
+
end
|
41
|
+
|
42
|
+
if matched_instance
|
43
|
+
instance.clear_aggregation_cache
|
44
|
+
instance.clear_association_cache
|
45
|
+
instance.instance_variable_set '@attributes', matched_instance.attributes
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# See ActiveRecord::ConnectionAdapters::AbstractAdapter.synchronize
|
51
|
+
def synchronize(instances, key=[ActiveRecord::Base.primary_key])
|
52
|
+
self.class.synchronize(instances, key)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/test/import_test.rb
CHANGED
@@ -12,6 +12,13 @@ describe "#import" do
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
+
it "should not produce an error when importing empty arrays" do
|
16
|
+
assert_nothing_raised do
|
17
|
+
Topic.import []
|
18
|
+
Topic.import %w(title author_name), []
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
15
22
|
context "with :validation option" do
|
16
23
|
let(:columns) { %w(title author_name) }
|
17
24
|
let(:valid_values) { [[ "LDAP", "Jerry Carter"], ["Rails Recipes", "Chad Fowler"]] }
|
@@ -59,6 +66,26 @@ describe "#import" do
|
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
69
|
+
context "with :synchronize option" do
|
70
|
+
context "synchronizing on new records" do
|
71
|
+
let(:new_topics) { Build(3, :topics) }
|
72
|
+
|
73
|
+
it "doesn't reload any data (doesn't work)" do
|
74
|
+
Topic.import new_topics, :synchronize => new_topics
|
75
|
+
assert new_topics.all?(&:new_record?), "No record should have been reloaded"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "synchronizing on new records with explicit conditions" do
|
80
|
+
let(:new_topics) { Build(3, :topics) }
|
81
|
+
|
82
|
+
it "reloads data for existing in-memory instances" do
|
83
|
+
Topic.import(new_topics, :synchronize => new_topics, :synchronize_key => [:title] )
|
84
|
+
assert new_topics.all?(&:new_record?), "Records should have been reloaded"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
62
89
|
context "with an array of unsaved model instances" do
|
63
90
|
let(:topic) { Build(:topic, :title => "The RSpec Book", :author_name => "David Chelimsky")}
|
64
91
|
let(:topics) { Build(9, :topics) }
|
@@ -135,6 +162,13 @@ describe "#import" do
|
|
135
162
|
end
|
136
163
|
end
|
137
164
|
|
165
|
+
context "with an array of columns and an array of values" do
|
166
|
+
it "should import ids when specified" do
|
167
|
+
Topic.import [:id, :author_name, :title], [[99, "Bob Jones", "Topic 99"]]
|
168
|
+
assert_equal 99, Topic.last.id
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
138
172
|
context "ActiveRecord timestamps" do
|
139
173
|
context "when the timestamps columns are present" do
|
140
174
|
setup do
|
@@ -210,5 +244,4 @@ describe "#import" do
|
|
210
244
|
assert_equal "05/14/2010".to_date, Topic.last.last_read.to_date
|
211
245
|
end
|
212
246
|
end
|
213
|
-
|
214
247
|
end
|
@@ -142,4 +142,30 @@ def should_support_mysql_import_functionality
|
|
142
142
|
end
|
143
143
|
|
144
144
|
end
|
145
|
+
|
146
|
+
describe "#import with :synchronization option" do
|
147
|
+
let(:topics){ Array.new }
|
148
|
+
let(:values){ [ [topics.first.id, "Jerry Carter"], [topics.last.id, "Chad Fowler"] ]}
|
149
|
+
let(:columns){ %W(id author_name) }
|
150
|
+
|
151
|
+
setup do
|
152
|
+
topics << Topic.create!(:title=>"LDAP", :author_name=>"Big Bird")
|
153
|
+
topics << Topic.create!(:title=>"Rails Recipes", :author_name=>"Elmo")
|
154
|
+
end
|
155
|
+
|
156
|
+
it "synchronizes passed in ActiveRecord model instances with the data just imported" do
|
157
|
+
columns2update = [ 'author_name' ]
|
158
|
+
|
159
|
+
expected_count = Topic.count
|
160
|
+
Topic.import( columns, values,
|
161
|
+
:validate=>false,
|
162
|
+
:on_duplicate_key_update=>columns2update,
|
163
|
+
:synchronize=>topics )
|
164
|
+
|
165
|
+
assert_equal expected_count, Topic.count, "no new records should have been created!"
|
166
|
+
assert_equal "Jerry Carter", topics.first.author_name, "wrong author!"
|
167
|
+
assert_equal "Chad Fowler", topics.last.author_name, "wrong author!"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
145
171
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe ".synchronize" do
|
4
|
+
let(:topics){ Generate(3, :topics) }
|
5
|
+
let(:titles){ %w(one two three) }
|
6
|
+
|
7
|
+
setup do
|
8
|
+
# update records outside of ActiveRecord knowing about it
|
9
|
+
Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[0]}_haha' WHERE id=#{topics[0].id}", "Updating record 1 without ActiveRecord" )
|
10
|
+
Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[1]}_haha' WHERE id=#{topics[1].id}", "Updating record 2 without ActiveRecord" )
|
11
|
+
Topic.connection.execute( "UPDATE #{Topic.table_name} SET title='#{titles[2]}_haha' WHERE id=#{topics[2].id}", "Updating record 3 without ActiveRecord" )
|
12
|
+
end
|
13
|
+
|
14
|
+
it "reloads data for the specified records" do
|
15
|
+
Book.synchronize topics
|
16
|
+
|
17
|
+
actual_titles = topics.map(&:title)
|
18
|
+
assert_equal "#{titles[0]}_haha", actual_titles[0], "the first record was not correctly updated"
|
19
|
+
assert_equal "#{titles[1]}_haha", actual_titles[1], "the second record was not correctly updated"
|
20
|
+
assert_equal "#{titles[2]}_haha", actual_titles[2], "the third record was not correctly updated"
|
21
|
+
end
|
22
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-import
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 6
|
10
|
+
version: 0.2.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Zach Dennis
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-04-06 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -42,10 +42,8 @@ extensions: []
|
|
42
42
|
|
43
43
|
extra_rdoc_files:
|
44
44
|
- README.markdown
|
45
|
-
- README.textile
|
46
45
|
files:
|
47
46
|
- README.markdown
|
48
|
-
- README.textile
|
49
47
|
- Rakefile
|
50
48
|
- VERSION
|
51
49
|
- lib/activerecord-import.rb
|
@@ -64,6 +62,7 @@ files:
|
|
64
62
|
- lib/activerecord-import/mysql2.rb
|
65
63
|
- lib/activerecord-import/postgresql.rb
|
66
64
|
- lib/activerecord-import/sqlite3.rb
|
65
|
+
- lib/activerecord-import/synchronize.rb
|
67
66
|
- test/active_record/connection_adapter_test.rb
|
68
67
|
- test/adapters/mysql.rb
|
69
68
|
- test/adapters/mysql2.rb
|
@@ -84,6 +83,7 @@ files:
|
|
84
83
|
- test/support/generate.rb
|
85
84
|
- test/support/mysql/assertions.rb
|
86
85
|
- test/support/mysql/import_examples.rb
|
86
|
+
- test/synchronize_test.rb
|
87
87
|
- test/test_helper.rb
|
88
88
|
has_rdoc: true
|
89
89
|
homepage: http://github.com/zdennis/activerecord-import
|
@@ -140,4 +140,5 @@ test_files:
|
|
140
140
|
- test/support/generate.rb
|
141
141
|
- test/support/mysql/assertions.rb
|
142
142
|
- test/support/mysql/import_examples.rb
|
143
|
+
- test/synchronize_test.rb
|
143
144
|
- test/test_helper.rb
|
data/README.textile
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
h1. activerecord-import
|
2
|
-
|
3
|
-
activerecord-import is a library for bulk inserting data using ActiveRecord.
|
4
|
-
|
5
|
-
h2. Why activerecord-import?
|
6
|
-
|
7
|
-
Because plain-vanilla, out-of-the-box ActiveRecord doesn't provide support for inserting large amounts of data efficiently. With vanilla ActiveRecord you would have to perform individual save operations on each model:
|
8
|
-
|
9
|
-
<pre>
|
10
|
-
10.times do |i|
|
11
|
-
Book.create! :name => "book #{i}"
|
12
|
-
end
|
13
|
-
</pre>
|
14
|
-
|
15
|
-
This may work fine if all you have is 10 records, but if you have hundreds, thousands, or millions of records it can turn into a nightmare. This is where activerecord-import comes into play.
|
16
|
-
|
17
|
-
h2. An Introductory Example
|
18
|
-
|
19
|
-
Here's an example with equivalent behaviour using the @#import@ method:
|
20
|
-
|
21
|
-
<pre>
|
22
|
-
books = []
|
23
|
-
10.times do |i|
|
24
|
-
books << Book.new(:name => "book #{i}")
|
25
|
-
end
|
26
|
-
Book.import books
|
27
|
-
</pre>
|
28
|
-
|
29
|
-
This call to import does whatever is most efficient for the underlying database adapter. Pretty slick, eh?
|
30
|
-
|
31
|
-
h2. Features
|
32
|
-
|
33
|
-
Here's a list of some of the high-level features that activerecord-import provides:
|
34
|
-
|
35
|
-
* activerecord-import can work with raw columns and arrays of values (fastest)
|
36
|
-
* activerecord-import works with model objects (faster)
|
37
|
-
* activerecord-import can perform validations (fast)
|
38
|
-
* activerecord-import can perform on duplicate key updates (requires mysql)
|
39
|
-
|
40
|
-
|
41
|
-
h1. Upgrading from ar-extensions
|
42
|
-
|
43
|
-
activerecord-import replaces the ar-extensions library and is compatible with Rails 3. It provides the exact same API for importing data, but it does not include any additional ar-extensions functionality.
|
44
|
-
|
45
|
-
h1. License
|
46
|
-
|
47
|
-
This is licensed under the ruby license.
|
48
|
-
|
49
|
-
h1. Author
|
50
|
-
|
51
|
-
Zach Dennis (zach.dennis@gmail.com)
|
52
|
-
|
53
|
-
h1. Contributors
|
54
|
-
|
55
|
-
* Blythe Dunham
|
56
|
-
* Gabe da Silveira
|
57
|
-
* Henry Work
|
58
|
-
* James Herdman
|
59
|
-
* Marcus Crafter
|
60
|
-
* Thibaud Guillaume-Gentil
|
61
|
-
* Mark Van Holstyn
|
62
|
-
* Victor Costan
|