vorpal 0.0.6.rc2 → 0.0.6.rc3
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 +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
|