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.
@@ -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
- @class_configs = []
13
- self.instance_exec(&block)
14
- AggregateRepository.new(@class_configs)
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
@@ -1,50 +1,48 @@
1
1
  module Vorpal
2
- module DbDriver
3
- extend self
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
- def insert(config, db_objects)
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
- def update(config, db_objects)
16
- db_objects.each do |db_object|
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
- def destroy(config, db_objects)
22
- config.db_class.delete_all(id: db_objects.map(&:id))
23
- end
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
- def load_by_foreign_key(config, id, foreign_key_info)
30
- arel = config.db_class.where(foreign_key_info.fk_column => id)
31
- arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
32
- arel.order(:id).all
33
- end
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
- def get_primary_keys(config, count)
36
- result = execute("select nextval('#{sequence_name(config)}') from generate_series(1,#{count});")
37
- result.column_values(0).map(&:to_i)
38
- end
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
- private
38
+ private
41
39
 
42
- def execute(sql)
43
- ActiveRecord::Base.connection.execute(sql)
44
- end
40
+ def execute(sql)
41
+ ActiveRecord::Base.connection.execute(sql)
42
+ end
45
43
 
46
- def sequence_name(config)
47
- "#{config.table_name}_id_seq"
44
+ def sequence_name(db_class)
45
+ "#{db_class.table_name}_id_seq"
46
+ end
48
47
  end
49
- end
50
48
  end
@@ -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.config_for_db(db_object.class)
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, has_many_config)
61
- child_config = has_many_config.child_config
62
- fk_info = has_many_config.foreign_key_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
- config, fk_info = @lookup_by_fk.first.first
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
- config = @lookup_by_id.first.first
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
- DbDriver.load_by_id(@config, @ids)
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
- DbDriver.load_by_foreign_key(@config, @fk_values, @fk_info)
139
+ driver.load_by_foreign_key(@config.db_class, @fk_values, @fk_info)
128
140
  end
129
141
  end
130
142
 
@@ -23,10 +23,6 @@ class IdentityMap
23
23
  key_objects.map { |k| @entities[key(k)] }
24
24
  end
25
25
 
26
- def map_raw(key_ids, key_class)
27
- key_ids.map { |key_id| @entities[[key_id, key_class.name]] }
28
- end
29
-
30
26
  private
31
27
 
32
28
  def key(key_object)
@@ -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
- def find_by_id(object, config)
24
- @objects[config].detect { |obj| obj.id == object.id }
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 loaded_ids(config)
28
- @objects[config].map(&:id)
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
- @objects.values.flatten
33
+ @objects_by_id.values
43
34
  end
44
35
 
45
36
  def id_lookup_done?(config, id)
46
- loaded_ids(config).include?(id)
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(h, key, values)
6
- if h[key].nil? || h[key].empty?
7
- h[key] = []
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
- h[key].concat(Array(values))
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
 
@@ -1,3 +1,3 @@
1
1
  module Vorpal
2
- VERSION = "0.0.6.rc2"
2
+ VERSION = "0.0.6.rc3"
3
3
  end
@@ -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
- test_repository = configure
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(Vorpal::DbDriver).to receive(:update).and_raise('not so good')
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