ghazel-ar-extensions 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +145 -0
- data/README +169 -0
- data/Rakefile +61 -0
- data/config/database.yml +7 -0
- data/config/database.yml.template +7 -0
- data/config/mysql.schema +72 -0
- data/config/postgresql.schema +39 -0
- data/db/migrate/generic_schema.rb +97 -0
- data/db/migrate/mysql_schema.rb +32 -0
- data/db/migrate/oracle_schema.rb +5 -0
- data/db/migrate/version.rb +4 -0
- data/init.rb +31 -0
- data/lib/ar-extensions/adapters/abstract_adapter.rb +146 -0
- data/lib/ar-extensions/adapters/mysql.rb +10 -0
- data/lib/ar-extensions/adapters/oracle.rb +14 -0
- data/lib/ar-extensions/adapters/postgresql.rb +9 -0
- data/lib/ar-extensions/adapters/sqlite.rb +7 -0
- data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
- data/lib/ar-extensions/create_and_update.rb +509 -0
- data/lib/ar-extensions/csv.rb +309 -0
- data/lib/ar-extensions/delete/mysql.rb +3 -0
- data/lib/ar-extensions/delete.rb +143 -0
- data/lib/ar-extensions/extensions.rb +513 -0
- data/lib/ar-extensions/finder_options/mysql.rb +6 -0
- data/lib/ar-extensions/finder_options.rb +275 -0
- data/lib/ar-extensions/finders.rb +94 -0
- data/lib/ar-extensions/foreign_keys.rb +70 -0
- data/lib/ar-extensions/fulltext/mysql.rb +44 -0
- data/lib/ar-extensions/fulltext.rb +62 -0
- data/lib/ar-extensions/import/mysql.rb +50 -0
- data/lib/ar-extensions/import/postgresql.rb +0 -0
- data/lib/ar-extensions/import/sqlite.rb +22 -0
- data/lib/ar-extensions/import.rb +348 -0
- data/lib/ar-extensions/insert_select/mysql.rb +7 -0
- data/lib/ar-extensions/insert_select.rb +178 -0
- data/lib/ar-extensions/synchronize.rb +30 -0
- data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
- data/lib/ar-extensions/temporary_table.rb +131 -0
- data/lib/ar-extensions/union/mysql.rb +6 -0
- data/lib/ar-extensions/union.rb +204 -0
- data/lib/ar-extensions/util/sql_generation.rb +27 -0
- data/lib/ar-extensions/util/support_methods.rb +32 -0
- data/lib/ar-extensions/version.rb +9 -0
- data/lib/ar-extensions.rb +5 -0
- metadata +110 -0
@@ -0,0 +1,348 @@
|
|
1
|
+
module ActiveRecord::Extensions::ConnectionAdapters ; end
|
2
|
+
|
3
|
+
module ActiveRecord::Extensions::Import #:nodoc:
|
4
|
+
|
5
|
+
module ImportSupport #:nodoc:
|
6
|
+
def supports_import? #:nodoc:
|
7
|
+
true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module OnDuplicateKeyUpdateSupport #:nodoc:
|
12
|
+
def supports_on_duplicate_key_update? #:nodoc:
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class ActiveRecord::Base
|
20
|
+
class << self
|
21
|
+
|
22
|
+
# use tz as set in ActiveRecord::Base
|
23
|
+
tproc = @@default_timezone == :utc ? lambda { Time.now.utc } : lambda { Time.now }
|
24
|
+
AREXT_RAILS_COLUMNS = {
|
25
|
+
:create => { "created_on" => tproc ,
|
26
|
+
"created_at" => tproc },
|
27
|
+
:update => { "updated_on" => tproc ,
|
28
|
+
"updated_at" => tproc }
|
29
|
+
}
|
30
|
+
AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
|
31
|
+
|
32
|
+
# Returns true if the current database connection adapter
|
33
|
+
# supports import functionality, otherwise returns false.
|
34
|
+
def supports_import?
|
35
|
+
connection.supports_import?
|
36
|
+
rescue NoMethodError
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if the current database connection adapter
|
41
|
+
# supports on duplicate key update functionality, otherwise
|
42
|
+
# returns false.
|
43
|
+
def supports_on_duplicate_key_update?
|
44
|
+
connection.supports_on_duplicate_key_update?
|
45
|
+
rescue NoMethodError
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
# Imports a collection of values to the database.
|
50
|
+
#
|
51
|
+
# This is more efficient than using ActiveRecord::Base#create or
|
52
|
+
# ActiveRecord::Base#save multiple times. This method works well if
|
53
|
+
# you want to create more than one record at a time and do not care
|
54
|
+
# about having ActiveRecord objects returned for each record
|
55
|
+
# inserted.
|
56
|
+
#
|
57
|
+
# This can be used with or without validations. It does not utilize
|
58
|
+
# the ActiveRecord::Callbacks during creation/modification while
|
59
|
+
# performing the import.
|
60
|
+
#
|
61
|
+
# == Usage
|
62
|
+
# Model.import array_of_models
|
63
|
+
# Model.import column_names, array_of_values
|
64
|
+
# Model.import column_names, array_of_values, options
|
65
|
+
#
|
66
|
+
# ==== Model.import array_of_models
|
67
|
+
#
|
68
|
+
# With this form you can call _import_ passing in an array of model
|
69
|
+
# objects that you want updated.
|
70
|
+
#
|
71
|
+
# ==== Model.import column_names, array_of_values
|
72
|
+
#
|
73
|
+
# The first parameter +column_names+ is an array of symbols or
|
74
|
+
# strings which specify the columns that you want to update.
|
75
|
+
#
|
76
|
+
# The second parameter, +array_of_values+, is an array of
|
77
|
+
# arrays. Each subarray is a single set of values for a new
|
78
|
+
# record. The order of values in each subarray should match up to
|
79
|
+
# the order of the +column_names+.
|
80
|
+
#
|
81
|
+
# ==== Model.import column_names, array_of_values, options
|
82
|
+
#
|
83
|
+
# The first two parameters are the same as the above form. The third
|
84
|
+
# parameter, +options+, is a hash. This is optional. Please see
|
85
|
+
# below for what +options+ are available.
|
86
|
+
#
|
87
|
+
# == Options
|
88
|
+
# * +validate+ - true|false, tells import whether or not to use \
|
89
|
+
# ActiveRecord validations. Validations are enforced by default.
|
90
|
+
# * +on_duplicate_key_update+ - an Array or Hash, tells import to \
|
91
|
+
# use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
|
92
|
+
# Key Update below.
|
93
|
+
# * +synchronize+ - an array of ActiveRecord instances for the model
|
94
|
+
# that you are currently importing data into. This synchronizes
|
95
|
+
# existing model instances in memory with updates from the import.
|
96
|
+
# * +timestamps+ - true|false, tells import to not add timestamps \
|
97
|
+
# (if false) even if record timestamps is disabled in ActiveRecord::Base
|
98
|
+
#
|
99
|
+
# == Examples
|
100
|
+
# class BlogPost < ActiveRecord::Base ; end
|
101
|
+
#
|
102
|
+
# # Example using array of model objects
|
103
|
+
# posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
|
104
|
+
# BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
|
105
|
+
# BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
|
106
|
+
# BlogPost.import posts
|
107
|
+
#
|
108
|
+
# # Example using column_names and array_of_values
|
109
|
+
# columns = [ :author_name, :title ]
|
110
|
+
# values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
|
111
|
+
# BlogPost.import columns, values
|
112
|
+
#
|
113
|
+
# # Example using column_names, array_of_value and options
|
114
|
+
# columns = [ :author_name, :title ]
|
115
|
+
# values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
|
116
|
+
# BlogPost.import( columns, values, :validate => false )
|
117
|
+
#
|
118
|
+
# # Example synchronizing existing instances in memory
|
119
|
+
# post = BlogPost.find_by_author_name( 'zdennis' )
|
120
|
+
# puts post.author_name # => 'zdennis'
|
121
|
+
# columns = [ :author_name, :title ]
|
122
|
+
# values = [ [ 'yoda', 'test post' ] ]
|
123
|
+
# BlogPost.import posts, :synchronize=>[ post ]
|
124
|
+
# puts post.author_name # => 'yoda'
|
125
|
+
#
|
126
|
+
# == On Duplicate Key Update (MySQL only)
|
127
|
+
#
|
128
|
+
# The :on_duplicate_key_update option can be either an Array or a Hash.
|
129
|
+
#
|
130
|
+
# ==== Using an Array
|
131
|
+
#
|
132
|
+
# The :on_duplicate_key_update option can be an array of column
|
133
|
+
# names. The column names are the only fields that are updated if
|
134
|
+
# a duplicate record is found. Below is an example:
|
135
|
+
#
|
136
|
+
# BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
|
137
|
+
#
|
138
|
+
# ==== Using A Hash
|
139
|
+
#
|
140
|
+
# The :on_duplicate_key_update option can be a hash of column name
|
141
|
+
# to model attribute name mappings. This gives you finer grained
|
142
|
+
# control over what fields are updated with what attributes on your
|
143
|
+
# model. Below is an example:
|
144
|
+
#
|
145
|
+
# BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
|
146
|
+
#
|
147
|
+
# = Returns
|
148
|
+
# This returns an object which responds to +failed_instances+ and +num_inserts+.
|
149
|
+
# * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
|
150
|
+
# * num_inserts - the number of insert statements it took to import the data
|
151
|
+
def import( *args )
|
152
|
+
@logger = Logger.new(STDOUT)
|
153
|
+
@logger.level = Logger::DEBUG
|
154
|
+
options = { :validate=>true, :timestamps=>true }
|
155
|
+
options.merge!( args.pop ) if args.last.is_a? Hash
|
156
|
+
|
157
|
+
# assume array of model objects
|
158
|
+
if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
|
159
|
+
if args.length == 2
|
160
|
+
models = args.last
|
161
|
+
column_names = args.first
|
162
|
+
else
|
163
|
+
models = args.first
|
164
|
+
column_names = self.column_names.dup
|
165
|
+
end
|
166
|
+
|
167
|
+
array_of_attributes = []
|
168
|
+
models.each do |model|
|
169
|
+
# this next line breaks sqlite.so with a segmentation fault
|
170
|
+
# if model.new_record? || options[:on_duplicate_key_update]
|
171
|
+
attributes = []
|
172
|
+
column_names.each do |name|
|
173
|
+
attributes << model.send( "#{name}_before_type_cast" )
|
174
|
+
end
|
175
|
+
array_of_attributes << attributes
|
176
|
+
# end
|
177
|
+
end
|
178
|
+
# supports 2-element array and array
|
179
|
+
elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
|
180
|
+
column_names, array_of_attributes = args
|
181
|
+
else
|
182
|
+
raise ArgumentError.new( "Invalid arguments!" )
|
183
|
+
end
|
184
|
+
|
185
|
+
# Force the primary key col into the insert if it's not
|
186
|
+
# on the list and we are using a sequence and stuff a nil
|
187
|
+
# value for it into each row so the sequencer will fire later
|
188
|
+
if !column_names.include?(primary_key) && sequence_name && connection.prefetch_primary_key?
|
189
|
+
column_names << primary_key
|
190
|
+
array_of_attributes.each { |a| a << nil }
|
191
|
+
end
|
192
|
+
|
193
|
+
is_validating = options.delete( :validate )
|
194
|
+
|
195
|
+
# dup the passed in array so we don't modify it unintentionally
|
196
|
+
array_of_attributes = array_of_attributes.dup
|
197
|
+
|
198
|
+
# record timestamps unless disabled in ActiveRecord::Base
|
199
|
+
if record_timestamps && options.delete( :timestamps )
|
200
|
+
add_special_rails_stamps column_names, array_of_attributes, options
|
201
|
+
end
|
202
|
+
|
203
|
+
return_obj = if is_validating
|
204
|
+
import_with_validations( column_names, array_of_attributes, options )
|
205
|
+
else
|
206
|
+
num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
207
|
+
OpenStruct.new :failed_instances=>[], :num_inserts=>num_inserts
|
208
|
+
end
|
209
|
+
if options[:synchronize]
|
210
|
+
synchronize( options[:synchronize] )
|
211
|
+
end
|
212
|
+
|
213
|
+
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
214
|
+
return_obj
|
215
|
+
end
|
216
|
+
|
217
|
+
# TODO import_from_table needs to be implemented.
|
218
|
+
def import_from_table( options ) # :nodoc:
|
219
|
+
end
|
220
|
+
|
221
|
+
# Imports the passed in +column_names+ and +array_of_attributes+
|
222
|
+
# given the passed in +options+ Hash with validations. Returns an
|
223
|
+
# object with the methods +failed_instances+ and +num_inserts+.
|
224
|
+
# +failed_instances+ is an array of instances that failed validations.
|
225
|
+
# +num_inserts+ is the number of inserts it took to import the data. See
|
226
|
+
# ActiveRecord::Base.import for more information on
|
227
|
+
# +column_names+, +array_of_attributes+ and +options+.
|
228
|
+
def import_with_validations( column_names, array_of_attributes, options={} )
|
229
|
+
failed_instances = []
|
230
|
+
|
231
|
+
# create instances for each of our column/value sets
|
232
|
+
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
233
|
+
|
234
|
+
# keep track of the instance and the position it is currently at. if this fails
|
235
|
+
# validation we'll use the index to remove it from the array_of_attributes
|
236
|
+
arr.each_with_index do |hsh,i|
|
237
|
+
instance = new( hsh )
|
238
|
+
if not instance.valid?
|
239
|
+
array_of_attributes[ i ] = nil
|
240
|
+
failed_instances << instance
|
241
|
+
end
|
242
|
+
end
|
243
|
+
array_of_attributes.compact!
|
244
|
+
|
245
|
+
num_inserts = array_of_attributes.empty? ? 0 : import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
246
|
+
OpenStruct.new :failed_instances=>failed_instances, :num_inserts => num_inserts
|
247
|
+
end
|
248
|
+
|
249
|
+
# Imports the passed in +column_names+ and +array_of_attributes+
|
250
|
+
# given the passed in +options+ Hash. This will return the number
|
251
|
+
# of insert operations it took to create these records without
|
252
|
+
# validations or callbacks. See ActiveRecord::Base.import for more
|
253
|
+
# information on +column_names+, +array_of_attributes_ and
|
254
|
+
# +options+.
|
255
|
+
def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
|
256
|
+
escaped_column_names = quote_column_names( column_names )
|
257
|
+
columns = []
|
258
|
+
array_of_attributes.first.each_with_index { |arr,i| columns << columns_hash[ column_names[i] ] }
|
259
|
+
|
260
|
+
if not supports_import?
|
261
|
+
columns_sql = "(" + escaped_column_names.join( ',' ) + ")"
|
262
|
+
insert_statements, values = [], []
|
263
|
+
number_inserted = 0
|
264
|
+
array_of_attributes.each do |arr|
|
265
|
+
my_values = []
|
266
|
+
arr.each_with_index do |val,j|
|
267
|
+
if !sequence_name.blank? && column_names[j] == primary_key && val.nil?
|
268
|
+
my_values << connection.next_value_for_sequence(sequence_name)
|
269
|
+
else
|
270
|
+
my_values << connection.quote( val, columns[j] )
|
271
|
+
end
|
272
|
+
end
|
273
|
+
insert_statements << "INSERT INTO #{quoted_table_name} #{columns_sql} VALUES(" + my_values.join( ',' ) + ")"
|
274
|
+
connection.execute( insert_statements.last )
|
275
|
+
number_inserted += 1
|
276
|
+
end
|
277
|
+
else
|
278
|
+
# generate the sql
|
279
|
+
insert_sql = connection.multiple_value_sets_insert_sql( quoted_table_name, escaped_column_names, options )
|
280
|
+
values_sql = connection.values_sql_for_column_names_and_attributes( columns, array_of_attributes )
|
281
|
+
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
282
|
+
|
283
|
+
# perform the inserts
|
284
|
+
number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
|
285
|
+
values_sql,
|
286
|
+
"#{self.class.name} Create Many Without Validations Or Callbacks" )
|
287
|
+
end
|
288
|
+
|
289
|
+
number_inserted
|
290
|
+
end
|
291
|
+
|
292
|
+
# Returns an array of quoted column names
|
293
|
+
def quote_column_names( names )
|
294
|
+
names.map{ |name| connection.quote_column_name( name ) }
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
|
301
|
+
def add_special_rails_stamps( column_names, array_of_attributes, options )
|
302
|
+
AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
|
303
|
+
if self.column_names.include?(key)
|
304
|
+
value = blk.call
|
305
|
+
if index=column_names.index(key)
|
306
|
+
# replace every instance of the array of attributes with our value
|
307
|
+
array_of_attributes.each{ |arr| arr[index] = value }
|
308
|
+
else
|
309
|
+
column_names << key
|
310
|
+
array_of_attributes.each { |arr| arr << value }
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
|
316
|
+
if self.column_names.include?(key)
|
317
|
+
value = blk.call
|
318
|
+
if index=column_names.index(key)
|
319
|
+
# replace every instance of the array of attributes with our value
|
320
|
+
array_of_attributes.each{ |arr| arr[index] = value }
|
321
|
+
else
|
322
|
+
column_names << key
|
323
|
+
array_of_attributes.each { |arr| arr << value }
|
324
|
+
end
|
325
|
+
|
326
|
+
if options[:on_duplicate_key_update]
|
327
|
+
options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array)
|
328
|
+
options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
|
329
|
+
else
|
330
|
+
options[:on_duplicate_key_update] = [ key.to_sym ]
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
|
337
|
+
def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
|
338
|
+
arr = []
|
339
|
+
array_of_attributes.each do |attributes|
|
340
|
+
c = 0
|
341
|
+
hsh = attributes.inject( {} ){|hsh,attr| hsh[ column_names[c] ] = attr ; c+=1 ; hsh }
|
342
|
+
arr << hsh
|
343
|
+
end
|
344
|
+
arr
|
345
|
+
end
|
346
|
+
|
347
|
+
end
|
348
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
#insert select functionality is dependent on finder options and import
|
2
|
+
require 'ar-extensions/finder_options/mysql'
|
3
|
+
require 'ar-extensions/import/mysql'
|
4
|
+
|
5
|
+
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
|
6
|
+
include ActiveRecord::Extensions::InsertSelectSupport
|
7
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# Insert records in bulk with a select statement
|
2
|
+
#
|
3
|
+
# == Parameters
|
4
|
+
# * +options+ - the options used for the finder sql (select)
|
5
|
+
#
|
6
|
+
# === Options
|
7
|
+
# Any valid finder options (options for <tt>ActiveRecord::Base.find(:all)</tt> )such as <tt>:joins</tt>, <tt>:conditions</tt>, <tt>:include</tt>, etc including:
|
8
|
+
# * <tt>:from</tt> - the symbol, class name or class used for the finder SQL (select)
|
9
|
+
# * <tt>:on_duplicate_key_update</tt> - an array of fields to update, or a custom string
|
10
|
+
# * <tt>:select</tt> - An array of fields to select or custom string. The SQL will be sanitized and ? replaced with values as with <tt>:conditions</tt>.
|
11
|
+
# * <tt>:ignore => true </tt> - will ignore any duplicates
|
12
|
+
# * <tt>:into</tt> - Specifies the columns for which data will be inserted. An array of fields to select or custom string.
|
13
|
+
#
|
14
|
+
# == Examples
|
15
|
+
# Create cart items for all books for shopping cart <tt>@cart+
|
16
|
+
# setting the +copies+ field to 1, the +updated_at+ field to Time.now and the +created_at+ field to the database function now()
|
17
|
+
# CartItem.insert_select(:from => :book,
|
18
|
+
# :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
|
19
|
+
# :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at]})
|
20
|
+
#
|
21
|
+
# GENERATED SQL example (MySQL):
|
22
|
+
# INSERT INTO `cart_items` ( `book_id`, `shopping_cart_id`, `copies`, `updated_at`, `created_at` )
|
23
|
+
# SELECT books.id, '134', 1, '2009-03-02 18:28:25', now() FROM `books`
|
24
|
+
#
|
25
|
+
# A similar example that
|
26
|
+
# * uses the class +Book+ instead of symbol <tt>:book</tt>
|
27
|
+
# * a custom string (instead of an Array) for the <tt>:select</tt> of the +insert_options+
|
28
|
+
# * Updates the +updated_at+ field of all existing cart item. This assumes there is a unique composite index on the +book_id+ and +shopping_cart_id+ fields
|
29
|
+
#
|
30
|
+
# CartItem.insert_select(:from => Book,
|
31
|
+
# :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
|
32
|
+
# :into => 'cart_items.book_id, shopping_cart_id, copies, updated_at, created_at',
|
33
|
+
# :on_duplicate_key_update => [:updated_at])
|
34
|
+
# GENERATED SQL example (MySQL):
|
35
|
+
# INSERT INTO `cart_items` ( cart_items.book_id, shopping_cart_id, copies, updated_at, created_at )
|
36
|
+
# SELECT books.id, '138', 1, '2009-03-02 18:32:34', now() FROM `books`
|
37
|
+
# ON DUPLICATE KEY UPDATE `cart_items`.`updated_at`=VALUES(`updated_at`)
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# Similar example ignoring duplicates
|
41
|
+
# CartItem.insert_select(:from => :book,
|
42
|
+
# :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
|
43
|
+
# :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at],
|
44
|
+
# :ignore => true)
|
45
|
+
#
|
46
|
+
# == Developers
|
47
|
+
# * Blythe Dunham http://blythedunham.com
|
48
|
+
#
|
49
|
+
# == Homepage
|
50
|
+
# * Project Site: http://www.continuousthinking.com/tags/arext
|
51
|
+
# * Rubyforge Project: http://rubyforge.org/projects/arext
|
52
|
+
# * Anonymous SVN: svn checkout svn://rubyforge.org/var/svn/arext
|
53
|
+
#
|
54
|
+
|
55
|
+
module ActiveRecord::Extensions::ConnectionAdapters; end
|
56
|
+
|
57
|
+
module ActiveRecord::Extensions::InsertSelectSupport #:nodoc:
|
58
|
+
def supports_insert_select? #:nodoc:
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ActiveRecord::Base
|
64
|
+
|
65
|
+
include ActiveRecord::Extensions::SqlGeneration
|
66
|
+
|
67
|
+
class << self
|
68
|
+
# Insert records in bulk with a select statement
|
69
|
+
#
|
70
|
+
# == Parameters
|
71
|
+
# * +options+ - the options used for the finder sql (select)
|
72
|
+
#
|
73
|
+
# === Options
|
74
|
+
# Any valid finder options (options for <tt>ActiveRecord::Base.find(:all)</tt> )such as <tt>:joins</tt>, <tt>:conditions</tt>, <tt>:include</tt>, etc including:
|
75
|
+
# * <tt>:from</tt> - the symbol, class name or class used for the finder SQL (select)
|
76
|
+
# * <tt>:on_duplicate_key_update</tt> - an array of fields to update, or a custom string
|
77
|
+
# * <tt>:select</tt> - An array of fields to select or custom string. The SQL will be sanitized and ? replaced with values as with <tt>:conditions</tt>.
|
78
|
+
# * <tt>:ignore => true </tt> - will ignore any duplicates
|
79
|
+
# * <tt>:into</tt> - Specifies the columns for which data will be inserted. An array of fields to select or custom string.
|
80
|
+
#
|
81
|
+
# == Examples
|
82
|
+
# Create cart items for all books for shopping cart <tt>@cart+
|
83
|
+
# setting the +copies+ field to 1, the +updated_at+ field to Time.now and the +created_at+ field to the database function now()
|
84
|
+
# CartItem.insert_select(:from => :book,
|
85
|
+
# :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
|
86
|
+
# :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at]})
|
87
|
+
#
|
88
|
+
# GENERATED SQL example (MySQL):
|
89
|
+
# INSERT INTO `cart_items` ( `book_id`, `shopping_cart_id`, `copies`, `updated_at`, `created_at` )
|
90
|
+
# SELECT books.id, '134', 1, '2009-03-02 18:28:25', now() FROM `books`
|
91
|
+
#
|
92
|
+
# A similar example that
|
93
|
+
# * uses the class +Book+ instead of symbol <tt>:book</tt>
|
94
|
+
# * a custom string (instead of an Array) for the <tt>:select</tt> of the +insert_options+
|
95
|
+
# * Updates the +updated_at+ field of all existing cart item. This assumes there is a unique composite index on the +book_id+ and +shopping_cart_id+ fields
|
96
|
+
#
|
97
|
+
# CartItem.insert_select(:from => Book,
|
98
|
+
# :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
|
99
|
+
# :into => 'cart_items.book_id, shopping_cart_id, copies, updated_at, created_at',
|
100
|
+
# :on_duplicate_key_update => [:updated_at])
|
101
|
+
# GENERATED SQL example (MySQL):
|
102
|
+
# INSERT INTO `cart_items` ( cart_items.book_id, shopping_cart_id, copies, updated_at, created_at )
|
103
|
+
# SELECT books.id, '138', 1, '2009-03-02 18:32:34', now() FROM `books`
|
104
|
+
# ON DUPLICATE KEY UPDATE `cart_items`.`updated_at`=VALUES(`updated_at`)
|
105
|
+
#
|
106
|
+
#
|
107
|
+
# Similar example ignoring duplicates
|
108
|
+
# CartItem.insert_select(:from => :book,
|
109
|
+
# :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
|
110
|
+
# :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at],
|
111
|
+
# :ignore => true)
|
112
|
+
def insert_select(options={})
|
113
|
+
select_obj = options.delete(:from).to_s.classify.constantize
|
114
|
+
#TODO: add batch support for high volume inserts
|
115
|
+
#return insert_select_batch(select_obj, select_options, insert_options) if insert_options[:batch]
|
116
|
+
sql = construct_insert_select_sql(select_obj, options)
|
117
|
+
connection.insert(sql, "#{name} Insert Select #{select_obj}")
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
def construct_insert_select_sql(select_obj, options)#:nodoc:
|
123
|
+
construct_ar_extension_sql(gather_insert_options(options), valid_insert_select_options) do |sql, into_op|
|
124
|
+
sql << " INTO #{quoted_table_name} "
|
125
|
+
sql << "( #{into_column_sql(options.delete(:into))} ) "
|
126
|
+
|
127
|
+
#sanitize the select sql based on the select object
|
128
|
+
sql << select_obj.send(:finder_sql_to_string, sanitize_select_options(options))
|
129
|
+
sql
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# return a list of the column names quoted accordingly
|
134
|
+
# nil => All columns except primary key (auto update)
|
135
|
+
# String => Exact String
|
136
|
+
# Array
|
137
|
+
# needs sanitation ["?, ?", 5, 'test'] => "5, 'test'" or [":date", {:date => Date.today}] => "12-30-2006"]
|
138
|
+
# list of strings or symbols returns quoted values [:start, :name] => `start`, `name` or ['abc'] => `start`
|
139
|
+
def select_column_sql(field_list=nil)#:nodoc:
|
140
|
+
if field_list.kind_of?(String)
|
141
|
+
field_list.dup
|
142
|
+
elsif ((field_list.kind_of?(Array) && field_list.first.is_a?(String)) &&
|
143
|
+
(field_list.last.is_a?(Hash) || field_list.first.include?('?')))
|
144
|
+
sanitize_sql(field_list)
|
145
|
+
else
|
146
|
+
field_list = field_list.blank? ? self.column_names - [self.primary_key] : [field_list].flatten
|
147
|
+
field_list.collect{|field| self.connection.quote_column_name(field.to_s) }.join(", ")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
alias_method :into_column_sql, :select_column_sql
|
152
|
+
|
153
|
+
#sanitize the select options for insert select
|
154
|
+
def sanitize_select_options(options)#:nodoc:
|
155
|
+
o = options.dup
|
156
|
+
select = o.delete :select
|
157
|
+
o[:override_select] = select ? select_column_sql(select) : ' * '
|
158
|
+
o
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
def valid_insert_select_options#:nodoc:
|
163
|
+
@@valid_insert_select_options ||= [:command, :into_pre, :into_post,
|
164
|
+
:into_keywords, :ignore,
|
165
|
+
:on_duplicate_key_update]
|
166
|
+
end
|
167
|
+
|
168
|
+
#move all the insert options to a seperate map
|
169
|
+
def gather_insert_options(options)#:nodoc:
|
170
|
+
into_options = valid_insert_select_options.inject(:command => 'INSERT') do |map, o|
|
171
|
+
v = options.delete(o)
|
172
|
+
map[o] = v if v
|
173
|
+
map
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,30 @@
|
|
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
|
6
|
+
# on an individual ActiveRecord instance but it is intended for use on
|
7
|
+
# multiple instances.
|
8
|
+
#
|
9
|
+
# This uses one query for all instance updates and then updates existing
|
10
|
+
# instances rather sending one query for each instance
|
11
|
+
def self.synchronize(instances, key=self.primary_key)
|
12
|
+
return if instances.empty?
|
13
|
+
|
14
|
+
keys = instances.map(&"#{key}".to_sym)
|
15
|
+
klass = instances.first.class
|
16
|
+
fresh_instances = klass.find( :all, :conditions=>{ key=>keys }, :order=>"#{key} ASC" )
|
17
|
+
|
18
|
+
instances.each_with_index do |instance, index|
|
19
|
+
instance.clear_aggregation_cache
|
20
|
+
instance.clear_association_cache
|
21
|
+
instance.instance_variable_set '@attributes', fresh_instances[index].attributes
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# See ActiveRecord::ConnectionAdapters::AbstractAdapter.synchronize
|
26
|
+
def synchronize(instances, key=ActiveRecord::Base.primary_key)
|
27
|
+
self.class.synchronize(instances, key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|