arperftoolkit_base 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +109 -0
- data/Rakefile +71 -0
- data/init.rb +1 -0
- data/lib/ar_perf_toolkit.rb +8 -0
- data/lib/base.rb +138 -0
- data/lib/insert.rb +170 -0
- data/lib/insert_bulk.rb +131 -0
- metadata +61 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006 Dr.-Ing. Stefan Kaes
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
AR Perf Toolkit Base
|
2
|
+
ActiveRecord enhancements to optimize queries and insertion functionality.
|
3
|
+
Includes support for finder index hints, ignore and duplicate update,
|
4
|
+
insert select, record duplicate deletion, bulk insert, and eager loading of
|
5
|
+
selected columns.
|
6
|
+
|
7
|
+
This has only been tested with MySQL.
|
8
|
+
|
9
|
+
|
10
|
+
#########################################################################################
|
11
|
+
Single Active Record Operations
|
12
|
+
#########################################################################################
|
13
|
+
The following methods are overwritten to accept a hash map parameter of options
|
14
|
+
|
15
|
+
* save
|
16
|
+
* create
|
17
|
+
* create!
|
18
|
+
* save!
|
19
|
+
* create_or_update
|
20
|
+
|
21
|
+
Options:
|
22
|
+
|
23
|
+
* duplicate - On duplicate key update update old rows on duplicate
|
24
|
+
Example: updates fields birthdate and email_address_id if a duplicate exists when the contact is created or updated
|
25
|
+
|
26
|
+
contact.save :duplicate => [:birthdate, :email_address_id ]
|
27
|
+
Set the existing duplicate email address id to 5
|
28
|
+
|
29
|
+
|
30
|
+
contact.save :duplicate => 'email_address_id = 5'
|
31
|
+
|
32
|
+
* keyword(ignore) adds a keyword after mysql command (example INSERT IGNORE)
|
33
|
+
Example. Ignore a duplicate record.
|
34
|
+
contact.save :keywords => :ignore
|
35
|
+
|
36
|
+
command. The sql command to use. Example: uses replace insead of insert
|
37
|
+
contact.save :command => :replace
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
#########################################################################################
|
42
|
+
Bulk Insert Operations
|
43
|
+
#########################################################################################
|
44
|
+
* insert_all
|
45
|
+
* insert_select
|
46
|
+
|
47
|
+
insert_all(objs, options={})
|
48
|
+
*Note* use instead of former upsert.
|
49
|
+
|
50
|
+
Insert a group of records in one transaction.
|
51
|
+
|
52
|
+
objs can be:
|
53
|
+
|
54
|
+
* Array of hash maps {:column_name => :value} where column_names are specified in the options[:select]
|
55
|
+
|
56
|
+
EmailAddress.insert_all(
|
57
|
+
[{:text => 'blythe@spongecell.com', :name =>'Blythe Dunham'},
|
58
|
+
{:text => 'blythedunham@gmail.com', :name =>'Blythe Dunham'}],
|
59
|
+
{:select => [:text, :name] })
|
60
|
+
|
61
|
+
* Array of active records (slower)
|
62
|
+
|
63
|
+
upsert_taggings << Tagging.new(:tag_id=>tag_id, :taggable_type=>child.to_s, :taggable_id=>record.id)
|
64
|
+
|
65
|
+
Tagging.insert_all(upsert_taggings, :keywords => :ignore)
|
66
|
+
|
67
|
+
Options
|
68
|
+
|
69
|
+
* skip_validation does not run valid? on each object
|
70
|
+
* duplicate
|
71
|
+
* keywords
|
72
|
+
* post_sql - sql appended to the end (indexes, rollup, etc)
|
73
|
+
* pre_sql - sql appended to the beginning (ex, priorities)
|
74
|
+
* command - choose the columns you wish to insert
|
75
|
+
|
76
|
+
insert_select(select_obj, select_options, insert_options={})
|
77
|
+
Insert records with a select statement
|
78
|
+
add all the books written by the author into the cart
|
79
|
+
Cart.insert_select Book, {:select => ['books.id, ?', Time.now], :conditions => ['books.author_id = ?', 1 }, {:select => [:book_id, :added-on]}
|
80
|
+
generated sql
|
81
|
+
INSERT INTO carts (`book_id`, `added_on`) SELECT books.id, '2007-05-01 01:24:28' FROM books where books.author_id = 1
|
82
|
+
|
83
|
+
|
84
|
+
select_options
|
85
|
+
|
86
|
+
* select => select porition of query. like conditions (santizes sql)
|
87
|
+
|
88
|
+
insert_options - See options of insert_all
|
89
|
+
|
90
|
+
#########################################################################################
|
91
|
+
Finder Operations
|
92
|
+
#########################################################################################
|
93
|
+
* finder_sql_to_string - prints out the query to string
|
94
|
+
* find - enhanced to accept additional params for sql customization
|
95
|
+
|
96
|
+
finder_sql_to_string
|
97
|
+
Print out the sql that would be executed. Same signature as ActiveRecord::Base.find
|
98
|
+
|
99
|
+
>> Contact.finder_sql_to_string :piggy => :primary_email_address
|
100
|
+
|
101
|
+
=> "SELECT contacts.*, email_addresses.crypted_text AS primary_email_address_crypted_text FROM contacts LEFT JOIN email_addresses ON email_addresses.id=contacts.email_address_id "
|
102
|
+
|
103
|
+
find
|
104
|
+
|
105
|
+
Adds these options:
|
106
|
+
|
107
|
+
* post_sql - sql appended to the end (indexes, rollup, etc)
|
108
|
+
* pre_sql - sql appended to the beginning (ex, priorities)
|
109
|
+
* keywords
|
data/Rakefile
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/sshpublisher'
|
5
|
+
|
6
|
+
PKG_NAME = "arperftoolkit_base"
|
7
|
+
PKG_VERSION = "0.0.1"
|
8
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
9
|
+
RUBY_FORGE_PROJECT = "eload_select"
|
10
|
+
|
11
|
+
spec = Gem::Specification.new do |s|
|
12
|
+
s.name = PKG_NAME
|
13
|
+
s.version = PKG_VERSION
|
14
|
+
s.platform = Gem::Platform::RUBY
|
15
|
+
s.summary = "ActiveRecord enhancements to optimize queries and insertion functionality. Includes support for finder index hints, ignore and duplicate update, insert select, record duplicate deletion, bulk insert, and eager loading of selected columns."
|
16
|
+
s.files = FileList["{lib,tasks}/**/*"].to_a + %w(init.rb LICENSE Rakefile README)
|
17
|
+
s.require_path = "lib"
|
18
|
+
s.autorequire = PKG_NAME
|
19
|
+
s.has_rdoc = true
|
20
|
+
s.test_files = nil
|
21
|
+
s.add_dependency "rails", ">= 1.2.0"
|
22
|
+
|
23
|
+
s.author = "Blythe Dunham"
|
24
|
+
s.email = "blythe@spongecell.com"
|
25
|
+
s.homepage = "http://spongetech.wordpress.com/"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Default: run unit tests.'
|
29
|
+
task :default => :test
|
30
|
+
|
31
|
+
desc 'Test the ar_test plugin.'
|
32
|
+
Rake::TestTask.new(:test) do |t|
|
33
|
+
t.libs << '.\lib'
|
34
|
+
t.pattern = 'test/**/*_test.rb'
|
35
|
+
t.verbose = true
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Generate documentation for the ar_test plugin.'
|
39
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
40
|
+
rdoc.rdoc_dir = 'rdoc'
|
41
|
+
rdoc.title = 'ArP'
|
42
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
43
|
+
rdoc.rdoc_files.include('README')
|
44
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
46
|
+
|
47
|
+
Rake::GemPackageTask.new(spec) do |p|
|
48
|
+
p.gem_spec = spec
|
49
|
+
p.need_tar = true
|
50
|
+
p.need_zip = true
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Publish the API docs and gem"
|
54
|
+
task :publish => [:pdoc, :release]
|
55
|
+
|
56
|
+
desc "Publish the release files to RubyForge."
|
57
|
+
task :release => [:gem, :package] do
|
58
|
+
require 'rubyforge'
|
59
|
+
|
60
|
+
options = {"cookie_jar" => RubyForge::COOKIE_F}
|
61
|
+
options["password"] = ENV["RUBY_FORGE_PASSWORD"] if ENV["RUBY_FORGE_PASSWORD"]
|
62
|
+
ruby_forge = RubyForge.new
|
63
|
+
ruby_forge.login
|
64
|
+
|
65
|
+
%w( gem tgz zip ).each do |ext|
|
66
|
+
file = "pkg/#{PKG_FILE_NAME}.#{ext}"
|
67
|
+
puts "Releasing #{File.basename(file)}..."
|
68
|
+
|
69
|
+
ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
|
70
|
+
end
|
71
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ar_perf_toolkit'
|
@@ -0,0 +1,8 @@
|
|
1
|
+
|
2
|
+
require File.dirname(__FILE__) + '/base'
|
3
|
+
require File.dirname(__FILE__) + '/insert'
|
4
|
+
require File.dirname(__FILE__) + '/insert_bulk'
|
5
|
+
|
6
|
+
ActiveRecord::Base.send(:include, ArPerfToolkit::Base)
|
7
|
+
ActiveRecord::Base.send(:include, ArPerfToolkit::Insert)
|
8
|
+
ActiveRecord::Base.send(:include, ArPerfToolkit::InsertBulk)
|
data/lib/base.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
|
2
|
+
# MONKEY MONKEY!!!! Move me to a better place and make me an optional param
|
3
|
+
#
|
4
|
+
|
5
|
+
module ArPerfToolkit
|
6
|
+
module Base
|
7
|
+
def self.included(base) #:nodoc:
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.class_eval do
|
10
|
+
class << self
|
11
|
+
VALID_FIND_OPTIONS.concat([:pre_sql, :post_sql, :keywords])
|
12
|
+
alias_method :construct_finder_sql, :construct_finder_sql_x
|
13
|
+
alias_method_chain :construct_finder_sql_with_included_associations, :perf_options unless method_defined?(:construct_finder_sql_with_included_associations_with_options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
# expose the finder sql to a string with the same options that
|
21
|
+
# the find method takes.
|
22
|
+
#
|
23
|
+
# sql = Contact.finder_sql_to_string(:include => :primary_email_address)
|
24
|
+
# Contact.find_by_sql(sql + 'USE_INDEX(blah)')
|
25
|
+
def finder_sql_to_string(options)
|
26
|
+
select_sql = self.send(((scoped?(:find, :include) || options[:include]) ?
|
27
|
+
:finder_sql_with_included_associations :
|
28
|
+
:construct_finder_sql), options)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
#Construct keywords around sql
|
33
|
+
def construct_sql(options={}, valid_options = [], &block)
|
34
|
+
options.assert_valid_keys(:keywords, :command, :duplicate, :post_sql, :pre_sql, :select, *valid_options)
|
35
|
+
sql = ''
|
36
|
+
sql << (options[:pre_sql] + ' ') if options[:pre_sql]
|
37
|
+
sql << "#{options[:command]} "
|
38
|
+
add_keywords!(sql, options[:keywords]) if options[:keywords]
|
39
|
+
yield sql, options
|
40
|
+
add_duplicate!(sql, options[:duplicate]) if options[:duplicate]
|
41
|
+
sql << options[:post_sql] if options[:post_sql]
|
42
|
+
sql
|
43
|
+
end
|
44
|
+
|
45
|
+
def construct_finder_sql_x(options)
|
46
|
+
#add piggy back option if plugin is installed
|
47
|
+
add_piggy_back!(options) if self.respond_to? :add_piggy_back!
|
48
|
+
scope = scope(:find)
|
49
|
+
sql = "#{options[:pre_sql]} SELECT".strip
|
50
|
+
sql << " #{options[:keywords]}" if options[:keywords]
|
51
|
+
sql << " #{(scope && scope[:select]) || options[:select] || '*'} "
|
52
|
+
sql << "FROM #{(scope && scope[:from]) || options[:from] || table_name} "
|
53
|
+
|
54
|
+
add_joins!(sql, options, scope)
|
55
|
+
add_conditions!(sql, options[:conditions], scope)
|
56
|
+
|
57
|
+
sql << " GROUP BY #{options[:group]} " if options[:group]
|
58
|
+
sql << " ORDER BY #{options[:order]} " if options[:order]
|
59
|
+
|
60
|
+
add_limit!(sql, options, scope)
|
61
|
+
sql << "#{options[:post_sql]}" if options[:post_sql]
|
62
|
+
sql
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
#add a util method here until I find a better home for it.
|
69
|
+
#delete all the dups
|
70
|
+
#
|
71
|
+
#ex.
|
72
|
+
# Make all the phone numbers of contacts unique by deleting the
|
73
|
+
# >>Contacts.delete_duplicates(:fields=>['phone_number_id'])
|
74
|
+
#
|
75
|
+
# :fields -> the fields to match on
|
76
|
+
# :conditions-> specify this to do it yourself.
|
77
|
+
# :additional_clause -> append something to the query, tho you could just use scope
|
78
|
+
# :winner_clause-> the part of the query specifying what wins. Default is that with the greatest id.
|
79
|
+
# :query_field -> if you dont like id as the winner, specify another field
|
80
|
+
def delete_duplicates options={}
|
81
|
+
|
82
|
+
query = options[:conditions] || "delete from " + table_name+" c1 using "+table_name+" c1, " +table_name+" c2 where (" +
|
83
|
+
options[:fields].collect{|field| ('c1.'+field.to_s+'=c2.'+field.to_s) }.join(" and ") +
|
84
|
+
(options[:additional_clause] ? 'and ('+options[:additional_clause]+') ' : '')+') and '+
|
85
|
+
(options[:winner_clause] || ('c1.'+(qf=(options[:query_field] || 'id').to_s) + '> c2.'+qf))
|
86
|
+
|
87
|
+
self.connection.execute(self.sanitize_sql(query))
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
#add keywords like IGNORE or DELAYED
|
92
|
+
def add_keywords!(sql, keywords)
|
93
|
+
sql << (keywords.kind_of?(Array) ? keywords.collect{|kw| " #{kw} "}.join('') : " #{keywords} ")
|
94
|
+
end
|
95
|
+
|
96
|
+
#duplicate can be expressed as a string: "field_name = 5"
|
97
|
+
#or an array of field names to update with new value
|
98
|
+
#[last_name, first_name] would update the dupe with the new last and first name
|
99
|
+
def add_duplicate!(sql, duplicate)
|
100
|
+
if duplicate
|
101
|
+
duplicate = [duplicate] if (duplicate.kind_of?(String) || duplicate.kind_of?(Symbol)) && !duplicate.to_s.include?('=')
|
102
|
+
sql << " ON DUPLICATE KEY UPDATE "
|
103
|
+
sql << (duplicate.kind_of?(Array) ? duplicate.collect{|val|
|
104
|
+
val = self.connection.quote_column_name val.to_s
|
105
|
+
"#{val}=VALUES(#{val})"
|
106
|
+
}.join(', ') : duplicate.to_s)
|
107
|
+
end
|
108
|
+
sql
|
109
|
+
end
|
110
|
+
|
111
|
+
#generate the finder sql for use with associations (:include => :something)
|
112
|
+
def finder_sql_with_included_associations(options = {})
|
113
|
+
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
|
114
|
+
sql = construct_finder_sql_with_included_associations(options, join_dependency)
|
115
|
+
end
|
116
|
+
|
117
|
+
def construct_finder_sql_with_included_associations_x options, join_dependency
|
118
|
+
construct_finder_sql_block(options) do
|
119
|
+
construct_finder_sql_with_included_associations(options, join_dependency)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def construct_finder_sql_with_included_associations_with_perf_options(options, join_dependency)
|
124
|
+
#use eager loading select if available
|
125
|
+
method = respond_to?(:construct_finder_sql_with_included_associations_with_eager_select) ? :construct_finder_sql_with_included_associations_with_eager_select : :construct_finder_sql_with_included_associations_without_perf_options
|
126
|
+
sql = send(method, options, join_dependency)
|
127
|
+
|
128
|
+
#substitute pre_sql and keywords
|
129
|
+
sql.gsub!(/^\s*SELECT/, "#{options[:pre_sql]} SELECT #{options[:keywords]}" ) if options[:pre_sql] || options[:keywords]
|
130
|
+
|
131
|
+
#substitue post sql
|
132
|
+
sql << " #{options[:post_sql]}" if options[:post_sql]
|
133
|
+
sql
|
134
|
+
end
|
135
|
+
|
136
|
+
end#ClassMethods
|
137
|
+
end
|
138
|
+
end
|
data/lib/insert.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
|
2
|
+
class Module
|
3
|
+
#support deprecated method_x commands
|
4
|
+
#alias method to method_without_arperf_options
|
5
|
+
#alias method_x to method
|
6
|
+
def alias_method_chain_arperf(methods)
|
7
|
+
methods = Array.new(methods)
|
8
|
+
methods.each do |target|
|
9
|
+
unless method_defined?("#{target}_without_arperf_options")
|
10
|
+
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
|
11
|
+
alias_method "#{aliased_target}_without_arperf_options#{punctuation}", target
|
12
|
+
alias_method target, "#{aliased_target}_x#{punctuation}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ArPerfToolkit
|
19
|
+
module Insert
|
20
|
+
def self.included(base) #:nodoc:
|
21
|
+
base.extend(ClassMethods)
|
22
|
+
|
23
|
+
#alias methods
|
24
|
+
base.class_eval do
|
25
|
+
alias_method_chain_arperf(%w(save update save! create_or_update))
|
26
|
+
class << self
|
27
|
+
alias_method_chain_arperf(%w(create create!))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
def create_ignore attributes = nil
|
34
|
+
create_x!(attributes, :keywords => 'IGNORE')
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
|
38
|
+
# fails under validations, the unsaved object is still returned.
|
39
|
+
def create_x(attributes = nil, options={})
|
40
|
+
if attributes.is_a?(Array)
|
41
|
+
attributes.collect { |attr| create(attr) }
|
42
|
+
else
|
43
|
+
object = new(attributes)
|
44
|
+
scope(:create).each { |att,value| object.send("#{att}=", value) } if scoped?(:create)
|
45
|
+
object.save_x options
|
46
|
+
object
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Creates an object just like Base.create but calls save! instead of save
|
51
|
+
# so an exception is raised if the record is invalid.
|
52
|
+
def create_x!(attributes = nil,options={})
|
53
|
+
if attributes.is_a?(Array)
|
54
|
+
attributes.collect { |attr| create!(attr) }
|
55
|
+
else
|
56
|
+
attributes.reverse_merge!(scope(:create)) if scoped?(:create)
|
57
|
+
|
58
|
+
object = new(attributes)
|
59
|
+
object.save_x!(options)
|
60
|
+
object
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end#ClassMethods
|
65
|
+
|
66
|
+
def save_ignore
|
67
|
+
save_x :keywords => 'IGNORE'
|
68
|
+
end
|
69
|
+
|
70
|
+
def save_ignore!
|
71
|
+
save_ignore || raise(ActiveRecord::RecordNotSaved)
|
72
|
+
end
|
73
|
+
|
74
|
+
def save_x(options={})
|
75
|
+
#invoke save_with_validation if the argument is not a hash
|
76
|
+
return save_without_arperf_options(options) unless options.is_a?(Hash)
|
77
|
+
|
78
|
+
perform_validation = options.delete(:perform_validation)
|
79
|
+
if (perform_validation.is_a?(FalseClass)) || valid?
|
80
|
+
raise ActiveRecord::ReadOnlyRecord if readonly?
|
81
|
+
create_or_update_x options
|
82
|
+
else
|
83
|
+
raise ActiveRecord::RecordInvalid.new(self) if perform_validation == :exception
|
84
|
+
false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def save_x!(options={})
|
89
|
+
options[:perform_validation] = :exception
|
90
|
+
save_x(options) || raise(ActiveRecord::RecordNotSaved)
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_or_update_x options={}
|
94
|
+
return false if callback(:before_save) == false
|
95
|
+
options[:keywords] = 'IGNORE' if options.delete(:ignore).kind_of?(TrueClass)
|
96
|
+
result =
|
97
|
+
if new_record? then
|
98
|
+
create_x(options)
|
99
|
+
else
|
100
|
+
options.delete(:duplicate)
|
101
|
+
update_x(options)
|
102
|
+
end
|
103
|
+
callback(:after_save)
|
104
|
+
result
|
105
|
+
end
|
106
|
+
|
107
|
+
#END _X STUFF. Remove this if we end up overwriting update and create directly
|
108
|
+
|
109
|
+
# Updates the associated record with values matching those of the instance attributes.
|
110
|
+
def update_x options={}
|
111
|
+
return false if callback(:before_update) == false
|
112
|
+
insert_with_timestamps(false)
|
113
|
+
options[:command]||='UPDATE'
|
114
|
+
raise(ArgumentError, "Unknown key: duplicate") if options[:duplicate]
|
115
|
+
sql = self.class.construct_sql(options) do |sql, options|
|
116
|
+
sql << "#{self.class.table_name} "
|
117
|
+
sql << "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
|
118
|
+
"WHERE (#{self.class.primary_key} = #{quote_value(id)}) "
|
119
|
+
end
|
120
|
+
connection.update(sql, "#{self.class.name} UpdateX")
|
121
|
+
callback(:after_update)
|
122
|
+
return true
|
123
|
+
end
|
124
|
+
|
125
|
+
# Creates a new record with values matching those of the instance attributes.
|
126
|
+
def create_x options={}
|
127
|
+
return false if callback(:before_create) == false
|
128
|
+
insert_with_timestamps(true)
|
129
|
+
|
130
|
+
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
|
131
|
+
self.id = connection.next_sequence_value(self.class.sequence_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
options[:command]||='INSERT'
|
135
|
+
sql = self.class.construct_sql(options) do |sql, options|
|
136
|
+
sql << "INTO #{self.class.table_name} (#{quoted_column_names.join(', ')}) "
|
137
|
+
sql << "VALUES(#{attributes_with_quotes.values.join(', ')})"
|
138
|
+
end
|
139
|
+
|
140
|
+
self.id = connection.insert(sql, "#{self.class.name} CreateX ",
|
141
|
+
self.class.primary_key, self.id, self.class.sequence_name)
|
142
|
+
|
143
|
+
@new_record = false
|
144
|
+
|
145
|
+
callback(:after_create)
|
146
|
+
return self.id != 0
|
147
|
+
end
|
148
|
+
|
149
|
+
#Replace deletes the existing duplicate if it exists then adds
|
150
|
+
#this
|
151
|
+
def replace_x options={}
|
152
|
+
create_x(options.merge(:command => 'REPLACE'))
|
153
|
+
end
|
154
|
+
|
155
|
+
protected
|
156
|
+
|
157
|
+
#update timestamps
|
158
|
+
def insert_with_timestamps(bCreate=true)
|
159
|
+
if record_timestamps
|
160
|
+
t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
|
161
|
+
write_attribute('created_at', t) if bCreate && respond_to?(:created_at) && created_at.nil?
|
162
|
+
write_attribute('created_on', t) if bCreate && respond_to?(:created_on) && created_on.nil?
|
163
|
+
|
164
|
+
write_attribute('updated_at', t) if respond_to?(:updated_at)
|
165
|
+
write_attribute('updated_on', t) if respond_to?(:updated_on)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
data/lib/insert_bulk.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
|
2
|
+
module ArPerfToolkit
|
3
|
+
module InsertBulk
|
4
|
+
def self.included(base) #:nodoc:
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
|
11
|
+
#Insert records with a select statement
|
12
|
+
# add all the books written by the author into the cart
|
13
|
+
#Cart.insert_select Book, {:select => ['books.id, ?', Time.now], :conditions => ['books.author_id = ?', 1 }, {:select => [:book_id, :added-on]}
|
14
|
+
# generated sql
|
15
|
+
#INSERT INTO carts (`book_id`, `added_on`) SELECT books.id, '2007-05-01 01:24:28' FROM books where books.author_id = 1
|
16
|
+
def insert_select(select_obj, select_options, insert_options={})
|
17
|
+
|
18
|
+
#santize and clean up the select columns. Cannot do * here because we need the columns in
|
19
|
+
#a particular order
|
20
|
+
select_options[:select] = select_obj.send :select_columns, select_options[:select]
|
21
|
+
|
22
|
+
#build the sql
|
23
|
+
sql = construct_sql({:command => 'INSERT'}.merge(insert_options)) do |sql, options|
|
24
|
+
sql << " INTO #{table_name} "
|
25
|
+
sql << "(%s) " % select_columns(insert_options[:select])
|
26
|
+
|
27
|
+
sql << select_obj.send(:finder_sql_to_string, select_options)
|
28
|
+
|
29
|
+
end
|
30
|
+
connection.insert(sql, "#{name} Insert Select #{select_obj}")
|
31
|
+
end
|
32
|
+
|
33
|
+
#the raw sql for bulk insert sql
|
34
|
+
def insert_all_sql(objs, options={})
|
35
|
+
value_sql = options.delete(:value_sql)
|
36
|
+
sql = construct_sql({:command => 'INSERT'}.merge(options), [:skip_validation]) do |sql, options|
|
37
|
+
sql << " INTO #{table_name} "
|
38
|
+
sql << "(%s) " % select_columns(options[:select])
|
39
|
+
sql << "VALUES "
|
40
|
+
sql << (value_sql || insert_value_pairs(objs, options[:select], options[:skip_validation].kind_of?(TrueClass)))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#Insert a group of records
|
45
|
+
#insert_all, Insert a whole bunch of records in one transaction
|
46
|
+
# EmailAddress.insert_all([{:text => 'blythe@spongecell.com', :name =>'Blythe Dunham'},{:text => 'blythedunham@gmail.com', :name =>'Blythe Dunham'}], {:select => [:text, :name] })
|
47
|
+
# options
|
48
|
+
# <tt>:skip_validation</tt> => does not run valid? on each object
|
49
|
+
# <tt>:duplicate</tt>
|
50
|
+
# <tt>:keywords</tt>
|
51
|
+
# <tt>:post_sql</tt>
|
52
|
+
# <tt>:pre_sql</tt>
|
53
|
+
# <tt>:command</tt>
|
54
|
+
# <tt>:select => </tt>choose the columns you wish to insert
|
55
|
+
|
56
|
+
def insert_all(objs, options={})
|
57
|
+
connection.insert(insert_all_sql(objs, options), "#{name} Insert All")
|
58
|
+
end
|
59
|
+
|
60
|
+
def update_all_x(updates, conditions = nil, options={})
|
61
|
+
raise(ArgumentError, "Unknown key: duplicate") if options[:duplicate]
|
62
|
+
|
63
|
+
sql = construct_sql({:command => 'UPDATE'}.merge(options)) do |sql, options|
|
64
|
+
sql << "#{table_name} SET #{sanitize_sql(updates)} "
|
65
|
+
add_conditions!(sql, conditions, scope(:find))
|
66
|
+
end
|
67
|
+
|
68
|
+
connection.update(sql, "#{name} Update #{options.inspect}")
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_all_ignore updates, conditions = nil, options={}
|
72
|
+
update_all_x updates, conditions, options.update(:keywords => 'IGNORE')
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
#return a list of the column names quoted accordingly
|
77
|
+
# nil => All columns except primary key (auto update)
|
78
|
+
# String => Exact String
|
79
|
+
# Array
|
80
|
+
# needs sanitation ["?, ?", 5, 'test'] => "5, 'test'" or [":date", {:date => Date.today}] => "12-30-2006"]
|
81
|
+
# list of strings or symbols returns quoted values [:start, :name] => `start`, `name` or ['abc'] => `start`
|
82
|
+
def select_columns(field_list=nil)
|
83
|
+
if field_list.kind_of?(String)
|
84
|
+
field_list
|
85
|
+
elsif field_list.kind_of?(Array) && field_list.first.is_a?(String) && (field_list.last.is_a?(Hash) || field_list.first.include?('?'))
|
86
|
+
sanitize_sql(field_list)
|
87
|
+
else
|
88
|
+
field_list = field_list.blank? ? self.column_names - [self.primary_key] : [field_list].flatten
|
89
|
+
field_list.collect{|field| self.connection.quote_column_name(field.to_s) }.join(", ")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def quoted_list(list)
|
94
|
+
list.collect{|item| connection.quote(item)}.join(', ')
|
95
|
+
end
|
96
|
+
|
97
|
+
#return a comma delimited list of value pairs
|
98
|
+
# ex. (`p1_val_attrib_1`, `p1_val_attrib_2 ...), (`p2_val_attrib_1`, `p2_val_attrib_2 ...),
|
99
|
+
#objs can be either a hash map or an ActiveRecord derived object o442r an array of a hash map or ActiveRecord
|
100
|
+
def insert_value_pairs(objs, field_list, skip_validation=false)
|
101
|
+
raise Exception.new("No values to insert") if objs.blank?
|
102
|
+
|
103
|
+
if objs.is_a?(Array) && objs.first.is_a?(Array) && !field_list.blank?
|
104
|
+
objs.collect{|obj| "(#{quoted_list(obj)})" }
|
105
|
+
else
|
106
|
+
[objs].flatten.inject([]){|list, obj|
|
107
|
+
obj= self.new(obj) if (obj.kind_of?(Hash))
|
108
|
+
raise ActiveRecord::RecordInvalid.new(obj) if !skip_validation && !obj.valid?
|
109
|
+
list << "(%s)" % obj.send(:attributes_with_quotes_x, field_list)
|
110
|
+
}
|
111
|
+
end.join(", ")
|
112
|
+
end
|
113
|
+
|
114
|
+
end#ClassMethods
|
115
|
+
protected
|
116
|
+
|
117
|
+
#get all the attributes with quotes delimited by commas minus the ones in the included_attributes list
|
118
|
+
def attributes_with_quotes_x(included_attributes = nil)
|
119
|
+
insert_with_timestamps
|
120
|
+
|
121
|
+
included_attributes = self.class.column_names - [self.class.primary_key] if included_attributes.blank?
|
122
|
+
|
123
|
+
included_attributes.collect{ |column_name|
|
124
|
+
#don't call attributes() here, it is slow and this method gets called a lot
|
125
|
+
quote_value(self.send(:read_attribute,column_name), column_for_attribute(column_name))
|
126
|
+
}.join(", ")
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: arperftoolkit_base
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2007-08-15 00:00:00 -03:00
|
8
|
+
summary: ActiveRecord enhancements to optimize queries and insertion functionality. Includes support for finder index hints, ignore and duplicate update, insert select, record duplicate deletion, bulk insert, and eager loading of selected columns.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: blythe@spongecell.com
|
12
|
+
homepage: http://spongetech.wordpress.com/
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire: arperftoolkit_base
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Blythe Dunham
|
31
|
+
files:
|
32
|
+
- lib/ar_perf_toolkit.rb
|
33
|
+
- lib/base.rb
|
34
|
+
- lib/insert.rb
|
35
|
+
- lib/insert_bulk.rb
|
36
|
+
- init.rb
|
37
|
+
- LICENSE
|
38
|
+
- Rakefile
|
39
|
+
- README
|
40
|
+
test_files: []
|
41
|
+
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
executables: []
|
47
|
+
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
dependencies:
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rails
|
55
|
+
version_requirement:
|
56
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.2.0
|
61
|
+
version:
|