activerecord-import 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile +4 -2
- data/README.markdown +8 -7
- data/Rakefile +1 -1
- data/benchmarks/benchmark.rb +2 -1
- data/benchmarks/lib/cli_parser.rb +1 -1
- data/benchmarks/lib/{mysql_benchmark.rb → mysql2_benchmark.rb} +6 -7
- data/gemfiles/3.1.gemfile +0 -2
- data/gemfiles/3.2.gemfile +0 -2
- data/gemfiles/4.0.gemfile +0 -2
- data/gemfiles/4.1.gemfile +0 -2
- data/gemfiles/4.2.gemfile +0 -2
- data/gemfiles/5.0.gemfile +0 -2
- data/lib/activerecord-import/adapters/abstract_adapter.rb +11 -2
- data/lib/activerecord-import/adapters/mysql_adapter.rb +14 -2
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +105 -1
- data/lib/activerecord-import/base.rb +0 -1
- data/lib/activerecord-import/import.rb +92 -21
- data/lib/activerecord-import/version.rb +1 -1
- data/test/database.yml.sample +0 -12
- data/test/import_test.rb +1 -1
- data/test/jdbcmysql/import_test.rb +2 -2
- data/test/jdbcpostgresql/import_test.rb +0 -1
- data/test/models/book.rb +4 -4
- data/test/models/promotion.rb +3 -0
- data/test/models/question.rb +3 -0
- data/test/models/rule.rb +3 -0
- data/test/models/topic.rb +1 -1
- data/test/mysql2/import_test.rb +2 -3
- data/test/mysqlspatial2/import_test.rb +2 -2
- data/test/postgresql/import_test.rb +4 -0
- data/test/schema/generic_schema.rb +19 -2
- data/test/support/{mysql/assertions.rb → assertions.rb} +12 -3
- data/test/support/factories.rb +14 -0
- data/test/support/mysql/import_examples.rb +28 -119
- data/test/support/postgresql/import_examples.rb +156 -1
- data/test/support/shared_examples/on_duplicate_key_update.rb +92 -0
- data/test/test_helper.rb +5 -1
- data/test/travis/build.sh +12 -8
- data/test/travis/database.yml +0 -12
- metadata +14 -23
- data/lib/activerecord-import/active_record/adapters/em_mysql2_adapter.rb +0 -8
- data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +0 -6
- data/lib/activerecord-import/em_mysql2.rb +0 -7
- data/lib/activerecord-import/mysql.rb +0 -7
- data/test/adapters/em_mysql2.rb +0 -1
- data/test/adapters/mysql.rb +0 -1
- data/test/adapters/mysqlspatial.rb +0 -1
- data/test/em_mysql2/import_test.rb +0 -6
- data/test/mysql/import_test.rb +0 -6
- data/test/mysqlspatial/import_test.rb +0 -6
- data/test/support/em-synchrony_extensions.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f8e8fa0974d52a903ef20a78f196030a515687d
|
4
|
+
data.tar.gz: 0ccc98cac5b73a78e9e9ffe57c2b74504c2b9e81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1562542547afcea6d646e3a5fcf2aabd806471f0000b2434d6fd979a60c2bc93b90e115e09e2b67df00b35a2244c67d0f2554e9480f0c5c6bb6f3dbdebd2e8be
|
7
|
+
data.tar.gz: 45f2552a368c3b9c70bf7abec385462f34a66e423a65db1e99536b8f3b383c02d21af4cb5b5a313c3769aa59a919bd4db34ddb6927ced52fae88272afaeb28de
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
## Changes in 0.12.0
|
2
|
+
|
3
|
+
### New Features
|
4
|
+
|
5
|
+
* PostgreSQL UPSERT support has been added. Thanks @jkowens via \#218
|
6
|
+
|
7
|
+
### Fixes
|
8
|
+
|
9
|
+
* has_one and has_many associations will now be recursively imported regardless of :autosave being set. Thanks @sferik, @jkowens via \#243, \#234
|
10
|
+
* Fixing an issue with enum column support for Rails > 4.1. Thanks @aquajach via \#235
|
11
|
+
|
12
|
+
### Removals
|
13
|
+
|
14
|
+
* Support for em-synchrony has been removed since it appears the project has been abandoned. Thanks @sferik, @zdennis via \#239
|
15
|
+
* Support for the mysql gem/adapter has been removed since it has officially been abandoned. Use the mysql2 gem/adapter instead. Thanks @sferik, @zdennis via \#239
|
16
|
+
|
17
|
+
### Misc
|
18
|
+
|
19
|
+
* Cleaned up TravisCI output and removing deprecation warnings. Thanks @jkowens, @zdennis \#242
|
20
|
+
|
21
|
+
|
22
|
+
## Changes before 0.12.0
|
23
|
+
|
24
|
+
> Never look back. What's gone is now history. But in the process make memory of events to help you understand what will help you to make your dream a true story. Mistakes of the past are lessons, success of the past is inspiration. – Dr. Anil Kr Sinha
|
data/Gemfile
CHANGED
@@ -2,8 +2,6 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
gem "pry-byebug"
|
6
|
-
|
7
5
|
# Database Adapters
|
8
6
|
platforms :ruby do
|
9
7
|
gem "mysql2", "~> 0.3.0"
|
@@ -38,6 +36,10 @@ platforms :mri_19 do
|
|
38
36
|
gem "debugger"
|
39
37
|
end
|
40
38
|
|
39
|
+
platforms :ruby do
|
40
|
+
gem "pry-byebug"
|
41
|
+
end
|
42
|
+
|
41
43
|
version = ENV['AR_VERSION'] || "4.2"
|
42
44
|
|
43
45
|
if version > "4.0"
|
data/README.markdown
CHANGED
@@ -5,19 +5,19 @@ activerecord-import is a library for bulk inserting data using ActiveRecord.
|
|
5
5
|
One of its major features is following activerecord associations and generating the minimal
|
6
6
|
number of SQL insert statements required, avoiding the N+1 insert problem. An example probably
|
7
7
|
explains it best. Say you had a schema like this:
|
8
|
-
|
8
|
+
|
9
9
|
- Publishers have Books
|
10
10
|
- Books have Reviews
|
11
|
-
|
11
|
+
|
12
12
|
and you wanted to bulk insert 100 new publishers with 10K books and 3 reviews per book. This library will follow the associations
|
13
13
|
down and generate only 3 SQL insert statements - one for the publishers, one for the books, and one for the reviews.
|
14
|
-
|
14
|
+
|
15
15
|
In contrast, the standard ActiveRecord save would generate
|
16
16
|
100 insert statements for the publishers, then it would visit each publisher and save all the books:
|
17
17
|
100 * 10,000 = 1,000,000 SQL insert statements
|
18
18
|
and then the reviews:
|
19
19
|
100 * 10,000 * 3 = 3M SQL insert statements,
|
20
|
-
|
20
|
+
|
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
|
|
@@ -54,7 +54,7 @@ To understand how rubygems loads code you can reference the following:
|
|
54
54
|
http://guides.rubygems.org/patterns/#loading_code
|
55
55
|
|
56
56
|
And an example of how active_record dynamically load adapters:
|
57
|
-
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/connection_specification.rb
|
57
|
+
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/connection_specification.rb
|
58
58
|
|
59
59
|
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 propegate 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.
|
60
60
|
|
@@ -77,7 +77,7 @@ When rubygems pushes the `lib` folder onto the load path a `require` will now fi
|
|
77
77
|
|
78
78
|
# License
|
79
79
|
|
80
|
-
This is licensed under the ruby license.
|
80
|
+
This is licensed under the ruby license.
|
81
81
|
|
82
82
|
# Author
|
83
83
|
|
@@ -85,11 +85,12 @@ Zach Dennis (zach.dennis@gmail.com)
|
|
85
85
|
|
86
86
|
# Contributors
|
87
87
|
|
88
|
+
* Jordan Owens
|
88
89
|
* Blythe Dunham
|
89
90
|
* Gabe da Silveira
|
90
91
|
* Henry Work
|
91
92
|
* James Herdman
|
92
93
|
* Marcus Crafter
|
93
94
|
* Thibaud Guillaume-Gentil
|
94
|
-
* Mark Van Holstyn
|
95
|
+
* Mark Van Holstyn
|
95
96
|
* Victor Costan
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ namespace :display do
|
|
13
13
|
end
|
14
14
|
task :default => ["display:notice"]
|
15
15
|
|
16
|
-
ADAPTERS = %w(
|
16
|
+
ADAPTERS = %w(mysql2 jdbcmysql jdbcpostgresql postgresql sqlite3 seamless_database_pool mysql2spatial spatialite postgis)
|
17
17
|
ADAPTERS.each do |adapter|
|
18
18
|
namespace :test do
|
19
19
|
desc "Runs #{adapter} database tests."
|
data/benchmarks/benchmark.rb
CHANGED
@@ -4,6 +4,8 @@ require "active_record"
|
|
4
4
|
|
5
5
|
benchmark_dir = File.dirname(__FILE__)
|
6
6
|
|
7
|
+
$LOAD_PATH.unshift('.')
|
8
|
+
|
7
9
|
# Get the gem into the load path
|
8
10
|
$LOAD_PATH.unshift(File.join(benchmark_dir, '..', 'lib'))
|
9
11
|
|
@@ -64,4 +66,3 @@ end
|
|
64
66
|
|
65
67
|
puts
|
66
68
|
puts "Done with benchmark!"
|
67
|
-
|
@@ -1,22 +1,21 @@
|
|
1
|
-
class
|
2
|
-
|
1
|
+
class Mysql2Benchmark < BenchmarkBase
|
2
|
+
|
3
3
|
def benchmark_all( array_of_cols_and_vals )
|
4
4
|
methods = self.methods.find_all { |m| m =~ /benchmark_/ }
|
5
5
|
methods.delete_if{ |m| m =~ /benchmark_(all|model)/ }
|
6
6
|
methods.each { |method| self.send( method, array_of_cols_and_vals ) }
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def benchmark_myisam( array_of_cols_and_vals )
|
10
10
|
bm_model( TestMyISAM, array_of_cols_and_vals )
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
def benchmark_innodb( array_of_cols_and_vals )
|
14
14
|
bm_model( TestInnoDb, array_of_cols_and_vals )
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def benchmark_memory( array_of_cols_and_vals )
|
18
18
|
bm_model( TestMemory, array_of_cols_and_vals )
|
19
19
|
end
|
20
|
-
|
21
|
-
end
|
22
20
|
|
21
|
+
end
|
data/gemfiles/3.1.gemfile
CHANGED
data/gemfiles/3.2.gemfile
CHANGED
data/gemfiles/4.0.gemfile
CHANGED
data/gemfiles/4.1.gemfile
CHANGED
data/gemfiles/4.2.gemfile
CHANGED
data/gemfiles/5.0.gemfile
CHANGED
@@ -44,8 +44,13 @@ module ActiveRecord::Import::AbstractAdapter
|
|
44
44
|
# Returns an array of post SQL statements given the passed in options.
|
45
45
|
def post_sql_statements( table_name, options ) # :nodoc:
|
46
46
|
post_sql_statements = []
|
47
|
-
|
48
|
-
|
47
|
+
|
48
|
+
if supports_on_duplicate_key_update?
|
49
|
+
if options[:on_duplicate_key_ignore] && respond_to?(:sql_for_on_duplicate_key_ignore)
|
50
|
+
post_sql_statements << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
|
51
|
+
elsif options[:on_duplicate_key_update]
|
52
|
+
post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
|
53
|
+
end
|
49
54
|
end
|
50
55
|
|
51
56
|
#custom user post_sql
|
@@ -62,5 +67,9 @@ module ActiveRecord::Import::AbstractAdapter
|
|
62
67
|
def max_allowed_packet
|
63
68
|
NO_MAX_PACKET
|
64
69
|
end
|
70
|
+
|
71
|
+
def supports_on_duplicate_key_update?
|
72
|
+
false
|
73
|
+
end
|
65
74
|
end
|
66
75
|
end
|
@@ -60,6 +60,19 @@ module ActiveRecord::Import::MysqlAdapter
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
+
# Add a column to be updated on duplicate key update
|
64
|
+
def add_column_for_on_duplicate_key_update( column, options={} ) # :nodoc:
|
65
|
+
if options.include?(:on_duplicate_key_update)
|
66
|
+
columns = options[:on_duplicate_key_update]
|
67
|
+
case columns
|
68
|
+
when Array then columns << column.to_sym unless columns.include?(column.to_sym)
|
69
|
+
when Hash then columns[column.to_sym] = column.to_sym
|
70
|
+
end
|
71
|
+
else
|
72
|
+
options[:on_duplicate_key_update] = [ column.to_sym ]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
63
76
|
# Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
|
64
77
|
# in +args+.
|
65
78
|
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
@@ -86,7 +99,6 @@ module ActiveRecord::Import::MysqlAdapter
|
|
86
99
|
end
|
87
100
|
|
88
101
|
def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
|
89
|
-
sql = ' ON DUPLICATE KEY UPDATE '
|
90
102
|
results = hsh.map do |column1, column2|
|
91
103
|
qc1 = quote_column_name( column1 )
|
92
104
|
qc2 = quote_column_name( column2 )
|
@@ -95,7 +107,7 @@ module ActiveRecord::Import::MysqlAdapter
|
|
95
107
|
results.join( ',')
|
96
108
|
end
|
97
109
|
|
98
|
-
#
|
110
|
+
# Return true if the statement is a duplicate key record error
|
99
111
|
def duplicate_key_update_error?(exception)# :nodoc:
|
100
112
|
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
|
101
113
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
module ActiveRecord::Import::PostgreSQLAdapter
|
2
2
|
include ActiveRecord::Import::ImportSupport
|
3
|
+
include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
|
4
|
+
|
5
|
+
MIN_VERSION_FOR_UPSERT = 90500
|
3
6
|
|
4
7
|
def insert_many( sql, values, *args ) # :nodoc:
|
5
8
|
number_of_inserts = 1
|
@@ -24,12 +27,113 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
24
27
|
|
25
28
|
def post_sql_statements( table_name, options ) # :nodoc:
|
26
29
|
unless options[:primary_key].blank?
|
27
|
-
super(table_name, options) << ("
|
30
|
+
super(table_name, options) << ("RETURNING #{options[:primary_key]}")
|
28
31
|
else
|
29
32
|
super(table_name, options)
|
30
33
|
end
|
31
34
|
end
|
32
35
|
|
36
|
+
# Add a column to be updated on duplicate key update
|
37
|
+
def add_column_for_on_duplicate_key_update( column, options={} ) # :nodoc:
|
38
|
+
arg = options[:on_duplicate_key_update]
|
39
|
+
if arg.is_a?( Hash )
|
40
|
+
columns = arg.fetch( :columns ) { arg[:columns] = [] }
|
41
|
+
case columns
|
42
|
+
when Array then columns << column.to_sym unless columns.include?( column.to_sym )
|
43
|
+
when Hash then columns[column.to_sym] = column.to_sym
|
44
|
+
end
|
45
|
+
elsif arg.is_a?( Array )
|
46
|
+
arg << column.to_sym unless arg.include?( column.to_sym )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a generated ON CONFLICT DO NOTHING statement given the passed
|
51
|
+
# in +args+.
|
52
|
+
def sql_for_on_duplicate_key_ignore( table_name, *args ) # :nodoc:
|
53
|
+
arg = args.first
|
54
|
+
if arg.is_a?( Hash )
|
55
|
+
conflict_target = sql_for_conflict_target( arg )
|
56
|
+
end
|
57
|
+
" ON CONFLICT #{conflict_target}DO NOTHING"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a generated ON CONFLICT DO UPDATE statement given the passed
|
61
|
+
# in +args+.
|
62
|
+
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
63
|
+
arg = args.first
|
64
|
+
if arg.is_a?( Array ) || arg.is_a?( String )
|
65
|
+
arg = { :columns => arg }
|
66
|
+
end
|
67
|
+
return unless arg.is_a?( Hash )
|
68
|
+
|
69
|
+
sql = " ON CONFLICT "
|
70
|
+
conflict_target = sql_for_conflict_target( arg )
|
71
|
+
|
72
|
+
columns = arg.fetch( :columns, [] )
|
73
|
+
if columns.respond_to?( :empty? ) && columns.empty?
|
74
|
+
return sql << "#{conflict_target}DO NOTHING"
|
75
|
+
end
|
76
|
+
|
77
|
+
conflict_target ||= sql_for_default_conflict_target( table_name )
|
78
|
+
unless conflict_target
|
79
|
+
raise ArgumentError, 'Expected :conflict_target or :constraint_name to be specified'
|
80
|
+
end
|
81
|
+
|
82
|
+
sql << "#{conflict_target}DO UPDATE SET "
|
83
|
+
if columns.is_a?( Array )
|
84
|
+
sql << sql_for_on_duplicate_key_update_as_array( table_name, columns )
|
85
|
+
elsif columns.is_a?( Hash )
|
86
|
+
sql << sql_for_on_duplicate_key_update_as_hash( table_name, columns )
|
87
|
+
elsif columns.is_a?( String )
|
88
|
+
sql << columns
|
89
|
+
else
|
90
|
+
raise ArgumentError, 'Expected :columns to be an Array or Hash'
|
91
|
+
end
|
92
|
+
sql
|
93
|
+
end
|
94
|
+
|
95
|
+
def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
|
96
|
+
results = arr.map do |column|
|
97
|
+
qc = quote_column_name( column )
|
98
|
+
"#{qc}=EXCLUDED.#{qc}"
|
99
|
+
end
|
100
|
+
results.join( ',' )
|
101
|
+
end
|
102
|
+
|
103
|
+
def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
|
104
|
+
results = hsh.map do |column1, column2|
|
105
|
+
qc1 = quote_column_name( column1 )
|
106
|
+
qc2 = quote_column_name( column2 )
|
107
|
+
"#{qc1}=EXCLUDED.#{qc2}"
|
108
|
+
end
|
109
|
+
results.join( ',' )
|
110
|
+
end
|
111
|
+
|
112
|
+
def sql_for_conflict_target( args={} )
|
113
|
+
if constraint_name = args[:constraint_name]
|
114
|
+
"ON CONSTRAINT #{constraint_name} "
|
115
|
+
elsif conflict_target = args[:conflict_target]
|
116
|
+
'(' << Array( conflict_target ).join( ', ' ) << ') '
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def sql_for_default_conflict_target( table_name )
|
121
|
+
"(#{primary_key( table_name )}) "
|
122
|
+
end
|
123
|
+
|
124
|
+
# Return true if the statement is a duplicate key record error
|
125
|
+
def duplicate_key_update_error?(exception)# :nodoc:
|
126
|
+
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
|
127
|
+
end
|
128
|
+
|
129
|
+
def supports_on_duplicate_key_update?(current_version=self.postgresql_version)
|
130
|
+
current_version >= MIN_VERSION_FOR_UPSERT
|
131
|
+
end
|
132
|
+
|
133
|
+
def supports_on_duplicate_key_ignore?(current_version=self.postgresql_version)
|
134
|
+
supports_on_duplicate_key_update?(current_version)
|
135
|
+
end
|
136
|
+
|
33
137
|
def support_setting_primary_key_of_imported_objects?
|
34
138
|
true
|
35
139
|
end
|
@@ -116,7 +116,7 @@ class ActiveRecord::Base
|
|
116
116
|
# supports on duplicate key update functionality, otherwise
|
117
117
|
# returns false.
|
118
118
|
def supports_on_duplicate_key_update?
|
119
|
-
connection.
|
119
|
+
connection.supports_on_duplicate_key_update?
|
120
120
|
end
|
121
121
|
|
122
122
|
# returns true if the current database connection adapter
|
@@ -165,19 +165,23 @@ class ActiveRecord::Base
|
|
165
165
|
# below for what +options+ are available.
|
166
166
|
#
|
167
167
|
# == Options
|
168
|
-
# * +validate+ - true|false, tells import whether or not to use
|
168
|
+
# * +validate+ - true|false, tells import whether or not to use
|
169
169
|
# ActiveRecord validations. Validations are enforced by default.
|
170
|
-
# * +
|
171
|
-
#
|
172
|
-
#
|
170
|
+
# * +ignore+ - true|false, tells import to use MySQL's INSERT IGNORE
|
171
|
+
# to discard records that contain duplicate keys.
|
172
|
+
# * +on_duplicate_key_ignore+ - true|false, tells import to use
|
173
|
+
# Postgres 9.5+ ON CONFLICT DO NOTHING.
|
174
|
+
# * +on_duplicate_key_update+ - an Array or Hash, tells import to
|
175
|
+
# use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+ ON CONFLICT
|
176
|
+
# DO UPDATE ability. See On Duplicate Key Update below.
|
173
177
|
# * +synchronize+ - an array of ActiveRecord instances for the model
|
174
178
|
# that you are currently importing data into. This synchronizes
|
175
179
|
# existing model instances in memory with updates from the import.
|
176
|
-
# * +timestamps+ - true|false, tells import to not add timestamps
|
180
|
+
# * +timestamps+ - true|false, tells import to not add timestamps
|
177
181
|
# (if false) even if record timestamps is disabled in ActiveRecord::Base
|
178
|
-
# * +recursive - true|false, tells import to import all
|
179
|
-
# if the adapter supports setting the primary keys of the
|
180
|
-
# objects.
|
182
|
+
# * +recursive - true|false, tells import to import all has_many/has_one
|
183
|
+
# associations if the adapter supports setting the primary keys of the
|
184
|
+
# newly imported objects.
|
181
185
|
#
|
182
186
|
# == Examples
|
183
187
|
# class BlogPost < ActiveRecord::Base ; end
|
@@ -211,7 +215,7 @@ class ActiveRecord::Base
|
|
211
215
|
# BlogPost.import posts, :synchronize => posts, :synchronize_keys => [:title]
|
212
216
|
# puts posts.first.persisted? # => true
|
213
217
|
#
|
214
|
-
# == On Duplicate Key Update (MySQL
|
218
|
+
# == On Duplicate Key Update (MySQL)
|
215
219
|
#
|
216
220
|
# The :on_duplicate_key_update option can be either an Array or a Hash.
|
217
221
|
#
|
@@ -225,13 +229,73 @@ class ActiveRecord::Base
|
|
225
229
|
#
|
226
230
|
# ==== Using A Hash
|
227
231
|
#
|
228
|
-
# The :on_duplicate_key_update option can be a hash of column
|
232
|
+
# The :on_duplicate_key_update option can be a hash of column names
|
229
233
|
# to model attribute name mappings. This gives you finer grained
|
230
234
|
# control over what fields are updated with what attributes on your
|
231
235
|
# model. Below is an example:
|
232
236
|
#
|
233
237
|
# BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
|
234
238
|
#
|
239
|
+
# == On Duplicate Key Update (Postgres 9.5+)
|
240
|
+
#
|
241
|
+
# The :on_duplicate_key_update option can be an Array or a Hash with up to
|
242
|
+
# two attributes, :conflict_target or :constraint_name and :columns.
|
243
|
+
#
|
244
|
+
# ==== Using an Array
|
245
|
+
#
|
246
|
+
# The :on_duplicate_key_update option can be an array of column
|
247
|
+
# names. This option only handles inserts that conflict with the
|
248
|
+
# primary key. If a table does not have a primary key, this will
|
249
|
+
# not work. The column names are the only fields that are updated
|
250
|
+
# if a duplicate record is found. Below is an example:
|
251
|
+
#
|
252
|
+
# BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
|
253
|
+
#
|
254
|
+
# ==== Using a Hash
|
255
|
+
#
|
256
|
+
# The :on_duplicate_update option can be a hash with up to two attributes,
|
257
|
+
# :conflict_target or constraint_name, and :columns. Unlike MySQL, Postgres
|
258
|
+
# requires the conflicting constraint to be explicitly specified. Using this
|
259
|
+
# option allows you to specify a constraint other than the primary key.
|
260
|
+
#
|
261
|
+
# ====== :conflict_target
|
262
|
+
#
|
263
|
+
# The :conflict_target attribute specifies the columns that make up the
|
264
|
+
# conflicting unique constraint and can be a single column or an array of
|
265
|
+
# column names. This attribute is ignored if :constraint_name is included,
|
266
|
+
# but it is the preferred method of identifying a constraint. It will
|
267
|
+
# default to the primary key. Below is an example:
|
268
|
+
#
|
269
|
+
# BlogPost.import columns, values, :on_duplicate_key_update=>{ :conflict_target => [:author_id, :slug], :columns => [ :date_modified ] }
|
270
|
+
#
|
271
|
+
# ====== :constraint_name
|
272
|
+
#
|
273
|
+
# The :constraint_name attribute explicitly identifies the conflicting
|
274
|
+
# unique index by name. Postgres documentation discourages using this method
|
275
|
+
# of identifying an index unless absolutely necessary. Below is an example:
|
276
|
+
#
|
277
|
+
# BlogPost.import columns, values, :on_duplicate_key_update=>{ :constraint_name => :blog_posts_pkey, :columns => [ :date_modified ] }
|
278
|
+
#
|
279
|
+
# ====== :columns
|
280
|
+
#
|
281
|
+
# The :columns attribute can be either an Array or a Hash.
|
282
|
+
#
|
283
|
+
# ======== Using an Array
|
284
|
+
#
|
285
|
+
# The :columns attribute can be an array of column names. The column names
|
286
|
+
# are the only fields that are updated if a duplicate record is found.
|
287
|
+
# Below is an example:
|
288
|
+
#
|
289
|
+
# BlogPost.import columns, values, :on_duplicate_key_update=>{ :conflict_target => :slug, :columns => [ :date_modified, :content, :author ] }
|
290
|
+
#
|
291
|
+
# ======== Using a Hash
|
292
|
+
#
|
293
|
+
# The :columns option can be a hash of column names to model attribute name
|
294
|
+
# mappings. This gives you finer grained control over what fields are updated
|
295
|
+
# with what attributes on your model. Below is an example:
|
296
|
+
#
|
297
|
+
# BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :conflict_target => :slug, :columns => { :title => :title } }
|
298
|
+
#
|
235
299
|
# = Returns
|
236
300
|
# This returns an object which responds to +failed_instances+ and +num_inserts+.
|
237
301
|
# * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
|
@@ -275,7 +339,14 @@ class ActiveRecord::Base
|
|
275
339
|
# this next line breaks sqlite.so with a segmentation fault
|
276
340
|
# if model.new_record? || options[:on_duplicate_key_update]
|
277
341
|
column_names.map do |name|
|
278
|
-
|
342
|
+
name = name.to_s
|
343
|
+
if respond_to?(:defined_enums) && defined_enums.has_key?(name) # ActiveRecord 5
|
344
|
+
model.read_attribute(name)
|
345
|
+
elsif model.class.column_defaults[name].is_a?(Integer)
|
346
|
+
model.read_attribute(name)
|
347
|
+
else
|
348
|
+
model.read_attribute_before_type_cast(name)
|
349
|
+
end
|
279
350
|
end
|
280
351
|
# end
|
281
352
|
end
|
@@ -445,7 +516,6 @@ class ActiveRecord::Base
|
|
445
516
|
# now, for all the dirty associations, collect them into a new set of models, then recurse.
|
446
517
|
# notes:
|
447
518
|
# does not handle associations that reference themselves
|
448
|
-
# assumes that the only associations to be saved are marked with :autosave
|
449
519
|
# should probably take a hash to associations to follow.
|
450
520
|
associated_objects_by_class={}
|
451
521
|
models.each {|model| find_associated_objects_for_import(associated_objects_by_class, model) }
|
@@ -462,12 +532,18 @@ class ActiveRecord::Base
|
|
462
532
|
def find_associated_objects_for_import(associated_objects_by_class, model)
|
463
533
|
associated_objects_by_class[model.class.name]||={}
|
464
534
|
|
465
|
-
|
535
|
+
association_reflections =
|
536
|
+
model.class.reflect_on_all_associations(:has_one) +
|
537
|
+
model.class.reflect_on_all_associations(:has_many)
|
538
|
+
association_reflections.each do |association_reflection|
|
466
539
|
associated_objects_by_class[model.class.name][association_reflection.name]||=[]
|
467
540
|
|
468
541
|
association = model.association(association_reflection.name)
|
469
542
|
association.loaded!
|
470
543
|
|
544
|
+
# Wrap target in an array if not already
|
545
|
+
association = Array(association.target)
|
546
|
+
|
471
547
|
changed_objects = association.select {|a| a.new_record? || a.changed?}
|
472
548
|
changed_objects.each do |child|
|
473
549
|
child.send("#{association_reflection.foreign_key}=", model.id)
|
@@ -529,13 +605,8 @@ class ActiveRecord::Base
|
|
529
605
|
array_of_attributes.each { |arr| arr << value }
|
530
606
|
end
|
531
607
|
|
532
|
-
if supports_on_duplicate_key_update?
|
533
|
-
|
534
|
-
options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array) && !options[:on_duplicate_key_update].include?(key.to_sym)
|
535
|
-
options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
|
536
|
-
else
|
537
|
-
options[:on_duplicate_key_update] = [ key.to_sym ]
|
538
|
-
end
|
608
|
+
if supports_on_duplicate_key_update?
|
609
|
+
connection.add_column_for_on_duplicate_key_update(key, options)
|
539
610
|
end
|
540
611
|
end
|
541
612
|
end
|