kristjan-st-elsewhere 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Brian A. Doll
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,64 @@
1
+ === St. Elsewhere
2
+
3
+ An ActiveRecord plugin to support relationships across different databases
4
+
5
+ http://emphaticsolutions.com/images/st.elsewhere.jpg
6
+
7
+ === The Scenario
8
+
9
+ For a variety of reasons, you might find yourself supporting multiple databases in your Rails application. Maybe you're connecting to a legacy database for a few models. Perhaps you have divided your Rails application into two parts, one database for your online catalog system and another for transactional data. Multiple database connections in Rails is {nothing new.}[http://www.google.com/search?hl=en&source=hp&q=multiple+database+connections+rails&aq=f&oq=&aqi=g4]
10
+
11
+ === The Problem
12
+
13
+ While there may be great benefits to connecting to multiple databases in your app, there are also costs. One example is that <b><tt>has_many :children, :through => parent_children</tt></b> may not work.
14
+
15
+ If your database connections are available on the same host {you can prefix your ActiveRecord table names with the database name}[http://gist.github.com/239853].
16
+
17
+ If your database connections are on two different hosts, no JOINs can save you and you'll need to implement your relationships in code.
18
+
19
+ === The Solution
20
+
21
+ St. Elsewhere adds a new class method (<tt>has_many_elsewhere</tt>) to support basic association methods across different database connections for ActiveRecord models.
22
+
23
+ Example:
24
+
25
+ http://emphaticsolutions.com/images/has_many_elsewhere.png
26
+
27
+ class Hospital < AcitveRecord::Base
28
+ has_many :hospital_doctors
29
+ has_many_elsewhere :doctors, :through => :hospital_doctors
30
+ end
31
+
32
+ class HospitalDoctor < ActiveRecord::Base
33
+ belongs_to :hospital
34
+ belongs_to :doctor
35
+ end
36
+
37
+ class TransactionalBase < ActiveRecord::Base
38
+ self.abstract_class = true
39
+ establish_connection "#{RAILS_ENV}-transactional"
40
+ end
41
+
42
+ class Doctor < TransactionalBase
43
+ has_many :hospital_doctors
44
+ has_many :hospitals, :through => :hospital_doctors
45
+ end
46
+
47
+ The following conventional methods are available for Hospital:
48
+ hospital.doctors, hospital.doctors=, hospital.doctor_ids, hospital.doctor_ids=
49
+
50
+ === Inefficiencies
51
+
52
+ <tt>has_many_elsewhere</tt> is certainly much less efficient than a comparable has_many relationship. <tt>has_many :through</tt> relationships use SQL JOINs which while efficient, do not work across multiple database connections. St. Elsewhere implements much of the same resulting API methods in code, using less efficient SQL.
53
+
54
+ === Install from gemcutter
55
+
56
+ gem install st-elsewhere
57
+
58
+ === Roadmap
59
+
60
+ Currently st-elsewhere is implemented as a basic ruby module that implements some of the basic functionality of has_many :through relationships in ActiveRecord. A much more robust implementation would be to create an ActiveRecord association proxy, like HasManyThroughAssociation, that emulates the same API and could be integrated into the standard has_many class method. I will likely be waiting for Rails 3 to be released (and thus the new base ORM implementation) before attempting the association proxy route.
61
+
62
+ === Thanks
63
+
64
+ Thanks to {James Reynolds}[http://drtoast.com/] for the great name and thanks to {Tanner Donovan}[http://github.com/ttdonovan] for patches and being the first production customer.
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "kristjan-st-elsewhere"
8
+ gem.summary = %Q{St. Elsewhere supports has_many :through relationships across different databases}
9
+ gem.description = %Q{This gem provides has_many_elsewhere, an ActiveRecord class method to support many to many relationships in Rails applications, across multiple database connections.}
10
+ gem.email = ["brian@emphaticsolutions.com", "kristjan@gmail.com"]
11
+ gem.homepage = "http://github.com/kristjan/st-elsewhere"
12
+ gem.authors = ["Brian Doll", "Kristján Pétursson"]
13
+ gem.add_development_dependency "rr", ">= 0"
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/*test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ task :test => :check_dependencies
28
+
29
+ task :default => :test
30
+
31
+ require 'rake/rdoctask'
32
+ Rake::RDocTask.new do |rdoc|
33
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
34
+
35
+ rdoc.rdoc_dir = 'rdoc'
36
+ rdoc.title = "st-elsewhere #{version}"
37
+ rdoc.rdoc_files.include('README*')
38
+ rdoc.rdoc_files.include('lib/**/*.rb')
39
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.6
@@ -0,0 +1,9 @@
1
+ 0.1.5 "Ed Flanders"
2
+ - This is the first acceptable production release of st-elsewhere.
3
+ - Implements:
4
+ - has_many_elsewhere :association, :through => :through_association
5
+ - Provides:
6
+ - ARKlass#associations
7
+ - ARKlass#association_ids
8
+ - ARKlass#associations=
9
+ - ARKlass#association_ids=
@@ -0,0 +1,53 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{kristjan-st-elsewhere}
8
+ s.version = "0.1.6"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brian Doll", "Kristj\303\241n P\303\251tursson"]
12
+ s.date = %q{2010-05-26}
13
+ s.description = %q{This gem provides has_many_elsewhere, an ActiveRecord class method to support many to many relationships in Rails applications, across multiple database connections.}
14
+ s.email = ["brian@emphaticsolutions.com", "kristjan@gmail.com"]
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ "MIT-LICENSE",
20
+ "README.rdoc",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "changelog.rdoc",
24
+ "kristjan-st-elsewhere.gemspec",
25
+ "lib/st-elsewhere.rb",
26
+ "tasks/st-elsewhere_tasks.rake",
27
+ "test/st-elsewhere_test.rb",
28
+ "test/test_helper.rb"
29
+ ]
30
+ s.homepage = %q{http://github.com/kristjan/st-elsewhere}
31
+ s.rdoc_options = ["--charset=UTF-8"]
32
+ s.require_paths = ["lib"]
33
+ s.rubygems_version = %q{1.3.7}
34
+ s.summary = %q{St. Elsewhere supports has_many :through relationships across different databases}
35
+ s.test_files = [
36
+ "test/st-elsewhere_test.rb",
37
+ "test/test_helper.rb"
38
+ ]
39
+
40
+ if s.respond_to? :specification_version then
41
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
45
+ s.add_development_dependency(%q<rr>, [">= 0"])
46
+ else
47
+ s.add_dependency(%q<rr>, [">= 0"])
48
+ end
49
+ else
50
+ s.add_dependency(%q<rr>, [">= 0"])
51
+ end
52
+ end
53
+
@@ -0,0 +1,132 @@
1
+ module StElsewhere
2
+
3
+ # Specifies a one-to-many association across database connections.
4
+ # This is currently an incomplete implementation and does not yet use all of the options supported by has_many
5
+ #
6
+ # The following methods for retrieval and query of collections of associated objects will be added:
7
+ #
8
+ # [collection=objects]
9
+ # Replaces the collections content by deleting and adding objects as appropriate.
10
+ # [collection_singular_ids]
11
+ # Returns an array of the associated objects' ids
12
+ # [collection_singular_ids=ids]
13
+ # Replace the collection with the objects identified by the primary keys in +ids+
14
+ # [collection.empty?]
15
+ # Returns +true+ if there are no associated objects.
16
+ # [collection.size]
17
+ # Returns the number of associated objects.
18
+ #
19
+ # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
20
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
21
+ #
22
+ # === Example
23
+ #
24
+ # Example: A Firm class declares <tt>has_many_elsewhere :clients</tt>, which will add:
25
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => ["firm_id = ?", id]</tt>)
26
+ # * <tt>Firm#clients=</tt>
27
+ # * <tt>Firm#client_ids</tt>
28
+ # * <tt>Firm#client_ids=</tt>
29
+ # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
30
+ # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
31
+ #
32
+ # === Supported options
33
+ # [:through]
34
+ # Specifies a Join Model through which to perform the query. You can only use a <tt>:through</tt> query through a
35
+ # <tt>belongs_to</tt> <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
36
+ # [:class_name]
37
+ # Specifies a the class name to use for the association, rather than
38
+ # inferring from the association name.
39
+ #
40
+ # Option examples:
41
+ # has_many_elsewhere :subscribers, :through => :subscriptions
42
+ # has_many_elsewhere :subscribers, :through => :subscriptions, :class_name => 'User'
43
+ def has_many_elsewhere(association_id, options = {}, &extension)
44
+ association_class = (options[:class_name] || association_id.to_s).classify.constantize
45
+ through = options[:through]
46
+ raise ArgumentError.new("You must include :through => association for has_many_elsewhere") if not through
47
+ collection_accessor_methods_elsewhere(association_id, association_class, through)
48
+ end
49
+
50
+ # Dynamically adds all accessor methods for the has_many_elsewhere association
51
+ def collection_accessor_methods_elsewhere(association_id, association_class, through)
52
+ association_singular = association_id.to_s.singularize
53
+ association_plural = association_id.to_s
54
+ through_association_singular = through.to_s.singularize
55
+ my_class = self
56
+ my_foreign_key = self.to_s.foreign_key
57
+ target_association_class = association_class
58
+ target_association_foreign_key = association_class.to_s.foreign_key
59
+
60
+ # Hospital#doctor_ids
61
+ define_method("#{association_singular}_ids") do
62
+ self.send("#{association_plural}").map{|a| a.id}
63
+ end
64
+
65
+ # Hospital#doctors
66
+ define_method("#{association_plural}") do
67
+ through_class = through.to_s.singularize.camelize.constantize
68
+ through_association_ids = self.send("#{through.to_s.singularize}_ids")
69
+ through_associations = through_class.find(through_association_ids)
70
+ through_associations.collect{|through_association| through_association.send("#{association_singular}")} || []
71
+ end
72
+
73
+ # Hospital#doctors=
74
+ define_method("#{association_plural}=") do |new_associations|
75
+ through_class = through.to_s.singularize.camelize.constantize
76
+ current_associations = self.send("#{association_singular}_ids")
77
+ desired_associations = self.class.associations_to_association_ids(new_associations)
78
+
79
+ removed_target_associations = current_associations - desired_associations
80
+ new_target_associations = desired_associations - current_associations
81
+
82
+ self.send("remove_#{association_singular}_associations", through_class, removed_target_associations)
83
+ self.send("add_#{association_singular}_associations", through_class, association_id, new_target_associations)
84
+ end
85
+
86
+ # Hospital#doctor_ids=
87
+ define_method("#{association_singular}_ids=") do |new_association_ids|
88
+ self.send("#{association_plural}=", new_association_ids)
89
+ end
90
+
91
+ # Hospital#remove_doctor_associations (private)
92
+ define_method("remove_#{association_singular}_associations") do |through_class, removed_target_associations|
93
+ association_instances_to_remove =
94
+ through_class.send("find_all_by_#{my_foreign_key}_and_#{target_association_foreign_key}", self.id, removed_target_associations)
95
+ through_class.delete(association_instances_to_remove)
96
+ end
97
+
98
+ # Hospital#add_doctor_associations (private)
99
+ define_method("add_#{association_singular}_associations") do |through_class, association_id, target_association_ids|
100
+ targets_to_add = target_association_class.find(target_association_ids)
101
+ targets_to_add.each do |target_association|
102
+ new_association = through_class.new(my_foreign_key => self.id, target_association_foreign_key => target_association.id)
103
+ new_association.save
104
+ end
105
+ end
106
+
107
+ private "remove_#{association_singular}_associations".to_sym, "add_#{association_singular}_associations".to_sym
108
+
109
+ end
110
+
111
+ def associations_to_association_ids(associations)
112
+ ids = []
113
+ if associations && !associations.empty?
114
+ associations.reject!{|a| a.to_s.empty? }
115
+ association_class = associations.first.class.to_s
116
+ ids = case association_class
117
+ when "String"
118
+ associations.map{|a| a.to_i }
119
+ when "Fixnum"
120
+ associations
121
+ else
122
+ associations.map{|a| a.id}
123
+ end
124
+ end
125
+ ids
126
+ end
127
+
128
+ private :collection_accessor_methods_elsewhere
129
+
130
+ end
131
+
132
+ ActiveRecord::Base.extend StElsewhere
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :foo do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,92 @@
1
+ # These tests are insanely incomplete. Testing a module like this without
2
+ # depending on an encompasing Rails application is quite difficult. These
3
+ # tests use mocks on the objects under test, which feels almost useless.
4
+ # I may consider publishing an accompanyhing Rails application with a test
5
+ # suite that tests this module in a usable state.
6
+
7
+ require 'test_helper'
8
+ require 'st-elsewhere'
9
+
10
+ class Doctor < Struct.new :id, :name
11
+ end
12
+
13
+ class HospitalDoctor < Struct.new :id, :doctor_id, :hospital_id
14
+ attr_accessor :doctor, :hospital
15
+ end
16
+
17
+ class HospitalDean < Struct.new :id, :dean_id, :hospital_id
18
+ attr_accessor :dean, :hospital
19
+ end
20
+
21
+ class Hospital < Struct.new :id, :name
22
+ extend StElsewhere
23
+ has_many_elsewhere :doctors, :through => :hospital_doctors
24
+ has_many_elsewhere :deans, :through => :hospital_deans, :class_name => 'Doctor'
25
+ end
26
+
27
+ class StElsewhereTest < Test::Unit::TestCase
28
+
29
+ def setup
30
+ @hospital = Hospital.new
31
+ @hospital.id = 1
32
+ @hospital.name = "St. Elsewhere"
33
+
34
+ @doctor = Doctor.new
35
+ @doctor.id = 1
36
+ @doctor.name = "Dr. Foo"
37
+
38
+ @doctor2 = Doctor.new
39
+ @doctor2.id = 2
40
+ @doctor2.name = "Dr. Bar"
41
+
42
+ @dean = Doctor.new
43
+ @dean.id = 3
44
+ @dean.name = "Lida Cuddy"
45
+
46
+ @hospital_doctor = HospitalDoctor.new
47
+ @hospital_doctor.id = 1
48
+ @hospital_doctor.doctor_id = @doctor.id
49
+ @hospital_doctor.hospital_id = @hospital.id
50
+
51
+ @hospital_doctor2 = HospitalDoctor.new
52
+ @hospital_doctor2.id = 2
53
+ @hospital_doctor2.doctor_id = @doctor2.id
54
+ @hospital_doctor2.hospital_id = @hospital.id
55
+
56
+ @hospital_dean = HospitalDean.new
57
+ @hospital_dean.id = 1
58
+ @hospital_dean.dean_id = @dean.id
59
+ @hospital_dean.hospital_id = @hospital.id
60
+ end
61
+
62
+ def test_basic_obj_setup
63
+ assert "St. Elsewhere".eql?(@hospital.name)
64
+ assert @hospital.respond_to? :doctors
65
+ assert @hospital.respond_to? :doctors=
66
+ assert @hospital.respond_to? :doctor_ids
67
+ assert @hospital.respond_to? :doctor_ids=
68
+ end
69
+
70
+ def test_basic_functionality
71
+ mock(@hospital_doctor).doctor {@doctor}
72
+ mock(@hospital_doctor2).doctor {@doctor2}
73
+ mock(HospitalDoctor).find([1,2]) {[@hospital_doctor, @hospital_doctor2]}
74
+ mock(@hospital).hospital_doctor_ids {[@hospital_doctor.id, @hospital_doctor2.id]}
75
+
76
+ assert @hospital.doctors.eql?([@doctor, @doctor2])
77
+ end
78
+
79
+ def test_manual_class_name
80
+ assert @hospital.respond_to? :deans
81
+ assert @hospital.respond_to? :deans=
82
+ assert @hospital.respond_to? :dean_ids
83
+ assert @hospital.respond_to? :dean_ids=
84
+
85
+ mock(@hospital_dean).dean {@dean}
86
+ mock(HospitalDean).find([1]) {[@hospital_dean]}
87
+ mock(@hospital).hospital_dean_ids {[@hospital_dean.id]}
88
+
89
+ assert @hospital.deans.eql?([@dean])
90
+ end
91
+
92
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_record'
4
+ require 'rr'
5
+
6
+ class Test::Unit::TestCase
7
+ include RR::Adapters::TestUnit
8
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kristjan-st-elsewhere
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 6
10
+ version: 0.1.6
11
+ platform: ruby
12
+ authors:
13
+ - Brian Doll
14
+ - "Kristj\xC3\xA1n P\xC3\xA9tursson"
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-05-26 00:00:00 -07:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: rr
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 3
31
+ segments:
32
+ - 0
33
+ version: "0"
34
+ type: :development
35
+ version_requirements: *id001
36
+ description: This gem provides has_many_elsewhere, an ActiveRecord class method to support many to many relationships in Rails applications, across multiple database connections.
37
+ email:
38
+ - brian@emphaticsolutions.com
39
+ - kristjan@gmail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files:
45
+ - README.rdoc
46
+ files:
47
+ - MIT-LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - changelog.rdoc
52
+ - kristjan-st-elsewhere.gemspec
53
+ - lib/st-elsewhere.rb
54
+ - tasks/st-elsewhere_tasks.rake
55
+ - test/st-elsewhere_test.rb
56
+ - test/test_helper.rb
57
+ has_rdoc: true
58
+ homepage: http://github.com/kristjan/st-elsewhere
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options:
63
+ - --charset=UTF-8
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project:
87
+ rubygems_version: 1.3.7
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: St. Elsewhere supports has_many :through relationships across different databases
91
+ test_files:
92
+ - test/st-elsewhere_test.rb
93
+ - test/test_helper.rb