decisiv-sharded_database 0.1.5.1 → 0.2.0.1

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.
data/README.rdoc CHANGED
@@ -9,7 +9,7 @@ The first step was solved by creating a SQL view to pool all production records
9
9
 
10
10
  == Uses
11
11
 
12
- Currently, sharded_database works by reflecting on a set of returned records, and taking the result of #determine_connection to delegate the connection to - for each instance. The path we chose had us creating a view that would return aggregate records, with an added 'source' column specifying the originating database. We then rigged our #determine_connection to return an abstract connection model depending on the value. Other ideas for implementation include an after_create callback that writes the source database / ID to a centralized table.
12
+ Currently, sharded_database works by reflecting on a set of returned records, and taking the result of #sharded_connection_klass method to delegate the connection to - for each instance. The path we chose had us creating a view that would return aggregate records, with an added 'source' column specifying the originating database. We then rigged our #sharded_connection_klass to return an abstract connection model depending on the value. Other ideas for implementation include an after_create callback that writes the source database / ID to a centralized table.
13
13
 
14
14
 
15
15
  == Example and Usage
@@ -20,13 +20,16 @@ Setup your Aggregate model, inheriting from a class that establishes a connectio
20
20
 
21
21
  class AggregateFoo < ActiveRecord::Base
22
22
  include ShardedDatabase::Aggregate
23
+ source_class 'Foo'
23
24
 
24
- def determine_connection
25
- Connections[source.to_sym]
25
+ def sharded_connection_klass
26
+ "Connection::#{source.classify}".constantize
26
27
  end
27
28
  end
28
29
 
29
- The #determine_connection class above expects that there is a Connections constant that is a hash of key/value pairs of 'source' symbols and abstract models that connect to various databases. This method can be implemented in any way you desire to determine the connection source.
30
+ The constant returned by #sharded_connection_klass is expected to be an subclass of ActiveRecord::Base and respond to #connection. It is suggested that you use the preserve_attributes class method in the aggregate class and use that attribute to augment and/or find the constant somehow.
31
+
32
+ As of 0.2.0, the class method #source_class is required - sharded_database no longer guesses for the correct source class!
30
33
 
31
34
 
32
35
  === Loading Aggregate Records
@@ -43,12 +46,70 @@ Updating an attribute on the first record in the above array would update the co
43
46
  Associations are also taken into account, and any associations that are defined on a Foo model will be sourced correctly. I have only tested this one level deep, though.
44
47
 
45
48
 
46
- === Accessing 'raw' AggregateFoo objects
49
+ === Accessing Non-Proxyable AggregateFoo Objects
50
+
51
+ Easily done. Simply add a :aggregate_proxy option set to false to your finders.
52
+
53
+ AggreateFoo.all(:aggregate_proxy => false)
54
+
55
+
56
+ == Working without an AggregateFoo table
57
+
58
+ In the event that you need to load a Foo object directly, and you may or may not know the appropriate connection at runtime, sharded_database is able to apply itself to an ActiveRecord model directly.
59
+
60
+ This is done by accepting a :connection key in the options hash for a #find method. The value of :connection can either be an abstract ActiveRecord class, a Proc, or a Symbol, which will attempt to call a method of the same name on the class doing the finding. The latter two options are also supplied the #find arguments, which is useful when wanting to dynamically determine a connection.
61
+
62
+
63
+ === Setup
64
+
65
+ Any class that includes ShardedDatabase::Aggregate and defines a source_class will have that class be given this functionality. To add manually:
66
+
67
+ class Bar < ActiveRecord::Base
68
+ include ShardedDatabase::ModelWithConnection
69
+ end
70
+
71
+
72
+ === Usage
73
+
74
+ As mentioned above, there are three acceptable values for the :connection key. Borrowing heavily from ActionController's callback setup, acceptable values can either be an ActiveRecord class, or a Proc or class method name (represented as a symbol) that will ultimately return an ActiveRecord class. Both the Proc and method will be passed a splat of the attributes passed to #find.
75
+
76
+ The method implementation will be demonstrated in the Example, but the other implementations are as follows:
77
+
78
+ <b>Supplying a class</b>
79
+ Bar.find(:first, :order => 'created_at asc', :connection => BarConnection)
80
+
81
+ <b>Supplying a Proc</b>
82
+ Bar.find(123, :connection => lambda { |*args| (args.first % 2) == 1 ? Connection::One : Connection::Two })
83
+
84
+ ...Which would use Connection::One, as % 2 of 123 is 1.
85
+
86
+
87
+ === Example
88
+
89
+ A real world situation would be something akin to Flickr. Data is sharded over N number of databases, and the criteria sent to your finder method would be used to connect to the right datastore.
90
+
91
+ Imagine you have a photo sharing application, and due to the amount of photos managed, you're forced to shard the 'photos' database across 3 database servers. To do this with sharded_database, you'd do something like:
92
+
93
+ class Photo < ActiveRecord::Base
94
+
95
+ def self.determine_database(*attrs) # [:all, { :conditions => { :name => 'mountain' } }]
96
+ case first_letter = attrs.last[:conditions][:name].first[0,1] # get the first letter
97
+ when a..f : Connection::AToF
98
+ when g..p : Connection::GToP
99
+ when q..z : Connection::QtoZ
100
+ end
101
+
102
+ end
103
+
104
+ Photo.all(:conditions => { :name => 'mountain' }, :connection => :determine_database)
105
+
106
+ Which would return any photo named 'mountain' against the database Connection::GToP is connected to.
47
107
 
