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,126 @@
|
|
1
|
+
# It's used to store the modified/deleted entity objects at transaction level.
|
2
|
+
# Before the transaction will be closed the stored enities will be send to the database.
|
3
|
+
#
|
4
|
+
# Author:: Andre Kullmann
|
5
|
+
#
|
6
|
+
module ActiveRecord
|
7
|
+
module Bulkoperation
|
8
|
+
module Util
|
9
|
+
class FlushDirtyObjects < Util::TransactionObject
|
10
|
+
class DirtyObjects
|
11
|
+
attr_reader :insert_or_update
|
12
|
+
attr_reader :insert_on_missing
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@insert_or_update = Set.new
|
16
|
+
@insert_on_missing = {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@scheduled_merges = ActiveRecord::Bulkoperation::Util::EntityHash.new
|
22
|
+
@scheduled_deletions = ActiveRecord::Bulkoperation::Util::EntityHash.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def before_commit
|
26
|
+
flush
|
27
|
+
end
|
28
|
+
|
29
|
+
def before_create_savepoint
|
30
|
+
flush
|
31
|
+
end
|
32
|
+
|
33
|
+
def after_rollback_to_savepoint
|
34
|
+
close
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_rollback
|
38
|
+
close
|
39
|
+
end
|
40
|
+
|
41
|
+
def close
|
42
|
+
|
43
|
+
@scheduled_merges.values.each do |objects|
|
44
|
+
if objects
|
45
|
+
objects.insert_or_update.each { |record| record.on_closed_scheduled_operation }
|
46
|
+
objects.insert_on_missing.values.each { |array| array.each{ |record| record.on_closed_scheduled_operation } }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
@scheduled_merges.clear
|
50
|
+
|
51
|
+
@scheduled_deletions.values.each { |array| array and array.each { |record| record.on_closed_scheduled_operation } }
|
52
|
+
@scheduled_deletions.clear
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_insert_on_missing(keys,object)
|
56
|
+
fail 'nil object' unless object
|
57
|
+
( ( @scheduled_merges[ object.class] ||= DirtyObjects.new).insert_on_missing[keys] ||= Set.new ) << object
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_merge(object)
|
61
|
+
fail 'nil object' unless object
|
62
|
+
( @scheduled_merges[ object.class] ||= DirtyObjects.new).insert_or_update << object
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_delete(object)
|
66
|
+
fail 'nil object' unless object
|
67
|
+
( @scheduled_deletions[ object.class] ||= []) << object
|
68
|
+
end
|
69
|
+
|
70
|
+
def flush(args = {})
|
71
|
+
fail 'nil object' unless args
|
72
|
+
|
73
|
+
affected = 0
|
74
|
+
|
75
|
+
@scheduled_deletions.each_pair_in_detail_hierarchy do |k, v|
|
76
|
+
affected += k.delete_group(v, args)
|
77
|
+
@scheduled_deletions[k] = nil
|
78
|
+
v.each { |record| record.on_closed_scheduled_operation }
|
79
|
+
end
|
80
|
+
|
81
|
+
@scheduled_merges.each_pair_in_master_hierarchy do |k, v|
|
82
|
+
|
83
|
+
affected += k.merge_group( v.insert_or_update, args )
|
84
|
+
|
85
|
+
v.insert_on_missing.each_pair do |keys,records|
|
86
|
+
|
87
|
+
affected += k.insert_on_missing_group( keys, records, args )
|
88
|
+
end
|
89
|
+
|
90
|
+
@scheduled_merges[k] = nil
|
91
|
+
v.insert_or_update.each { |record| record.on_closed_scheduled_operation }
|
92
|
+
v.insert_on_missing.values.each { |array| array.each{ |record| record.on_closed_scheduled_operation } }
|
93
|
+
end
|
94
|
+
|
95
|
+
affected
|
96
|
+
end
|
97
|
+
|
98
|
+
def flush_record_class(record_class, args = {})
|
99
|
+
fail 'nil object' unless record_class
|
100
|
+
fail 'nil object' unless args
|
101
|
+
|
102
|
+
if list = @scheduled_deletions[ record_class]
|
103
|
+
record_class.delete_group(list, args)
|
104
|
+
@scheduled_deletions[ record_class] = nil
|
105
|
+
list.each { |record| record.on_closed_scheduled_operation }
|
106
|
+
end
|
107
|
+
|
108
|
+
if objects = @scheduled_merges[ record_class]
|
109
|
+
|
110
|
+
record_class.merge_group( objects.insert_or_update, args )
|
111
|
+
|
112
|
+
objects.insert_on_missing.each_pair do |keys,records|
|
113
|
+
|
114
|
+
record_class.insert_on_missing_group( keys, records, args )
|
115
|
+
end
|
116
|
+
|
117
|
+
@scheduled_merges[record_class] = nil
|
118
|
+
objects.insert_or_update.each { |record| record.on_closed_scheduled_operation }
|
119
|
+
objects.insert_on_missing.values.each { |array| array.each{ |record| record.on_closed_scheduled_operation } }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Will fetch the next N values from a sequence and store it in a queue.
|
2
|
+
# Now the values will can be fetched from the queue without any database roundtrip.
|
3
|
+
# If the queue is empty, the next N values will be fetched from database and stored in the queue.
|
4
|
+
#
|
5
|
+
# It's using the DUAL table so it should only work with oracle.
|
6
|
+
#
|
7
|
+
# Author:: Andre Kullmann
|
8
|
+
#
|
9
|
+
module ActiveRecord
|
10
|
+
module Bulkoperation
|
11
|
+
module Util
|
12
|
+
class SequenceCache
|
13
|
+
|
14
|
+
#
|
15
|
+
# seq - the sequence name to use with the SequenceCache object
|
16
|
+
# refetch - how many values should be fetch from the database with each roundtrip
|
17
|
+
def initialize(seq, prefetch = 10)
|
18
|
+
@seq, @prefetch = seq, prefetch
|
19
|
+
@queue = Queue.new
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# return - the next value from the sequence
|
24
|
+
def next_value
|
25
|
+
while ( value = next_value_from_queue ).nil?
|
26
|
+
fill_queue
|
27
|
+
end
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def next_value_from_queue
|
34
|
+
@queue.pop(true)
|
35
|
+
rescue ThreadError => e
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def fill_queue
|
40
|
+
fetch.each do |f|
|
41
|
+
@queue << f
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch
|
46
|
+
st = ActiveRecord::Base.connection.exec_query("SELECT #{@seq}.nextval id FROM dual connect by level <= :a", "SQL", [[nil,@prefetch]])
|
47
|
+
st.map {|r| r['id']}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# A TransactionObject belongs to a connection. It's like a singleton for each transaction.
|
2
|
+
#
|
3
|
+
# Author:: Andre Kullmann
|
4
|
+
#
|
5
|
+
module ActiveRecord
|
6
|
+
module Bulkoperation
|
7
|
+
module Util
|
8
|
+
class TransactionObject
|
9
|
+
def self.get
|
10
|
+
result = ActiveRecord::Base.connection.connection_listeners.select { |l| l.class == self }.first
|
11
|
+
unless result
|
12
|
+
result = new
|
13
|
+
ActiveRecord::Base.connection.connection_listeners << result
|
14
|
+
end
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
def after_commit
|
19
|
+
close
|
20
|
+
ActiveRecord::Base.connection.connection_listeners.delete(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_rollback
|
24
|
+
close
|
25
|
+
ActiveRecord::Base.connection.connection_listeners.delete(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def after_rollback_to_savepoint
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
|
+
|
3
|
+
class ActiveRecordConnectionTest < ActiveSupport::TestCase
|
4
|
+
def test_method_definition
|
5
|
+
assert ActiveRecord::Base.connection.respond_to? :connection_listeners
|
6
|
+
assert ActiveRecord::Base.connection.respond_to? :commit_db_transaction
|
7
|
+
assert ActiveRecord::Base.connection.respond_to? :rollback_db_transaction
|
8
|
+
assert ActiveRecord::Base.connection.respond_to? :rollback_to_savepoint
|
9
|
+
assert ActiveRecord::Base.connection.respond_to? :create_savepoint
|
10
|
+
|
11
|
+
assert ActiveRecord::Base.connection.respond_to? :commit_db_transaction_without_callback
|
12
|
+
assert ActiveRecord::Base.connection.respond_to? :rollback_db_transaction_without_callback
|
13
|
+
assert ActiveRecord::Base.connection.respond_to? :rollback_to_savepoint_without_callback
|
14
|
+
assert ActiveRecord::Base.connection.respond_to? :create_savepoint_without_callback
|
15
|
+
end
|
16
|
+
|
17
|
+
class MyTestClass
|
18
|
+
def before_commit
|
19
|
+
puts "get calllsslsl"
|
20
|
+
end
|
21
|
+
|
22
|
+
def after_rollback
|
23
|
+
puts "get calllsslsl"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_method_calls_definition
|
28
|
+
test_class = MyTestClass.new
|
29
|
+
test_class.expects(:before_commit)
|
30
|
+
test_class.expects(:after_commit)
|
31
|
+
|
32
|
+
test_class.expects(:after_rollback)
|
33
|
+
|
34
|
+
ActiveRecord::Base.connection.connection_listeners << test_class
|
35
|
+
|
36
|
+
ActiveRecord::Base.connection.rollback_db_transaction
|
37
|
+
|
38
|
+
ActiveRecord::Base.connection.commit_db_transaction
|
39
|
+
ActiveRecord::Base.connection.connection_listeners.clear
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ENV["DB_ADAPTER"] = "oracle_enhanced"
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
|
+
|
3
|
+
class BulkoperationTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def teardown
|
6
|
+
TestTable.delete_all
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_bulk_operation_methods
|
10
|
+
assert ActiveRecord::Base.respond_to? :flush_scheduled_operations
|
11
|
+
test_obj = TestTable.new
|
12
|
+
assert test_obj.respond_to? :schedule_merge
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_bulk_operation_workflow
|
16
|
+
test_obj = TestTable.new
|
17
|
+
test_obj.author_name = 'test-workflow'
|
18
|
+
test_obj.schedule_merge
|
19
|
+
|
20
|
+
TestTable.flush_scheduled_operations
|
21
|
+
record = TestTable.first
|
22
|
+
assert_equal('test-workflow',record.author_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_multi_thread
|
26
|
+
thread_count = 5
|
27
|
+
inner_count = 10
|
28
|
+
threads = []
|
29
|
+
thread_count.times do |t|
|
30
|
+
t = Thread.new do
|
31
|
+
inner_count.times do |i|
|
32
|
+
test_obj = TestTable.new
|
33
|
+
test_obj.author_name = "t1-#{1}"
|
34
|
+
test_obj.schedule_merge
|
35
|
+
end
|
36
|
+
TestTable.flush_scheduled_operations
|
37
|
+
end
|
38
|
+
threads << t
|
39
|
+
end
|
40
|
+
threads.each do |t|
|
41
|
+
t.join
|
42
|
+
end
|
43
|
+
|
44
|
+
assert_equal (thread_count * inner_count) , TestTable.all.count
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_multi_update
|
48
|
+
thread_count = 5
|
49
|
+
inner_count = 10
|
50
|
+
threads = []
|
51
|
+
thread_count.times do |t|
|
52
|
+
t = Thread.new do
|
53
|
+
inner_count.times do |i|
|
54
|
+
test_obj = TestTable.new
|
55
|
+
test_obj.author_name = "t1-#{1}"
|
56
|
+
test_obj.schedule_merge
|
57
|
+
end
|
58
|
+
TestTable.flush_scheduled_operations
|
59
|
+
end
|
60
|
+
threads << t
|
61
|
+
end
|
62
|
+
threads.each do |t|
|
63
|
+
t.join
|
64
|
+
end
|
65
|
+
|
66
|
+
assert_equal (thread_count * inner_count) , TestTable.all.count
|
67
|
+
TestTable.all.each do |t|
|
68
|
+
t.author_name = "tr-fff"
|
69
|
+
t.schedule_merge
|
70
|
+
end
|
71
|
+
TestTable.flush_scheduled_operations
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_update_fk_relation
|
75
|
+
#some problems during database creation and recreation
|
76
|
+
return
|
77
|
+
group = Group.new
|
78
|
+
group.schedule_merge
|
79
|
+
test_obj = TestTable.new
|
80
|
+
test_obj.author_name = 'test-1'
|
81
|
+
test_obj.group_id = group.id
|
82
|
+
test_obj.schedule_merge
|
83
|
+
|
84
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
|
85
|
+
|
86
|
+
TestTable.flush_scheduled_operations
|
87
|
+
|
88
|
+
|
89
|
+
first = TestTable.first
|
90
|
+
assert_equal('test-1',test_obj.author_name)
|
91
|
+
|
92
|
+
first.author_name = 'test-2'
|
93
|
+
first.schedule_merge
|
94
|
+
TestTable.flush_scheduled_operations
|
95
|
+
|
96
|
+
assert_equal('test-2',first.author_name)
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_connection_listener_get_called
|
100
|
+
test_obj = TestTable.new
|
101
|
+
test_obj.author_name = 'test-1'
|
102
|
+
test_obj.schedule_merge
|
103
|
+
|
104
|
+
assert_equal(0,TestTable.count)
|
105
|
+
ActiveRecord::Base.connection.commit_db_transaction
|
106
|
+
assert_equal(1,TestTable.count)
|
107
|
+
first = TestTable.first
|
108
|
+
assert_equal('test-1',test_obj.author_name)
|
109
|
+
TestTable.delete_all
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_schedule_merge_relation
|
113
|
+
group = Group.new
|
114
|
+
group.schedule_merge
|
115
|
+
test_obj = TestTable.new
|
116
|
+
test_obj.author_name = 'test-1'
|
117
|
+
group.test_tables.schedule_merge(test_obj)
|
118
|
+
|
119
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_schedule_merge_has_and_belongs_to_many_relation
|
123
|
+
part = Part.new
|
124
|
+
part.schedule_merge
|
125
|
+
assembly = Assembly.new
|
126
|
+
part.assemblies.schedule_merge(assembly)
|
127
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
|
128
|
+
db_part = Part.first
|
129
|
+
db_assembly = Assembly.first
|
130
|
+
|
131
|
+
assert_equal(1,db_part.assemblies.count)
|
132
|
+
assert_equal(db_assembly[:id],db_part.assemblies.first[:id])
|
133
|
+
|
134
|
+
assert_equal(1,db_assembly.parts.count)
|
135
|
+
assert_equal(db_part[:id],db_assembly.parts.first[:id])
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_schedule_merge_has_and_belongs_to_many_relation_custom_table_and_columns
|
139
|
+
|
140
|
+
course = Course.new
|
141
|
+
course.course_id = 10
|
142
|
+
course.schedule_merge
|
143
|
+
student = Student.new
|
144
|
+
student.student_id = 12
|
145
|
+
course.students.schedule_merge(student)
|
146
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
|
147
|
+
return
|
148
|
+
db_part = Course.first
|
149
|
+
db_assembly = Assembly.first
|
150
|
+
|
151
|
+
assert_equal(1,db_part.assemblies.count)
|
152
|
+
assert_equal(db_assembly[:id],db_part.assemblies.first[:id])
|
153
|
+
|
154
|
+
assert_equal(1,db_assembly.parts.count)
|
155
|
+
assert_equal(db_part[:id],db_assembly.parts.first[:id])
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_schedule_merge_has_and_belongs_to_many_relation_self_join
|
159
|
+
product = Product.new
|
160
|
+
product2 = Product.new
|
161
|
+
product.schedule_merge
|
162
|
+
product.related_products.schedule_merge(product2)
|
163
|
+
ActiveRecord::Bulkoperation::Util::FlushDirtyObjects.get.flush
|
164
|
+
return
|
165
|
+
db_part = Course.first
|
166
|
+
db_assembly = Assembly.first
|
167
|
+
|
168
|
+
assert_equal(1,db_part.assemblies.count)
|
169
|
+
assert_equal(db_assembly[:id],db_part.assemblies.first[:id])
|
170
|
+
|
171
|
+
assert_equal(1,db_assembly.parts.count)
|
172
|
+
assert_equal(db_part[:id],db_assembly.parts.first[:id])
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|