activerecord_bulkoperation 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|
+
|