48
- Easily done. Simply add a :raw option set to true to your finder queries.
49
108
 
50
- AggreateFoo.all(:raw => true)
109
+ == TODO
51
110
 
111
+ - Apply associations to objects found using the :connection key
112
+ - Fix association application to regenerate a proper reflection/association
52
113
 
53
114
 
54
- Copyright &copy; 2008, Brennan Dunn, Decisiv Inc. Released under the MIT license.
115
+ Copyright &copy; 2008, Brennan Dunn, Decisiv Inc. Released under the MIT license.
@@ -7,55 +7,86 @@ module ShardedDatabase
7
7
  klass.extend ClassMethods
8
8
  klass.send :include, InstanceMethods
9
9
  klass.class_eval do
10
- cattr_accessor :connection_field, :source_class, :foreign_id
11
- @connection_field = :oem
10
+ cattr_accessor :foreign_id
12
11
  @foreign_id = :other_id
13
12
 
14
13
  class << self
15
- alias_method_chain :find, :raw
14
+ alias_method_chain :find, :aggregate_proxy
16
15
  end
17
16
  end
18
17
  end
19
18
 
20
19
  module ClassMethods
21
20
 
22
- def find_with_raw(*args)
23
- @raw = args.last.is_a?(Hash) && args.last.delete(:raw)
24
- @raw ? temporarily_undef_method(:after_find) { find_without_raw(*args) } : find_without_raw(*args)
21
+ def find_with_aggregate_proxy(*args)
22
+ without_aggregate_proxy = args.last.is_a?(Hash) && args.last.delete(:aggregate_proxy).is_a?(FalseClass)
23
+ if without_aggregate_proxy
24
+ temporarily_undef_method(:after_find) { find_without_aggregate_proxy(*args) }
25
+ else
26
+ find_without_aggregate_proxy(*args)
27
+ end
25
28
  end
26
-
29
+
27
30
  def preserve_attributes(*attrs)
28
31
  @preserved_attributes = attrs.map(&:to_s)
29
32
  end
33
+
34
+ def source_class(klass_name)
35
+ @source_class = klass_name
36
+ apply_model_with_connection_to(klass_name)
37
+ end
38
+
39
+
40
+ private
41
+
42
+ def apply_model_with_connection_to(klass_name)
43
+ require_dependency klass_name.underscore unless defined?(klass_name.constantize) # ensure that the source class has been loaded
44
+ klass_name.constantize.send :include, ShardedDatabase::ModelWithConnection
45
+ end
30
46
 
31
47
  end
32
48
 
