vorpal 0.0.6.rc2 → 0.0.6.rc3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/lib/vorpal/aggregate_repository.rb +31 -56
- data/lib/vorpal/configs.rb +208 -198
- data/lib/vorpal/configuration.rb +15 -4
- data/lib/vorpal/db_driver.rb +33 -35
- data/lib/vorpal/db_loader.rb +29 -17
- data/lib/vorpal/identity_map.rb +0 -4
- data/lib/vorpal/loaded_objects.rb +8 -21
- data/lib/vorpal/util/array_hash.rb +10 -4
- data/lib/vorpal/version.rb +1 -1
- data/spec/helpers/db_helpers.rb +18 -0
- data/spec/integration_spec_helper.rb +4 -0
- data/spec/vorpal/aggregate_repository_spec.rb +5 -70
- data/spec/vorpal/configs_spec.rb +107 -0
- data/spec/vorpal/performance_spec.rb +192 -0
- metadata +8 -2
data/lib/vorpal/configuration.rb
CHANGED
@@ -7,11 +7,15 @@ module Configuration
|
|
7
7
|
|
8
8
|
# Configures and creates a {Vorpal::AggregateRepository} instance.
|
9
9
|
#
|
10
|
+
# @param options [Hash] Global configuration options for the repository instance.
|
11
|
+
# @option options [Object] :db_driver (Object that will be used to interact with the DB.)
|
12
|
+
# Must be duck-type compatible with Vorpal::DbDriver.
|
13
|
+
#
|
10
14
|
# @return [Vorpal::AggregateRepository] Repository instance.
|
11
|
-
def define(&block)
|
12
|
-
|
13
|
-
|
14
|
-
AggregateRepository.new(
|
15
|
+
def define(options={}, &block)
|
16
|
+
master_config = build_config(&block)
|
17
|
+
db_driver = options.fetch(:db_driver, DbDriver.new)
|
18
|
+
AggregateRepository.new(db_driver, master_config)
|
15
19
|
end
|
16
20
|
|
17
21
|
# Maps a domain class to a relational table.
|
@@ -35,5 +39,12 @@ module Configuration
|
|
35
39
|
|
36
40
|
@class_configs << builder.build
|
37
41
|
end
|
42
|
+
|
43
|
+
# @private
|
44
|
+
def build_config(&block)
|
45
|
+
@class_configs = []
|
46
|
+
self.instance_exec(&block)
|
47
|
+
MasterConfig.new(@class_configs)
|
48
|
+
end
|
38
49
|
end
|
39
50
|
end
|
data/lib/vorpal/db_driver.rb
CHANGED
@@ -1,50 +1,48 @@
|
|
1
1
|
module Vorpal
|
2
|
-
|
3
|
-
|
2
|
+
class DbDriver
|
3
|
+
def insert(db_class, db_objects)
|
4
|
+
if defined? ActiveRecord::Import
|
5
|
+
db_class.import(db_objects, validate: false)
|
6
|
+
else
|
7
|
+
db_objects.each do |db_object|
|
8
|
+
db_object.save!(validate: false)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
4
12
|
|
5
|
-
|
6
|
-
if defined? ActiveRecord::Import
|
7
|
-
config.db_class.import db_objects
|
8
|
-
else
|
13
|
+
def update(db_class, db_objects)
|
9
14
|
db_objects.each do |db_object|
|
10
|
-
db_object.save!
|
15
|
+
db_object.save!(validate: false)
|
11
16
|
end
|
12
17
|
end
|
13
|
-
end
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
db_object.save!
|
19
|
+
def destroy(db_class, db_objects)
|
20
|
+
db_class.delete_all(id: db_objects.map(&:id))
|
18
21
|
end
|
19
|
-
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
def load_by_id(config, ids)
|
26
|
-
config.db_class.where(id: ids)
|
27
|
-
end
|
23
|
+
def load_by_id(db_class, ids)
|
24
|
+
db_class.where(id: ids)
|
25
|
+
end
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
27
|
+
def load_by_foreign_key(db_class, id, foreign_key_info)
|
28
|
+
arel = db_class.where(foreign_key_info.fk_column => id)
|
29
|
+
arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
|
30
|
+
arel.order(:id).all
|
31
|
+
end
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
def get_primary_keys(db_class, count)
|
34
|
+
result = execute("select nextval('#{sequence_name(db_class)}') from generate_series(1,#{count});")
|
35
|
+
result.column_values(0).map(&:to_i)
|
36
|
+
end
|
39
37
|
|
40
|
-
|
38
|
+
private
|
41
39
|
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
def execute(sql)
|
41
|
+
ActiveRecord::Base.connection.execute(sql)
|
42
|
+
end
|
45
43
|
|
46
|
-
|
47
|
-
|
44
|
+
def sequence_name(db_class)
|
45
|
+
"#{db_class.table_name}_id_seq"
|
46
|
+
end
|
48
47
|
end
|
49
|
-
end
|
50
48
|
end
|
data/lib/vorpal/db_loader.rb
CHANGED
@@ -4,11 +4,14 @@ require 'vorpal/db_driver'
|
|
4
4
|
|
5
5
|
module Vorpal
|
6
6
|
|
7
|
+
# Handles loading of objects from the database.
|
8
|
+
#
|
7
9
|
# @private
|
8
10
|
class DbLoader
|
9
|
-
def initialize(configs, only_owned)
|
11
|
+
def initialize(configs, only_owned, driver)
|
10
12
|
@configs = configs
|
11
13
|
@only_owned = only_owned
|
14
|
+
@driver = driver
|
12
15
|
end
|
13
16
|
|
14
17
|
def load_from_db(ids, domain_class)
|
@@ -19,7 +22,7 @@ class DbLoader
|
|
19
22
|
|
20
23
|
until @lookup_instructions.empty?
|
21
24
|
lookup = @lookup_instructions.next_lookup
|
22
|
-
new_objects = lookup.load_all
|
25
|
+
new_objects = lookup.load_all(@driver)
|
23
26
|
@loaded_objects.add(lookup.config, new_objects)
|
24
27
|
explore_objects(new_objects)
|
25
28
|
end
|
@@ -31,7 +34,7 @@ class DbLoader
|
|
31
34
|
|
32
35
|
def explore_objects(objects_to_explore)
|
33
36
|
objects_to_explore.each do |db_object|
|
34
|
-
config = @configs.
|
37
|
+
config = @configs.config_for_db_object(db_object)
|
35
38
|
config.has_manys.each do |has_many_config|
|
36
39
|
lookup_by_fk(db_object, has_many_config) if explore_association?(has_many_config)
|
37
40
|
end
|
@@ -57,11 +60,10 @@ class DbLoader
|
|
57
60
|
@lookup_instructions.lookup_by_id(child_config, id)
|
58
61
|
end
|
59
62
|
|
60
|
-
def lookup_by_fk(db_object,
|
61
|
-
child_config =
|
62
|
-
fk_info =
|
63
|
+
def lookup_by_fk(db_object, has_some_config)
|
64
|
+
child_config = has_some_config.child_config
|
65
|
+
fk_info = has_some_config.foreign_key_info
|
63
66
|
fk_value = db_object.id
|
64
|
-
return if @loaded_objects.fk_lookup_done?(child_config, fk_info, fk_value)
|
65
67
|
@lookup_instructions.lookup_by_fk(child_config, fk_info, fk_value)
|
66
68
|
end
|
67
69
|
end
|
@@ -84,19 +86,29 @@ class LookupInstructions
|
|
84
86
|
|
85
87
|
def next_lookup
|
86
88
|
if @lookup_by_id.empty?
|
87
|
-
|
88
|
-
fk_values = @lookup_by_fk.delete([config, fk_info])
|
89
|
-
LookupByFk.new(config, fk_info, fk_values)
|
89
|
+
pop_fk_lookup
|
90
90
|
else
|
91
|
-
|
92
|
-
ids = @lookup_by_id.delete(config)
|
93
|
-
LookupById.new(config, ids)
|
91
|
+
pop_id_lookup
|
94
92
|
end
|
95
93
|
end
|
96
94
|
|
97
95
|
def empty?
|
98
96
|
@lookup_by_id.empty? && @lookup_by_fk.empty?
|
99
97
|
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def pop_id_lookup
|
102
|
+
config, ids = pop(@lookup_by_id)
|
103
|
+
LookupById.new(config, ids)
|
104
|
+
end
|
105
|
+
|
106
|
+
def pop_fk_lookup
|
107
|
+
key, fk_values = pop(@lookup_by_fk)
|
108
|
+
config = key.first
|
109
|
+
fk_info = key.last
|
110
|
+
LookupByFk.new(config, fk_info, fk_values)
|
111
|
+
end
|
100
112
|
end
|
101
113
|
|
102
114
|
# @private
|
@@ -107,9 +119,9 @@ class LookupById
|
|
107
119
|
@ids = ids
|
108
120
|
end
|
109
121
|
|
110
|
-
def load_all
|
122
|
+
def load_all(driver)
|
111
123
|
return [] if @ids.empty?
|
112
|
-
|
124
|
+
driver.load_by_id(@config.db_class, @ids)
|
113
125
|
end
|
114
126
|
end
|
115
127
|
|
@@ -122,9 +134,9 @@ class LookupByFk
|
|
122
134
|
@fk_values = fk_values
|
123
135
|
end
|
124
136
|
|
125
|
-
def load_all
|
137
|
+
def load_all(driver)
|
126
138
|
return [] if @fk_values.empty?
|
127
|
-
|
139
|
+
driver.load_by_foreign_key(@config.db_class, @fk_values, @fk_info)
|
128
140
|
end
|
129
141
|
end
|
130
142
|
|
data/lib/vorpal/identity_map.rb
CHANGED
@@ -14,40 +14,27 @@ class LoadedObjects
|
|
14
14
|
|
15
15
|
def initialize
|
16
16
|
@objects = Hash.new([])
|
17
|
+
@objects_by_id = Hash.new
|
17
18
|
end
|
18
19
|
|
19
20
|
def add(config, objects)
|
20
21
|
add_to_hash(@objects, config, objects)
|
21
|
-
end
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
objects.each do |object|
|
24
|
+
@objects_by_id[[config.domain_class.name, object.id]] = object
|
25
|
+
end
|
25
26
|
end
|
26
27
|
|
27
|
-
def
|
28
|
-
@
|
29
|
-
end
|
30
|
-
|
31
|
-
def loaded_fk_values(config, fk_info)
|
32
|
-
if fk_info.polymorphic?
|
33
|
-
@objects[config].
|
34
|
-
find_all { |db_object| fk_info.matches_polymorphic_type?(db_object) }.
|
35
|
-
map(&(fk_info.fk_column.to_sym))
|
36
|
-
else
|
37
|
-
@objects[config].map(&(fk_info.fk_column.to_sym))
|
38
|
-
end
|
28
|
+
def find_by_id(config, id)
|
29
|
+
@objects_by_id[[config.domain_class.name, id]]
|
39
30
|
end
|
40
31
|
|
41
32
|
def all_objects
|
42
|
-
@
|
33
|
+
@objects_by_id.values
|
43
34
|
end
|
44
35
|
|
45
36
|
def id_lookup_done?(config, id)
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
def fk_lookup_done?(config, fk_info, fk_value)
|
50
|
-
loaded_fk_values(config, fk_info).include?(fk_value)
|
37
|
+
!find_by_id(config, id).nil?
|
51
38
|
end
|
52
39
|
end
|
53
40
|
end
|
@@ -2,11 +2,17 @@ module Vorpal
|
|
2
2
|
|
3
3
|
# @private
|
4
4
|
module ArrayHash
|
5
|
-
def add_to_hash(
|
6
|
-
if
|
7
|
-
|
5
|
+
def add_to_hash(array_hash, key, values)
|
6
|
+
if array_hash[key].nil? || array_hash[key].empty?
|
7
|
+
array_hash[key] = []
|
8
8
|
end
|
9
|
-
|
9
|
+
array_hash[key].concat(Array(values))
|
10
|
+
end
|
11
|
+
|
12
|
+
def pop(array_hash)
|
13
|
+
key = array_hash.first.first
|
14
|
+
values = array_hash.delete(key)
|
15
|
+
[key, values]
|
10
16
|
end
|
11
17
|
end
|
12
18
|
|
data/lib/vorpal/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
module DbHelpers
|
2
|
+
# when you change a table's columns, set force to true to re-generate the table in the DB
|
3
|
+
def define_table(table_name, columns, force)
|
4
|
+
if !ActiveRecord::Base.connection.table_exists?(table_name) || force
|
5
|
+
ActiveRecord::Base.connection.create_table(table_name, force: true) do |t|
|
6
|
+
columns.each do |name, type|
|
7
|
+
t.send(type, name)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def defineAr(table_name)
|
14
|
+
Class.new(ActiveRecord::Base) do
|
15
|
+
self.table_name = table_name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -12,7 +12,11 @@ ActiveRecord::Base.establish_connection(
|
|
12
12
|
min_messages: 'error'
|
13
13
|
)
|
14
14
|
|
15
|
+
require 'helpers/db_helpers'
|
16
|
+
|
15
17
|
RSpec.configure do |config|
|
18
|
+
config.include DbHelpers
|
19
|
+
|
16
20
|
# implements `use_transactional_fixtures = true`
|
17
21
|
# from lib/active_record/fixtures.rb
|
18
22
|
# works with Rails 3.2. Probably not with Rails 4
|
@@ -104,11 +104,12 @@ describe 'Aggregate Repository' do
|
|
104
104
|
|
105
105
|
describe 'on error' do
|
106
106
|
it 'nils ids of new objects' do
|
107
|
-
|
107
|
+
db_driver = Vorpal::DbDriver.new
|
108
|
+
test_repository = configure(db_driver: db_driver)
|
108
109
|
|
109
110
|
tree_db = TreeDB.create!
|
110
111
|
|
111
|
-
expect(
|
112
|
+
expect(db_driver).to receive(:update).and_raise('not so good')
|
112
113
|
|
113
114
|
fissure = Fissure.new
|
114
115
|
tree = Tree.new(id: tree_db.id, fissures: [fissure])
|
@@ -580,72 +581,6 @@ describe 'Aggregate Repository' do
|
|
580
581
|
end
|
581
582
|
end
|
582
583
|
|
583
|
-
describe 'lots of data' do
|
584
|
-
it 'avoids N+1s on load' do
|
585
|
-
test_repository = Vorpal.define do
|
586
|
-
map Tree do
|
587
|
-
fields :name
|
588
|
-
belongs_to :trunk
|
589
|
-
# has_many :fissures
|
590
|
-
has_many :branches
|
591
|
-
end
|
592
|
-
|
593
|
-
map Trunk do
|
594
|
-
fields :length
|
595
|
-
has_one :tree
|
596
|
-
has_many :bugs, fk: :lives_on_id, fk_type: :lives_on_type
|
597
|
-
end
|
598
|
-
|
599
|
-
map Branch do
|
600
|
-
fields :length
|
601
|
-
belongs_to :tree
|
602
|
-
has_many :bugs, fk: :lives_on_id, fk_type: :lives_on_type
|
603
|
-
has_many :branches
|
604
|
-
end
|
605
|
-
|
606
|
-
map Bug do
|
607
|
-
fields :name
|
608
|
-
belongs_to :lives_on, fk: :lives_on_id, fk_type: :lives_on_type, child_classes: [Trunk, Branch]
|
609
|
-
end
|
610
|
-
# map Fissure, to: Fissure
|
611
|
-
end
|
612
|
-
|
613
|
-
ids = (1..3).map do
|
614
|
-
trunk_db = TrunkDB.create!
|
615
|
-
tree_db = TreeDB.create!(trunk_id: trunk_db.id)
|
616
|
-
branch_db1 = BranchDB.create!(tree_id: tree_db.id)
|
617
|
-
branch_db2 = BranchDB.create!(tree_id: tree_db.id)
|
618
|
-
branch_db3 = BranchDB.create!(branch_id: branch_db2.id)
|
619
|
-
BugDB.create!(name: 'trunk bug', lives_on_id: trunk_db.id, lives_on_type: Trunk.name)
|
620
|
-
BugDB.create!(name: 'branch bug!', lives_on_id: branch_db1.id, lives_on_type: Branch.name)
|
621
|
-
tree_db.id
|
622
|
-
end
|
623
|
-
|
624
|
-
puts '*************************'
|
625
|
-
puts '*************************'
|
626
|
-
puts '*************************'
|
627
|
-
puts '*************************'
|
628
|
-
test_repository.load_all(ids, Tree)
|
629
|
-
end
|
630
|
-
end
|
631
|
-
|
632
|
-
# when you change a table's columns, set force to true to re-generate the table in the DB
|
633
|
-
def define_table(table_name, columns, force)
|
634
|
-
if !ActiveRecord::Base.connection.table_exists?(table_name) || force
|
635
|
-
ActiveRecord::Base.connection.create_table(table_name, force: true) do |t|
|
636
|
-
columns.each do |name, type|
|
637
|
-
t.send(type, name)
|
638
|
-
end
|
639
|
-
end
|
640
|
-
end
|
641
|
-
end
|
642
|
-
|
643
|
-
def defineAr(table_name)
|
644
|
-
Class.new(ActiveRecord::Base) do
|
645
|
-
self.table_name = table_name
|
646
|
-
end
|
647
|
-
end
|
648
|
-
|
649
584
|
private
|
650
585
|
|
651
586
|
def configure_polymorphic_has_many
|
@@ -763,8 +698,8 @@ private
|
|
763
698
|
end
|
764
699
|
end
|
765
700
|
|
766
|
-
def configure
|
767
|
-
Vorpal.define do
|
701
|
+
def configure(options={})
|
702
|
+
Vorpal.define(options) do
|
768
703
|
map Tree do
|
769
704
|
fields :name
|
770
705
|
belongs_to :trunk
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'unit_spec_helper'
|
2
|
+
require 'vorpal/configs'
|
3
|
+
|
4
|
+
describe Vorpal::MasterConfig do
|
5
|
+
class Post
|
6
|
+
attr_accessor :comments
|
7
|
+
attr_accessor :best_comment
|
8
|
+
end
|
9
|
+
|
10
|
+
class Comment
|
11
|
+
attr_accessor :post
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:post_config) { Vorpal::ClassConfig.new(domain_class: Post) }
|
15
|
+
let(:comment_config) { Vorpal::ClassConfig.new(domain_class: Comment) }
|
16
|
+
let(:post_has_many_comments_config) { Vorpal::HasManyConfig.new(name: 'comments', fk: 'post_id', child_class: Comment) }
|
17
|
+
let(:post_has_one_comment_config) { Vorpal::HasOneConfig.new(name: 'best_comment', fk: 'post_id', child_class: Comment) }
|
18
|
+
let(:comment_belongs_to_post_config) { Vorpal::BelongsToConfig.new(name: 'post', fk: 'post_id', child_classes: [Post]) }
|
19
|
+
|
20
|
+
describe 'local_association_configs' do
|
21
|
+
it 'builds an association_config for a belongs_to' do
|
22
|
+
comment_config.belongs_tos << comment_belongs_to_post_config
|
23
|
+
|
24
|
+
Vorpal::MasterConfig.new([post_config, comment_config])
|
25
|
+
|
26
|
+
expect(comment_config.local_association_configs.size).to eq(1)
|
27
|
+
expect(post_config.local_association_configs.size).to eq(0)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'sets the association end configs' do
|
31
|
+
comment_config.belongs_tos << comment_belongs_to_post_config
|
32
|
+
post_config.has_manys << post_has_many_comments_config
|
33
|
+
|
34
|
+
Vorpal::MasterConfig.new([post_config, comment_config])
|
35
|
+
|
36
|
+
association_config = comment_config.local_association_configs.first
|
37
|
+
|
38
|
+
expect(association_config.remote_end_config).to eq(post_has_many_comments_config)
|
39
|
+
expect(association_config.local_end_config).to eq(comment_belongs_to_post_config)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'builds an association_config for a has_many' do
|
43
|
+
post_config.has_manys << post_has_many_comments_config
|
44
|
+
|
45
|
+
Vorpal::MasterConfig.new([post_config, comment_config])
|
46
|
+
|
47
|
+
expect(comment_config.local_association_configs.size).to eq(1)
|
48
|
+
expect(post_config.local_association_configs.size).to eq(0)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe Vorpal::AssociationConfig do
|
53
|
+
describe 'associate' do
|
54
|
+
let(:post) { Post.new }
|
55
|
+
let(:comment) { Comment.new }
|
56
|
+
|
57
|
+
it 'sets both ends of a one-to-one association' do
|
58
|
+
config = Vorpal::AssociationConfig.new(comment_config, 'post_id', nil)
|
59
|
+
config.add_remote_class_config(post_config)
|
60
|
+
|
61
|
+
config.local_end_config = comment_belongs_to_post_config
|
62
|
+
config.remote_end_config = post_has_one_comment_config
|
63
|
+
|
64
|
+
config.associate(comment, post)
|
65
|
+
|
66
|
+
expect(comment.post).to eq(post)
|
67
|
+
expect(post.best_comment).to eq(comment)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'sets both ends of a one-to-many association' do
|
71
|
+
config = Vorpal::AssociationConfig.new(comment_config, 'post_id', nil)
|
72
|
+
config.add_remote_class_config(post_config)
|
73
|
+
|
74
|
+
config.local_end_config = comment_belongs_to_post_config
|
75
|
+
config.remote_end_config = post_has_many_comments_config
|
76
|
+
|
77
|
+
config.associate(comment, post)
|
78
|
+
|
79
|
+
expect(comment.post).to eq(post)
|
80
|
+
expect(post.comments).to eq([comment])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'remote_class_config' do
|
85
|
+
it 'works with non-polymorphic associations' do
|
86
|
+
config = Vorpal::AssociationConfig.new(comment_config, 'post_id', nil)
|
87
|
+
config.add_remote_class_config(post_config)
|
88
|
+
|
89
|
+
post = Post.new
|
90
|
+
class_config = config.remote_class_config(post)
|
91
|
+
|
92
|
+
expect(class_config).to eq(post_config)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'works with polymorphic associations' do
|
96
|
+
config = Vorpal::AssociationConfig.new(comment_config, 'commented_upon_id', 'commented_upon_type')
|
97
|
+
config.add_remote_class_config(post_config)
|
98
|
+
config.add_remote_class_config(comment_config)
|
99
|
+
|
100
|
+
comment = double('comment', commented_upon_type: 'Comment')
|
101
|
+
class_config = config.remote_class_config(comment)
|
102
|
+
|
103
|
+
expect(class_config).to eq(comment_config)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|