activerecord_bulkoperation 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/README.md +1 -0
- data/Rakefile +25 -0
- data/activerecord_bulkoperation.gemspec +23 -0
- data/gemfiles/4.2.gemfile +14 -0
- data/lib/activerecord_bulkoperation.rb +20 -0
- data/lib/activerecord_bulkoperation/active_record/adapters/abstract_adapter.rb +49 -0
- data/lib/activerecord_bulkoperation/active_record/adapters/oracle_enhanced_adapter.rb +10 -0
- data/lib/activerecord_bulkoperation/active_record/associations/associations.rb +169 -0
- data/lib/activerecord_bulkoperation/adapters/abstract_adapter.rb +43 -0
- data/lib/activerecord_bulkoperation/adapters/oracle_enhanced_adapter.rb +44 -0
- data/lib/activerecord_bulkoperation/base.rb +53 -0
- data/lib/activerecord_bulkoperation/bulkoperation.rb +260 -0
- data/lib/activerecord_bulkoperation/connection_adapters/oracle_enhanced/jdbc_connection.rb +111 -0
- data/lib/activerecord_bulkoperation/connection_adapters/oracle_enhanced/oci_connection.rb +106 -0
- data/lib/activerecord_bulkoperation/group_operations.rb +296 -0
- data/lib/activerecord_bulkoperation/group_operations_select.rb +60 -0
- data/lib/activerecord_bulkoperation/util/connection_object.rb +22 -0
- data/lib/activerecord_bulkoperation/util/entity_hash.rb +78 -0
- data/lib/activerecord_bulkoperation/util/flush_dirty_objects.rb +126 -0
- data/lib/activerecord_bulkoperation/util/sequence_cache.rb +52 -0
- data/lib/activerecord_bulkoperation/util/transaction_object.rb +36 -0
- data/lib/activerecord_bulkoperation/version.rb +5 -0
- data/test/active_record_connection_test.rb +41 -0
- data/test/adapters/oracle_enhanced.rb +1 -0
- data/test/bulkoperation_test.rb +176 -0
- data/test/database.yml +8 -0
- data/test/entity_hash_test.rb +11 -0
- data/test/find_group_by_test.rb +132 -0
- data/test/flush_dirty_objects_test.rb +11 -0
- data/test/models/assembly.rb +3 -0
- data/test/models/course.rb +3 -0
- data/test/models/group.rb +3 -0
- data/test/models/item.rb +2 -0
- data/test/models/part.rb +3 -0
- data/test/models/product.rb +7 -0
- data/test/models/student.rb +3 -0
- data/test/models/test_table.rb +2 -0
- data/test/postgresql/bulk_test.rb +13 -0
- data/test/schema/generic_schema.rb +59 -0
- data/test/sequence_cache_test.rb +31 -0
- data/test/support/postgresql/bulk_examples.rb +8 -0
- data/test/test_helper.rb +45 -0
- data/test/transaction_object_test.rb +11 -0
- metadata +141 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class NoPersistentRecord < ActiveRecordError
|
3
|
+
end
|
4
|
+
|
5
|
+
class NoOrginalRecordFound < ActiveRecordError
|
6
|
+
end
|
7
|
+
|
8
|
+
class ExternalDataChange < ActiveRecordError
|
9
|
+
end
|
10
|
+
|
11
|
+
class Base
|
12
|
+
extend ActiveRecord::Bulkoperation::BatchUpdate::InstanceMethods
|
13
|
+
class << self
|
14
|
+
def flush_scheduled_operations(args = {})
|
15
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush_record_class(self, args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_id_column?
|
19
|
+
@has_id_column ||= columns_hash.key?('id')
|
20
|
+
end
|
21
|
+
|
22
|
+
def sequence_exists?(name)
|
23
|
+
arr = connection.find_by_sequence_name_sql_array(name)
|
24
|
+
sql = sanitize_sql(arr)
|
25
|
+
find_by_sql(sql).count == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def check_sequence
|
30
|
+
if sequence_exists?("#{table_name}_seq")
|
31
|
+
"#{table_name}_seq"
|
32
|
+
else
|
33
|
+
sequence_name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def next_sequence_value
|
38
|
+
@sequence_cache ||= ActiveRecord::Bulkoperation::Util::SequenceCache.new(check_sequence)
|
39
|
+
@sequence_cache.next_value
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_foreign_master_tables(table_name)
|
43
|
+
arr = connection.find_foreign_master_tables_sql_array(table_name)
|
44
|
+
sql = sanitize_sql(arr)
|
45
|
+
Array(find_by_sql(sql)).map { |e|e.table_name }
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_foreign_detail_tables(table_name)
|
49
|
+
arr = connection.find_foreign_detail_tables_sql_array(table_name)
|
50
|
+
sql = sanitize_sql(arr)
|
51
|
+
Array(find_by_sql(sql)).map { |e|e.table_name }
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.find_detail_references(table_name)
|
55
|
+
arr = connection.find_detail_references_sql_array(table_name)
|
56
|
+
sql = sanitize_sql([sql, table_name.upcase])
|
57
|
+
find_by_sql(sql)
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_options_from_args!(args) #:nodoc:
|
61
|
+
args.last.is_a?(Hash) ? args.pop : {}
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_delete_by_primary_key_sql
|
65
|
+
keys = primary_key_columns
|
66
|
+
|
67
|
+
index = 1
|
68
|
+
"DELETE FROM #{table_name} " \
|
69
|
+
'WHERE ' + "#{keys.map { |c| string = "#{c.name} = :#{index}"; index += 1; string }.join(' AND ') } "
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_optimistic_delete_sql
|
73
|
+
index = 1
|
74
|
+
"DELETE FROM #{table_name} " \
|
75
|
+
'WHERE ' + "#{columns.map { |c| string = build_optimistic_where_element(index, c); index += 1; index += 1 if c.null; string }.join(' AND ') } "
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_optimistic_update_sql
|
80
|
+
index = 1
|
81
|
+
"UPDATE #{table_name} " \
|
82
|
+
'SET ' +
|
83
|
+
"#{columns.map { |c| string = c.name + " = :#{index}"; index += 1; string }.join(', ') } " +
|
84
|
+
'WHERE ' +
|
85
|
+
"#{columns.map { |c| string = build_optimistic_where_element(index, c); index += 1; index += 1 if c.null; string }.join(' AND ') } "# +
|
86
|
+
#"AND ROWID = :#{index}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_update_by_primary_key_sql
|
90
|
+
keys = primary_key_columns
|
91
|
+
|
92
|
+
index = 1
|
93
|
+
"UPDATE #{table_name} " \
|
94
|
+
'SET ' +
|
95
|
+
"#{columns.map { |c| string = c.name + " = :#{index}"; index += 1; string }.join(', ') } " +
|
96
|
+
'WHERE ' +
|
97
|
+
"#{keys.map { |c| string = "#{c.name} = :#{index}"; index += 1; string }.join(' AND ') } "
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def primary_key_columns
|
103
|
+
@primary_key_columns ||= primary_key_as_array.map { |c| columns_hash[c.to_s] }
|
104
|
+
end
|
105
|
+
|
106
|
+
def primary_key_as_array
|
107
|
+
primary_key.is_a?(Array) ? primary_key : [primary_key]
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_optimistic_where_element(index, column)
|
111
|
+
sql = '( '
|
112
|
+
# AK: remove timezone information. ruby appends the summer/winter timezone releated to the date and send this as timestamp to oracle
|
113
|
+
# and a timestamp with timezone is not equals the date in the row
|
114
|
+
if column.type == :datetime
|
115
|
+
sql += "#{column.name} = cast( :#{index} as date ) "
|
116
|
+
else
|
117
|
+
sql += "#{column.name} = :#{index} "
|
118
|
+
end
|
119
|
+
|
120
|
+
index += 1
|
121
|
+
if column.null
|
122
|
+
sql += "OR ( #{column.name} IS NULL AND :#{index} IS NULL ) "
|
123
|
+
index += 1
|
124
|
+
end
|
125
|
+
|
126
|
+
sql += ') '
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
# Checks if a active record object was changed and if schedule if for DB merge
|
132
|
+
#
|
133
|
+
# args - Hash
|
134
|
+
#
|
135
|
+
# return - self
|
136
|
+
def schedule_merge_on_change( args={} )
|
137
|
+
if orginal_selected_record.nil?
|
138
|
+
schedule_merge
|
139
|
+
return self
|
140
|
+
end
|
141
|
+
|
142
|
+
ignore_columns = args[:ignore_columns] || []
|
143
|
+
|
144
|
+
attributes.each_pair do |key, value|
|
145
|
+
next if ignore_columns.include?(key)
|
146
|
+
if value != orginal_selected_record[key]
|
147
|
+
schedule_merge
|
148
|
+
return self
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
self
|
153
|
+
end
|
154
|
+
|
155
|
+
def schedule_merge
|
156
|
+
set_id_from_sequence
|
157
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.add_merge(self)
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
def schedule_insert_on_missing( *unique_columns )
|
162
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.add_insert_on_missing( unique_columns, self )
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def insert_on_missing( *unique_columns )
|
168
|
+
unique_columns = Array( self.class.primary_key ) if unique_columns.nil? || unique_columns.empty?
|
169
|
+
set_id_from_sequence
|
170
|
+
self.class.insert_on_missing_group( unique_columns, [self] ) > 0
|
171
|
+
end
|
172
|
+
|
173
|
+
def schedule_delete
|
174
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.add_delete(self)
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
# if id column is nil a value will be fetched from the sequence and set to the id property
|
179
|
+
def set_id_from_sequence
|
180
|
+
if self.class.has_id_column? && @attributes.fetch_value( 'id' ).nil?
|
181
|
+
new_id = self.class.next_sequence_value
|
182
|
+
if self.class.primary_key == 'id'
|
183
|
+
self.id = new_id
|
184
|
+
else
|
185
|
+
id_will_change! # <- neu
|
186
|
+
@attributes.write_from_user( 'id', new_id )
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
def optimistic_update
|
193
|
+
fail NoPersistentRecord.new if @new_record
|
194
|
+
fail NoOrginalRecordFound.new unless orginal_selected_record
|
195
|
+
|
196
|
+
sql = self.class.build_optimistic_update_sql
|
197
|
+
|
198
|
+
types = []
|
199
|
+
|
200
|
+
for c in self.class.columns
|
201
|
+
type = self.class.to_type_symbol(c)
|
202
|
+
types << type
|
203
|
+
end
|
204
|
+
|
205
|
+
for c in self.class.columns
|
206
|
+
type = self.class.to_type_symbol(c)
|
207
|
+
types << type
|
208
|
+
types << type if c.null
|
209
|
+
end
|
210
|
+
|
211
|
+
binds = self.class.columns.map { |c| read_attribute(c.name) }
|
212
|
+
|
213
|
+
get_optimistic_where_binds.each { |v| binds << v }
|
214
|
+
|
215
|
+
self.class.execute_batch_update(sql, types, [binds])
|
216
|
+
end
|
217
|
+
|
218
|
+
def optimistic_delete
|
219
|
+
fail NoPersistentRecord.new if @new_record
|
220
|
+
fail NoOrginalRecordFound.new unless orginal_selected_record
|
221
|
+
|
222
|
+
sql = self.class.build_optimistic_delete_sql
|
223
|
+
|
224
|
+
binds = get_optimistic_where_binds
|
225
|
+
|
226
|
+
types = []
|
227
|
+
|
228
|
+
for c in self.class.columns
|
229
|
+
type = self.class.to_type_symbol(c)
|
230
|
+
types << type
|
231
|
+
types << type if c.null
|
232
|
+
end
|
233
|
+
|
234
|
+
self.class.execute_batch_update(sql, types, [binds])
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def get_optimistic_where_binds
|
240
|
+
binds = []
|
241
|
+
self.class.columns.map do |c|
|
242
|
+
v = orginal_selected_record[ c.name]
|
243
|
+
binds << v
|
244
|
+
binds << v if c.null
|
245
|
+
end
|
246
|
+
|
247
|
+
binds
|
248
|
+
end
|
249
|
+
|
250
|
+
def rowid=(id)
|
251
|
+
self['rowid'] = id
|
252
|
+
end
|
253
|
+
alias_method :row_id=, :rowid=
|
254
|
+
|
255
|
+
def rowid
|
256
|
+
self['rowid']
|
257
|
+
end
|
258
|
+
alias_method :row_id, :rowid
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'java'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Bulkoperation
|
5
|
+
module BatchUpdate
|
6
|
+
module InstanceMethods
|
7
|
+
|
8
|
+
def enhanced_write_lobs #:nodoc:
|
9
|
+
# disable this feature for jruby
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute_batch_update(sql, types, values, optimistic = true)
|
13
|
+
fail ArgumentError.new('String expected') unless sql.is_a? String
|
14
|
+
|
15
|
+
types = [types] unless types.is_a? Array
|
16
|
+
fail ArgumentError.new('Array of symbols expected') unless types.select { |i| not i.is_a? Symbol }.empty?
|
17
|
+
|
18
|
+
fail ArgumentError.new('Array expected') unless values.is_a? Array
|
19
|
+
values = [values] unless values.select { |i| not i.is_a? Array }.empty?
|
20
|
+
|
21
|
+
unless values.select { |i| not i.count == types.count }.empty?
|
22
|
+
fail ArgumentError.new('types.count must be equal to arr.count for every arr in values')
|
23
|
+
end
|
24
|
+
|
25
|
+
unless connection.adapter_name == 'Oracle' || connection.adapter_name == 'OracleEnhanced'
|
26
|
+
fail "Operation is provided only on Oracle connections. (adapter_name is #{connection.adapter_name})"
|
27
|
+
end
|
28
|
+
|
29
|
+
stmt = connection.raw_connection.prepareStatement(sql)
|
30
|
+
|
31
|
+
count = 0
|
32
|
+
|
33
|
+
begin
|
34
|
+
stmt.setExecuteBatch(300) # TODO
|
35
|
+
|
36
|
+
for row in values
|
37
|
+
i = 1
|
38
|
+
( 0..row.count - 1).each{ |j|
|
39
|
+
bind(stmt, i, types[j], row[j])
|
40
|
+
i += 1
|
41
|
+
}
|
42
|
+
|
43
|
+
count += stmt.executeUpdate
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
count += stmt.sendBatch
|
48
|
+
|
49
|
+
fail ExternalDataChange.new(sql) if count != values.count and optimistic
|
50
|
+
|
51
|
+
rescue Exception => e
|
52
|
+
exc = e
|
53
|
+
while exc.kind_of?(Java.java.lang.Exception) and exc.cause
|
54
|
+
exc = exc.cause
|
55
|
+
end
|
56
|
+
raise exc
|
57
|
+
ensure
|
58
|
+
stmt.close
|
59
|
+
end
|
60
|
+
|
61
|
+
count
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def bind(stmt, index, type, value)
|
66
|
+
if type == :string
|
67
|
+
stmt.setString(index, value) if value
|
68
|
+
stmt.setNull(index, Java.java.sql.Types::VARCHAR) unless value
|
69
|
+
|
70
|
+
elsif type == :integer
|
71
|
+
stmt.setLong(index, value) if value
|
72
|
+
stmt.setNull(index, Java.java.sql.Types::NUMERIC) unless value
|
73
|
+
|
74
|
+
elsif type == :float
|
75
|
+
stmt.setDouble(index, value) if value
|
76
|
+
stmt.setNull(index, Java.java.sql.Types::NUMERIC) unless value
|
77
|
+
|
78
|
+
elsif type == :date
|
79
|
+
|
80
|
+
if value
|
81
|
+
# TODO Timezone handling; if the given value have another timezone as default
|
82
|
+
c = Java.java.util.Calendar.getInstance
|
83
|
+
c.set(Java.java.util.Calendar::YEAR, value.year)
|
84
|
+
c.set(Java.java.util.Calendar::MONTH, value.month - 1)
|
85
|
+
c.set(Java.java.util.Calendar::DATE, value.day)
|
86
|
+
if value.kind_of?(DateTime) or value.kind_of?(Time)
|
87
|
+
c.set(Java.java.util.Calendar::HOUR_OF_DAY, value.hour)
|
88
|
+
c.set(Java.java.util.Calendar::MINUTE, value.min)
|
89
|
+
c.set(Java.java.util.Calendar::SECOND, value.sec)
|
90
|
+
else
|
91
|
+
c.set(Java.java.util.Calendar::HOUR_OF_DAY, 0)
|
92
|
+
c.set(Java.java.util.Calendar::MINUTE, 0)
|
93
|
+
c.set(Java.java.util.Calendar::SECOND, 0)
|
94
|
+
end
|
95
|
+
c.set(Java.java.util.Calendar::MILLISECOND, 0)
|
96
|
+
|
97
|
+
stmt.setTimestamp(index, Java.java.sql.Timestamp.new(c.getTimeInMillis))
|
98
|
+
|
99
|
+
else
|
100
|
+
stmt.setNull(index, Java.java.sql.Types::TIMESTAMP)
|
101
|
+
end
|
102
|
+
|
103
|
+
else
|
104
|
+
|
105
|
+
fail ArgumentError.new("unsupported type #{type}")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Bulkoperation
|
3
|
+
module BatchUpdate
|
4
|
+
module InstanceMethods
|
5
|
+
|
6
|
+
def execute_batch_update(sql, types, values, optimistic = true)
|
7
|
+
fail ArgumentError.new('String expected') unless sql.is_a? String
|
8
|
+
|
9
|
+
types = [types] unless types.is_a? Array
|
10
|
+
fail ArgumentError.new('Array of Symbol expected') unless types.select { |i| not i.is_a? Symbol }.empty?
|
11
|
+
|
12
|
+
fail ArgumentError.new('Array expected') unless values.is_a? Array
|
13
|
+
values = [values] unless values.select { |i| not i.is_a? Array }.empty?
|
14
|
+
|
15
|
+
unless values.select { |i| not i.count == types.count }.empty?
|
16
|
+
fail ArgumentError.new('types.count must be equal to arr.count for every arr in values')
|
17
|
+
end
|
18
|
+
|
19
|
+
unless connection.adapter_name == 'Oracle' || connection.adapter_name == 'OracleEnhanced'
|
20
|
+
fail "Operation is provided only on Oracle connections. (adapter_name is #{connection.adapter_name})"
|
21
|
+
end
|
22
|
+
|
23
|
+
oci_conn = connection.raw_connection if connected?
|
24
|
+
fail 'Unable to access the raw OCI connection.' unless oci_conn
|
25
|
+
cursor = oci_conn.parse(sql)
|
26
|
+
fail "Unable to obtain cursor for this statement:\n#{sql}." unless cursor
|
27
|
+
|
28
|
+
affected_rows = 0
|
29
|
+
|
30
|
+
begin
|
31
|
+
values.each do |row|
|
32
|
+
|
33
|
+
(0..row.count - 1).each do |i|
|
34
|
+
bind(cursor, i + 1, types[i], row[i])
|
35
|
+
end
|
36
|
+
|
37
|
+
result = cursor.exec
|
38
|
+
#result = 1
|
39
|
+
if result != 1 and optimistic
|
40
|
+
|
41
|
+
msg = sql
|
42
|
+
i = 1
|
43
|
+
row.each do |v|
|
44
|
+
val = get_value(v)
|
45
|
+
msg = msg.gsub(":#{i},", connection.quote(val) + ',')
|
46
|
+
msg = msg.gsub(":#{i} ", connection.quote(val) + ' ')
|
47
|
+
i += 1
|
48
|
+
end
|
49
|
+
i -= 1
|
50
|
+
msg = msg.gsub(":#{i}", connection.quote(get_value(row.last)))
|
51
|
+
|
52
|
+
fail ExternalDataChange.new("The record you want to update was updated by another process.\n#{msg}")
|
53
|
+
end
|
54
|
+
|
55
|
+
affected_rows += result
|
56
|
+
|
57
|
+
connection.send(:log, sql, 'Update') {}
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
cursor.close
|
61
|
+
end
|
62
|
+
|
63
|
+
affected_rows
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def get_value(value)
|
69
|
+
if(value.respond_to? :value)
|
70
|
+
value.value
|
71
|
+
else
|
72
|
+
value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def bind(cursor, index, type, input_value)
|
77
|
+
value = get_value(input_value)
|
78
|
+
if type == :string
|
79
|
+
cursor.bind_param(":#{index}", value, String)
|
80
|
+
|
81
|
+
elsif type == :integer
|
82
|
+
cursor.bind_param(":#{index}", value, Fixnum)
|
83
|
+
|
84
|
+
elsif type == :float
|
85
|
+
cursor.bind_param(":#{index}", value, Float)
|
86
|
+
|
87
|
+
elsif type == :date
|
88
|
+
if value.nil?
|
89
|
+
cursor.bind_param(":#{index}", nil, Date)
|
90
|
+
else
|
91
|
+
if ( value.kind_of?(DateTime) or value.kind_of?(Time)) and value.hour == 0 and value.min == 0 and value.sec == 0
|
92
|
+
cursor.bind_param(":#{index}", value, Date)
|
93
|
+
else
|
94
|
+
cursor.bind_param(":#{index}", value, DateTime)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
else
|
99
|
+
|
100
|
+
fail ArgumentError.new("unsupported type #{type}")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|