33
49
  module InstanceMethods
34
50
 
35
- def determine_connection
36
- # stub method - implement your own!
51
+ def sharded_connection_klass
52
+ raise NotImplementedError,
53
+ "You must implement your own #sharded_connection_klass method that returns an ActiveRecord::Base subclass which yeilds a connection."
37
54
  end
38
55
 
39
56
  def after_find
40
- @klass = determine_connection || raise(ShardedDatabase::NoConnectionError, 'Cannot determine connection class')
57
+ @klass = sharded_connection_klass
41
58
  @connection = @klass.respond_to?(:connection) ? @klass.connection : raise(ShardedDatabase::NoConnectionError, 'Connection class does not respond to :connection')
42
59
  @foreign_id = self[self.class.foreign_id.to_sym]
43
60
 
44
61
  metaclass.delegate :connection, :to => @klass
45
-
62
+
63
+ preserve_attributes
64
+ apply_proxy
65
+ channel_associations_to_proper_connection
66
+
67
+ end
68
+
69
+
70
+ private
71
+
72
+ def preserve_attributes
46
73
  (self.class.instance_variable_get("@preserved_attributes") || []).each do |attr|
47
- metaclass.send :alias_method, "proxy_#{attr}", attr
74
+ metaclass.send :alias_method, "original_#{attr}", attr
48
75
  end
49
-
76
+ end
77
+
78
+ def apply_proxy
50
79
  class << self
51
80
  alias_method :proxy_class, :class
52
-
81
+
53
82
  include AggregateProxy
54
83
  instance_methods.each do |m|
55
- undef_method(m) unless m =~ /^__|proxy_|inspect/
84
+ undef_method(m) unless m =~ /^__|proxy_|original_|inspect/
56
85
  end
57
86
  end
58
-
87
+ end
88
+
89
+ def channel_associations_to_proper_connection
59
90
  self.class.reflect_on_all_associations.each do |a|
60
91
  metaclass.send :alias_method, "proxy_#{a.name}".to_sym, a.name.to_sym
61
92
  metaclass.send :undef_method, a.name
@@ -83,7 +114,6 @@ module ShardedDatabase
83
114
  end
84
115
  }, __FILE__, __LINE__
85
116
  end
86
-
87
117
  end
88
118
 
89
119
  end
