activerecord-import 0.11.0 → 0.12.0
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.
- 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
|