st-elsewhere 0.1.0
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/MIT-LICENSE +20 -0
- data/README.rdoc +50 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/init.rb +2 -0
- data/lib/st-elsewhere.rb +107 -0
- data/tasks/st-elsewhere_tasks.rake +4 -0
- data/test/st-elsewhere_test.rb +8 -0
- data/test/test_helper.rb +3 -0
- metadata +64 -0
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,50 @@
|
|
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> does not work.
|
14
|
+
|
15
|
+
You'll encounter one of two errors, depending on your setup:
|
16
|
+
* If the schemas are different, you'll see something like: <tt>ActiveRecord::StatementInvalid: Mysql::Error: Table 'appname_transactional.parent_children' doesn't exist</tt>
|
17
|
+
* If the schemas are the same but data only exists in one schema or the other, you'll just get empty relationships.
|
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
|
+
class Hospital < AcitveRecord::Base
|
26
|
+
has_many :hospital_doctors
|
27
|
+
has_many_elsewhere :doctors, :through => :hospital_doctors
|
28
|
+
end
|
29
|
+
|
30
|
+
class HospitalDoctor < ActiveRecord::Base
|
31
|
+
belongs_to :hospital
|
32
|
+
belongs_to :doctor
|
33
|
+
end
|
34
|
+
|
35
|
+
class TransactionalBase < ActiveRecord::Base
|
36
|
+
self.abstract_class = true
|
37
|
+
establish_connection "#{RAILS_ENV}-transactional"
|
38
|
+
end
|
39
|
+
|
40
|
+
class Doctor < TransactionalBase
|
41
|
+
has_many :hospital_doctors
|
42
|
+
has_many :hospitals, :through => :hospital_doctors
|
43
|
+
end
|
44
|
+
|
45
|
+
The following conventional methods are available for Hospital:
|
46
|
+
hospital.doctors, hospital.doctors=, hospital.doctor_ids, hospital.doctor_ids=
|
47
|
+
|
48
|
+
=== Inefficiencies
|
49
|
+
|
50
|
+
<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.
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "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"
|
11
|
+
gem.homepage = "http://github.com/briandoll/st-elsewhere"
|
12
|
+
gem.authors = ["Brian Doll"]
|
13
|
+
# gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
task :test => :check_dependencies
|
29
|
+
|
30
|
+
task :default => :test
|
31
|
+
|
32
|
+
require 'rake/rdoctask'
|
33
|
+
Rake::RDocTask.new do |rdoc|
|
34
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
35
|
+
|
36
|
+
rdoc.rdoc_dir = 'rdoc'
|
37
|
+
rdoc.title = "st-elsewhere #{version}"
|
38
|
+
rdoc.rdoc_files.include('README*')
|
39
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
40
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/init.rb
ADDED
data/lib/st-elsewhere.rb
ADDED
@@ -0,0 +1,107 @@
|
|
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<<(object, ...)]
|
9
|
+
# TODO: Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
10
|
+
# [collection=objects]
|
11
|
+
# Replaces the collections content by deleting and adding objects as appropriate.
|
12
|
+
# [collection_singular_ids]
|
13
|
+
# Returns an array of the associated objects' ids
|
14
|
+
# [collection_singular_ids=ids]
|
15
|
+
# Replace the collection with the objects identified by the primary keys in +ids+
|
16
|
+
# [collection.empty?]
|
17
|
+
# Returns +true+ if there are no associated objects.
|
18
|
+
# [collection.size]
|
19
|
+
# Returns the number of associated objects.
|
20
|
+
#
|
21
|
+
# (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
|
22
|
+
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
|
23
|
+
#
|
24
|
+
# === Example
|
25
|
+
#
|
26
|
+
# Example: A Firm class declares <tt>has_many_elsewhere :clients</tt>, which will add:
|
27
|
+
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => ["firm_id = ?", id]</tt>)
|
28
|
+
# * <tt>Firm#clients=</tt>
|
29
|
+
# * <tt>Firm#client_ids</tt>
|
30
|
+
# * <tt>Firm#client_ids=</tt>
|
31
|
+
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
32
|
+
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
33
|
+
#
|
34
|
+
# === Supported options
|
35
|
+
# [:through]
|
36
|
+
# Specifies a Join Model through which to perform the query. You can only use a <tt>:through</tt> query through a
|
37
|
+
# <tt>belongs_to</tt> <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
|
38
|
+
#
|
39
|
+
# Option examples:
|
40
|
+
# has_many_elsewhere :subscribers, :through => :subscriptions
|
41
|
+
def has_many_elsewhere(association_id, options = {}, &extension)
|
42
|
+
association_class = association_id.to_s.classify.constantize
|
43
|
+
through = options[:through]
|
44
|
+
raise ArgumentError.new("You must include :through => association for has_many_elsewhere") if not through
|
45
|
+
collection_accessor_methods_elsewhere(association_id, association_class, through)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Dynamically adds all accessor methods for the has_many_elsewhere association
|
49
|
+
def collection_accessor_methods_elsewhere(association_id, association_class, through)
|
50
|
+
association_singular = association_id.to_s.singularize
|
51
|
+
association_plural = association_id.to_s
|
52
|
+
through_association_singular = through.to_s.singularize
|
53
|
+
|
54
|
+
# Hospital#doctor_ids
|
55
|
+
define_method("#{association_singular}_ids") do
|
56
|
+
self.send("#{association_plural}").map{|a| a.id}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Hospital#doctors
|
60
|
+
define_method("#{association_plural}") do
|
61
|
+
through_class = through.to_s.singularize.camelize.constantize
|
62
|
+
through_association_ids = self.send("#{through.to_s.singularize}_ids")
|
63
|
+
through_associations = through_class.find(through_association_ids)
|
64
|
+
through_associations.collect{|through_association| through_association.send("#{association_singular}")} || []
|
65
|
+
end
|
66
|
+
|
67
|
+
# Hospital#doctors=
|
68
|
+
define_method("#{association_plural}=") do |new_associations|
|
69
|
+
through_class = through.to_s.singularize.camelize.constantize
|
70
|
+
current_associations = self.send("#{through_association_singular}_ids")
|
71
|
+
removed_associations = current_associations - new_associations
|
72
|
+
new_associations = new_associations - current_associations
|
73
|
+
|
74
|
+
self.send("remove_#{association_singular}_associations", association_class, removed_associations)
|
75
|
+
self.send("add_#{association_singular}_associations", through_class, association_id, new_associations)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Hospital#doctor_ids=
|
79
|
+
define_method("#{association_singular}_ids=") do |new_association_ids|
|
80
|
+
self.send("#{association_plural}=", new_association_ids)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Hospital#remove_doctor_associations (private)
|
84
|
+
define_method("remove_#{association_singular}_associations") do |association_class, associations|
|
85
|
+
associations.each do |association|
|
86
|
+
association_class.delete(association)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Hospital#add_doctor_associations (private)
|
91
|
+
define_method("add_#{association_singular}_associations") do |through_class, association_id, associations|
|
92
|
+
myself = "#{self.class.to_s.downcase}_id"
|
93
|
+
my_buddy = "#{association_singular}_id"
|
94
|
+
associations.each do |association|
|
95
|
+
my_buddy_id = Fixnum.eql?(association.class) ? association : association.id
|
96
|
+
new_association = through_class.new(myself => self.id, my_buddy => my_buddy_id)
|
97
|
+
new_association.save
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private "remove_#{association_singular}_associations".to_sym, "add_#{association_singular}_associations".to_sym
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
private :collection_accessor_methods_elsewhere
|
106
|
+
|
107
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: st-elsewhere
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Doll
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-14 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: This gem provides has_many_elsewhere, an ActiveRecord class method to support many to many relationships in Rails applications, across multiple database connections.
|
17
|
+
email: brian@emphaticsolutions.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- MIT-LICENSE
|
26
|
+
- README.rdoc
|
27
|
+
- Rakefile
|
28
|
+
- VERSION
|
29
|
+
- init.rb
|
30
|
+
- lib/st-elsewhere.rb
|
31
|
+
- tasks/st-elsewhere_tasks.rake
|
32
|
+
- test/st-elsewhere_test.rb
|
33
|
+
- test/test_helper.rb
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://github.com/briandoll/st-elsewhere
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options:
|
40
|
+
- --charset=UTF-8
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.5
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: St. Elsewhere supports has_many :through relationships across different databases
|
62
|
+
test_files:
|
63
|
+
- test/st-elsewhere_test.rb
|
64
|
+
- test/test_helper.rb
|