@@ -9,20 +9,21 @@ module ShardedDatabase
9
9
  load_target.inspect.gsub(/#<([\w\:]+)\s(.*?)>/) { "#<#{$1}(#{@klass.name}) #{$2}>" }
10
10
  end
11
11
 
12
+ def respond_to?(method)
13
+ load_target.respond_to?(method) || super
14
+ end
15
+
12
16
 
13
17
  private
14
18
 
15
19
  def load_target
16
20
  @target ||= begin
17
- klass = (self.proxy_class.source_class || self.proxy_class.name.gsub('Aggregate','')).constantize
18
- borrow_connection(klass, @klass) { |k| k.find(@foreign_id) }
21
+ klass = (self.proxy_class.instance_variable_get('@source_class')).constantize
22
+ ModelWithConnection.borrow_connection(klass, @klass) { |k| k.find(@foreign_id) }
19
23
  end
20
24
  end
21
25
 
22
26
  def method_missing(method, *args, &block)
23
- if association_method?(method)
24
- #apply_connection_to_association(method)
25
- end
26
27
  load_target.respond_to?(method) ? load_target.send(method, *args, &block) : super
27
28
  end
28
29
 
@@ -30,11 +31,5 @@ module ShardedDatabase
30
31
  load_target.class.reflect_on_all_associations.any? { |a| a.name == method.to_sym }
31
32
  end
32
33
 
33
- def borrow_connection(requesting_class, target_class, &block)
34
- eigen = requesting_class.metaclass
35
- eigen.delegate :connection, :to => target_class
36
- yield(requesting_class)
37
- end
38
-
39
34
  end
40
35
  end
@@ -1,9 +1,10 @@
1
+ require 'sharded_database/model_with_connection'
1
2
  require 'sharded_database/aggregate_proxy'
2
3
  require 'sharded_database/aggregate'
3
4
  require 'sharded_database/core_extensions'
4
5
 
5
6
  module ShardedDatabase
6
7
 
7
- VERSION = '0.1.5'
8
+ VERSION = '0.2.0'
8
9
 
9
10
  end
@@ -5,16 +5,17 @@ master:
5
5
  password:
6
6
  host: localhost
7
7
 
8
- one_db:
8
+ shard_one:
9
9
  adapter: mysql
10
10
  database: sharded_database_one
11
11
  username: root
12
12
  password:
13
13
  host: localhost
14
14
 
15
- two_db:
15
+ shard_two:
16
16
  adapter: mysql
17
17
  database: sharded_database_two
18
18
  username: root
19
19
  password:
20
- host: localhost
20
+ host: localhost
21
+
data/test/lib/models.rb CHANGED
@@ -1,42 +1,33 @@
1
1
  module Connection ; end
2
2
 
3
3
  class Connection::One < ActiveRecord::Base
4
- establish_connection :one_db
4
+ establish_connection :shard_one
5
5
  self.abstract_class = true
6
6
  end
7
7
 
8
8
  class Connection::Two < ActiveRecord::Base
9
- establish_connection :two_db
9
+ establish_connection :shard_two
10
10
  self.abstract_class = true
11
11
  end
12
12
 
13
- Connections = { :one => Connection::One, :two => Connection::Two }
14
-
15
13
  class GlobalConnection < ActiveRecord::Base
16
14
  establish_connection :master
17
15
  self.abstract_class = true
18
16
  end
19
17
 
20
- class AggregateEstimate < GlobalConnection
21
- belongs_to :gun
22
- include ShardedDatabase::Aggregate
23
- self.foreign_id = :other_id
24
- preserve_attributes :source
25
-
26
- def determine_connection
27
- Connections[source.to_sym]
28
- end
29
-
30
- end
31
-
32
18
  class Company < ActiveRecord::Base
33
19
  has_many :items
34
20
  end
35
21
 
36
- class Estimate < ActiveRecord::Base
22
+ class Employee < ActiveRecord::Base
37
23
  belongs_to :company
38
24
  has_many :items
39
25
 
26
+ def self.pick_connection(*args)
27
+ id_to_find = args.first
28
+ (id_to_find % 2) == 1 ? Connection::One : Connection::Two
29
+ end
30
+
40
31
  def call_company
41
32
  company
42
33
  end
@@ -44,5 +35,18 @@ class Estimate < ActiveRecord::Base
44
35
  end
45
36
 
46
37
  class Item < ActiveRecord::Base
47
- belongs_to :estimate
38
+ belongs_to :employee
39
+ end
40
+
41
+ class AggregateEmployee < GlobalConnection
42
+ belongs_to :gun
43
+ include ShardedDatabase::Aggregate
44
+ self.foreign_id = :other_id
45
+ source_class 'Employee'
46
+ preserve_attributes :source
47
+
48
+ def sharded_connection_klass
49
+ "Connection::#{source.classify}".constantize
50
+ end
51
+
48
52
  end
@@ -21,11 +21,11 @@ module ShardedDatabase
21
21
  SQLite3::Database.new(file)
22
22
  end
23
23
 
24
- # setup aggregate table
24
+ # Setup master table
25
25
  ::ActiveRecord::Base.establish_connection :master
26
26
  ::ActiveRecord::Base.class_eval do
27
27
  silence do
28
- connection.create_table :aggregate_estimates, :force => true do |t|
28
+ connection.create_table :aggregate_employees, :force => true do |t|
29
29
  t.string :source
30
30
  t.integer :other_id
31
31
  t.timestamp :created_at
@@ -33,18 +33,18 @@ module ShardedDatabase
33
33
  end
34
34
  end
35
35
 
36
- # setup NUM_db.estimates
36
+ # Setup sharded DBs for employees
37
37
  %w(one two).each do |num|
38
- ::ActiveRecord::Base.establish_connection "#{num}_db".to_sym
38
+ ::ActiveRecord::Base.establish_connection "shard_#{num}".to_sym
39
39
  ::ActiveRecord::Base.class_eval do
40
40
  silence do
41
- connection.create_table :estimates, :force => true do |t|
41
+ connection.create_table :employees, :force => true do |t|
42
42
  t.belongs_to :company
43
43
  t.string :name
44
44
  end
45
45
 
46
46
  connection.create_table :items, :force => true do |t|
47
- t.belongs_to :estimate
47
+ t.belongs_to :employee
48
48
  t.string :name
49
49
  end
50
50
 
@@ -62,19 +62,19 @@ module ShardedDatabase
62
62
  one_company = Class.new(Connection::One) { set_table_name 'companies' }
63
63
  @company_1 = one_company.create :name => 'One Company'
64
64
 
65
- one_estimate = Class.new(Connection::One) { set_table_name 'estimates' ; has_many(:items) ; belongs_to(:company) }
66
- @one_1 = one_estimate.create :name => 'One Estimate', :company_id => @company_1.id
65
+ one_employee = Class.new(Connection::One) { set_table_name 'employees' ; has_many(:items) ; belongs_to(:company) }
66
+ @one_1 = one_employee.create :name => 'One Employee', :company_id => @company_1.id
67
67
 
68
- two_estimate = Class.new(Connection::Two) { set_table_name 'estimates' ; has_many(:items) }
69
- @two_1 = two_estimate.create :name => 'One Estimate 1'
70
- @two_2 = two_estimate.create :name => 'Two Estimate 2'
68
+ two_employee = Class.new(Connection::Two) { set_table_name 'employees' ; has_many(:items) }
69
+ @two_1 = two_employee.create :name => 'One Employee 1'
70
+ @two_2 = two_employee.create :name => 'Two Employee 2'
71
71
 
72
- one_item = Class.new(Connection::One) { set_table_name 'items' ; belongs_to(:estimate) }
73
- one_item.create :name => 'One Test Item', :estimate_id => @one_1.id
72
+ one_item = Class.new(Connection::One) { set_table_name 'items' ; belongs_to(:employee) }
73
+ one_item.create :name => 'One Test Item', :employee_id => @one_1.id
74
74
 
75
- AggregateEstimate.create :source => 'one', :other_id => @one_1.id
76
- AggregateEstimate.create :source => 'two', :other_id => @two_1.id
77
- AggregateEstimate.create :source => 'two', :other_id => @two_2.id
75
+ AggregateEmployee.create :source => 'one', :other_id => @one_1.id
76
+ AggregateEmployee.create :source => 'two', :other_id => @two_1.id
77
+ AggregateEmployee.create :source => 'two', :other_id => @two_2.id
78
78
  end
79
79
 
80
80
  end
@@ -4,25 +4,25 @@ class AssociationTest < ShardedDatabase::TestCase
4
4
 
5
5
  def setup
6
6
  setup_environment
7
- @parent = AggregateEstimate.find_by_source('one')
7
+ @parent = AggregateEmployee.find_by_source('one')
8
8
  end
9
9
 
10
10
  context 'Connection delegation on has_many associations' do
11
11
 
12
12
  should 'fetch items from the parent instance connection' do
13
13
  assert ! @parent.items.empty?
14
- assert_connection :one_db, @parent.items.first
14
+ assert_connection :shard_one, @parent.items.first
15
15
  end
16
16
 
17
17
  should 'keep its connection when bubbling up to an associations parent' do
18
- assert_equal @parent, @parent.items.first.estimate
18
+ assert_equal @parent, @parent.items.first.employee
19
19
  end
20
20
 
21
21
  end
22
22
 
23
23
  context '[UNFINISHED] Connection delegation on belongs_to associations' do
24
24
 
25
- should 'fetch the associated company for an estimate from the respective connection' do
25
+ should 'fetch the associated company for an employee from the respective connection' do
26
26
  assert_instance_of Company, @parent.company
27
27
  end
28
28
 
@@ -5,18 +5,38 @@ class ConnectionTest < ShardedDatabase::TestCase
5
5
 
6
6
 
7
7
  should 'foundationally support instances having a different connection than their parent class' do
8
- assert_not_equal AggregateEstimate.first.connection, AggregateEstimate.connection
8
+ assert_not_equal AggregateEmployee.first.connection, AggregateEmployee.connection
9
9
  end
10
10
 
11
11
  should 'have instances of the same source share the same connection' do
12
- first, second = AggregateEstimate.find_all_by_source('two', :limit => 2)
12
+ first, second = AggregateEmployee.find_all_by_source('two', :limit => 2)
13
13
 
14
- assert_connection :two_db, first, second
14
+ assert_connection :shard_two, first, second
15
15
  assert_equal first.connection.object_id, second.connection.object_id # ensure that delegation is working correctly
16
16
  end
17
17
 
18
18
  should 'display instance connection when inspecting' do
19
- assert_match %{(Connection::One)}, AggregateEstimate.find_by_source('one').inspect
19
+ assert_match %{(Connection::One)}, AggregateEmployee.find_by_source('one').inspect
20
+ end
21
+
22
+ context 'loading records when given a :connection key in the options hash' do
23
+
24
+ should 'allow for an ActiveRecord::Base class to be supplied' do
25
+ object = Employee.first(:connection => Connection::One)
26
+ assert_connection :shard_one, object
27
+ end
28
+
29
+ should 'allow for a Proc object to be supplied' do
30
+ proc = lambda { |*args| (args.first % 2) == 1 ? Connection::One : Connection::Two }
31
+
32
+ assert_connection :shard_one, Employee.find(1, :connection => proc)
33
+ assert_connection :shard_two, Employee.find(2, :connection => proc)
34
+ end
35
+
36
+ should 'allow for a symbol to be supplied (which calls a method)' do
37
+ assert_connection :shard_one, Employee.find(1, :connection => :pick_connection)
38
+ end
39
+
20
40
  end
21
41
 
22
42
  end
@@ -4,14 +4,14 @@ class InstanceTest < ShardedDatabase::TestCase
4
4
  def setup ; setup_environment ; end
5
5
 
6
6
 
7
- context 'Loading raw aggregate objects' do
7
+ context 'Loading non-proxyable aggregate objects' do
8
8
 
9
9
  setup do
10
- @aggregates = AggregateEstimate.all(:raw => true)
10
+ @aggregates = AggregateEmployee.all(:aggregate_proxy => false)
11
11
  end
12
12
 
13
- should 'all be AggregateEstimate instances' do
14
- assert @aggregates.all? { |a| a.is_a?(AggregateEstimate) }
13
+ should 'all be AggregateEmployee instances' do
14
+ assert @aggregates.all? { |a| a.is_a?(AggregateEmployee) }
15
15
  end
16
16
 
17
17
  end
@@ -19,21 +19,25 @@ class InstanceTest < ShardedDatabase::TestCase
19
19
  context "A transformed aggregate instance" do
20
20
 
21
21
  setup do
22
- @estimate = AggregateEstimate.first
22
+ @employee = AggregateEmployee.first
23
23
  end
24
-
24
+
25
25
  should 'channel calls to #class to the proxy class' do
26
- assert @estimate.is_a?(Estimate)
26
+ assert @employee.is_a?(Employee)
27
27
  end
28
28
 
29
29
  should 'have the same attribute fields as the proxy class' do
30
- assert_same_elements @estimate.attributes.keys, Estimate.column_names
30
+ assert_same_elements @employee.attributes.keys, Employee.column_names
31
31
  end
32
32
 
33
33
  should 'preserve attributes supplied to #preserve_attributes' do
34
- assert_equal 'one', @estimate.proxy_source
34
+ assert_equal 'one', @employee.original_source
35
35
  end
36
36
 
37
+ should 'have #respond_to? proxy to the instance' do
38
+ assert @employee.respond_to?(:call_company)
39
+ end
40
+
37
41
  end
38
42
 
39
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decisiv-sharded_database
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5.1
4
+ version: 0.2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brennan Dunn