Skip to content
This repository has been archived by the owner on Apr 17, 2018. It is now read-only.

Commit

Permalink
Many-to-many w/ semi-anonymous resource
Browse files Browse the repository at this point in the history
  • Loading branch information
bernerdschaefer committed Jun 17, 2008
1 parent df3233e commit 574b0f6
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 48 deletions.
1 change: 1 addition & 0 deletions lib/dm-core/associations.rb
Expand Up @@ -91,6 +91,7 @@ def has(cardinality, name, options = {})
end

klass = options[:max] == 1 ? OneToOne : OneToMany
klass = ManyToMany if options[:through] == DataMapper::Resource
relationship = klass.setup(options.delete(:name), self, options)

# Please leave this in - I will release contextual serialization soon
Expand Down
96 changes: 67 additions & 29 deletions lib/dm-core/associations/many_to_many.rb
@@ -1,3 +1,4 @@
require File.join(File.dirname(__FILE__), "one_to_many")
module DataMapper
module Associations
module ManyToMany
Expand All @@ -11,46 +12,83 @@ def self.setup(name, model, options = {})
assert_kind_of 'model', model, Resource::ClassMethods
assert_kind_of 'options', options, Hash

raise NotImplementedError, 'many to many relationships not ready yet'

repository_name = model.repository.name

model.class_eval <<-EOS, __FILE__, __LINE__
def #{name}(query = {})
#{name}_association.all(query)
end
# TODO: add accessor/mutator to model with class_eval
def #{name}=(children)
#{name}_association.replace(children)
end
model.relationships(repository_name)[name] = Relationship.new(
name,
repository_name,
model.name,
options.fetch(:class_name, Extlib::Inflection.classify(name)),
options
)
end
private
class Proxy
include Assertions
def #{name}_association
@#{name}_association ||= begin
unless relationship = model.relationships(#{repository_name.inspect})[#{name.inspect}]
raise ArgumentError, 'Relationship #{name.inspect} does not exist'
end
association = Proxy.new(relationship, self)
parent_associations << association
association
end
end
EOS

opts = options.dup
opts.delete(:through)
opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
opts[:parent_model] = model.name
opts[:repository_name] = repository_name
opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
opts[:parent_key] = opts[:parent_key]
opts[:child_key] = opts[:child_key]

names = [opts[:child_model], opts[:parent_model]].sort!

instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not ].include?(m) }
class_name = Extlib::Inflection.pluralize(names[0]) + names[1]
storage_name = Extlib::Inflection.tableize(class_name)

def save
raise NotImplementedError
end
opts[:near_relationship_name] = storage_name.to_sym

def kind_of?(klass)
# TODO: uncomment once proxy target method defined
super # || child.kind_of?(klass)
end
model.has 1.0/0, storage_name.to_sym
model.relationships(repository_name)[name] = RelationshipChain.new( opts )

def respond_to?(method, include_private = false)
# TODO: uncomment once proxy target method defined
super # || child.respond_to?(method)
unless Object.const_defined?(class_name)
resource = DataMapper::Resource.new(storage_name)
resource.class_eval <<-EOS, __FILE__, __LINE__
def self.name; #{class_name.inspect} end
EOS
names.each do |name|
name = Extlib::Inflection.underscore(name)
resource.class_eval <<-EOS, __FILE__, __LINE__
property :#{name}_id, Integer, :key => true
belongs_to :#{name}
EOS
end
Object.const_set(class_name, resource)
end
end

private

def initialize
raise NotImplementedError
class Proxy < DataMapper::Associations::OneToMany::Proxy

def <<(resource)
remote_relationship = @relationship.send(:remote_relationship)
resource.save if resource.new_record?
through = @relationship.child_model.new(
@relationship.child_key.key.first.name => @relationship.parent_key.key.first.get(@parent),
remote_relationship.child_key.key.first.name => remote_relationship.parent_key.key.first.get(resource)
)
@parent.send(@relationship.send(:instance_variable_get, :@near_relationship_name)) << through
end

def save
end

def assert_mutable
end

end # class Proxy
end # module ManyToMany
end # module Associations
Expand Down
31 changes: 29 additions & 2 deletions lib/dm-core/associations/one_to_many.rb
Expand Up @@ -40,14 +40,41 @@ def #{name}_association

model.relationships(repository_name)[name] = if options.has_key?(:through)
opts = options.dup
opts[:child_model_name] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
opts[:parent_model_name] = model.name

opts[:child_model] ||= opts.delete(:class_name) || Extlib::Inflection.classify(name)
opts[:parent_model] = model.name
opts[:repository_name] = repository_name
opts[:near_relationship_name] = opts.delete(:through)
opts[:remote_relationship_name] ||= opts.delete(:remote_name) || name
opts[:parent_key] = opts[:parent_key]
opts[:child_key] = opts[:child_key]

if opts[:near_relationship_name] == DataMapper::Resource
names = [opts[:child_model], opts[:parent_model]].sort!

class_name = Extlib::Inflection.pluralize(names[0]) + names[1]
storage_name = Extlib::Inflection.tableize(class_name)

opts[:near_relationship_name] = storage_name.to_sym

model.has 1.0/0, storage_name.to_sym

unless Object.const_defined?(class_name)
resource = DataMapper::Resource.new(storage_name)
resource.class_eval <<-EOS, __FILE__, __LINE__
def self.name; #{class_name.inspect} end
EOS
names.each do |name|
name = Extlib::Inflection.underscore(name)
resource.class_eval <<-EOS, __FILE__, __LINE__
property :#{name}_id, Integer, :key => true
belongs_to :#{name}
EOS
end
Object.const_set(class_name, resource)
end
end

RelationshipChain.new( opts )
else
Relationship.new(
Expand Down
4 changes: 2 additions & 2 deletions lib/dm-core/associations/one_to_one.rb
Expand Up @@ -38,8 +38,8 @@ def #{name}_association

model.relationships(repository_name)[name] = if options.has_key?(:through)
RelationshipChain.new(
:child_model_name => options.fetch(:class_name, Extlib::Inflection.classify(name)),
:parent_model_name => model.name,
:child_model => options.fetch(:class_name, Extlib::Inflection.classify(name)),
:parent_model => model.name,
:repository_name => repository_name,
:near_relationship_name => options[:through],
:remote_relationship_name => options.fetch(:remote_name, name),
Expand Down
15 changes: 7 additions & 8 deletions lib/dm-core/associations/relationship.rb
Expand Up @@ -23,7 +23,6 @@ def child_key
child_model.property(property_name, parent_property.type, attributes)
end
end

PropertySet.new(child_key)
end
end
Expand All @@ -41,11 +40,11 @@ def parent_key
end

def parent_model
find_const(@parent_model_name)
Class === @parent_model ? @parent_model : find_const(@parent_model)
end

def child_model
find_const(@child_model_name)
Class === @child_model ? @child_model : find_const(@child_model)
end

# @private
Expand Down Expand Up @@ -81,11 +80,11 @@ def attach_parent(child, parent)
# and parent_properties refer to the PK. For more information:
# http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html
# I wash my hands of it!
def initialize(name, repository_name, child_model_name, parent_model_name, options = {})
def initialize(name, repository_name, child_model, parent_model, options = {})
assert_kind_of 'name', name, Symbol
assert_kind_of 'repository_name', repository_name, Symbol
assert_kind_of 'child_model_name', child_model_name, String
assert_kind_of 'parent_model_name', parent_model_name, String
assert_kind_of 'child_model', child_model, String, Class
assert_kind_of 'parent_model', parent_model, String, Class

if child_properties = options[:child_key]
assert_kind_of 'options[:child_key]', child_properties, Array
Expand All @@ -97,10 +96,10 @@ def initialize(name, repository_name, child_model_name, parent_model_name, optio

@name = name
@repository_name = repository_name
@child_model_name = child_model_name
@child_model = child_model
@child_properties = child_properties # may be nil
@query = options.reject { |k,v| OPTIONS.include?(k) }
@parent_model_name = parent_model_name
@parent_model = parent_model
@parent_properties = parent_properties # may be nil
@options = options
end
Expand Down
8 changes: 4 additions & 4 deletions lib/dm-core/associations/relationship_chain.rb
Expand Up @@ -3,7 +3,7 @@ module Associations
class RelationshipChain < Relationship
OPTIONS = [
:repository_name, :near_relationship_name, :remote_relationship_name,
:child_model_name, :parent_model_name, :parent_key, :child_key,
:child_model, :parent_model, :parent_key, :child_key,
:min, :max
]

Expand Down Expand Up @@ -47,7 +47,7 @@ def remote_relationship
end

def grandchild_model
find_const(@child_model_name)
Class === @child_model ? @child_model : find_const(@child_model)
end

def initialize(options)
Expand All @@ -58,8 +58,8 @@ def initialize(options)
@repository_name = options.fetch(:repository_name)
@near_relationship_name = options.fetch(:near_relationship_name)
@remote_relationship_name = options.fetch(:remote_relationship_name)
@child_model_name = options.fetch(:child_model_name)
@parent_model_name = options.fetch(:parent_model_name)
@child_model = options.fetch(:child_model)
@parent_model = options.fetch(:parent_model)
@parent_properties = options.fetch(:parent_key)
@child_properties = options.fetch(:child_key)

Expand Down
5 changes: 3 additions & 2 deletions lib/dm-core/resource.rb
Expand Up @@ -7,8 +7,8 @@ module Resource
def self.new(default_name, &b)
x = Class.new
x.send(:include, self)
x.instance_variable_set(:@storage_names, Hash.new { |h,k| h[k] = repository(k).adapter.resource_naming_convention.call(default_name) })
x.instance_eval(&b)
x.instance_variable_set(:@storage_names, Hash.new { |h,k| h[k] = default_name })
x.instance_eval(&b) if block_given?
x
end

Expand All @@ -19,6 +19,7 @@ def self.new(default_name, &b)
# @private
def self.included(model)
model.extend ClassMethods
model.const_set('Resource', self) unless model.const_defined?('Resource')
descendants << model
end

Expand Down
92 changes: 92 additions & 0 deletions spec/integration/associations/many_to_many_spec.rb
@@ -0,0 +1,92 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
require 'pp'

describe "ManyToMany" do
before(:all) do

class Editor
include DataMapper::Resource

def self.default_repository_name; ADAPTER end

property :id, Integer, :serial => true
property :name, String

has n, :books, :through => Resource
end

class Book
include DataMapper::Resource

def self.default_repository_name; ADAPTER end

property :id, Serial
property :title, String

has n, :editors, :through => Resource
end

adapter = repository(ADAPTER).adapter
adapter.execute("CREATE TABLE books_editors (book_id INT, editor_id INT)")
adapter.execute("INSERT INTO books_editors (book_id, editor_id) VALUES (1, 1)")
adapter.execute("INSERT INTO books_editors (book_id, editor_id) VALUES (2, 1)")
adapter.execute("INSERT INTO books_editors (book_id, editor_id) VALUES (1, 2)")

[Book, Editor].each { |k| k.auto_migrate!(ADAPTER) }

repository(ADAPTER) do
Book.create!(:title => "Dubliners")
Book.create!(:title => "Portrait of the Artist as a Young Man")
Book.create!(:title => "Ulysses")
Editor.create!(:name => "Jon Doe")
Editor.create!(:name => "Jane Doe")
end

end

it "should correctly link records" do
repository(ADAPTER) do
Editor.get(1).books.size.should == 2
Editor.get(2).books.size.should == 1
Book.get(1).editors.size.should == 2
Book.get(2).editors.size.should == 1
end
end

it "should be able to have associated objects manually added" do
repository(ADAPTER) do
book = Book.get(3)
# book.editors.size.should == 0

be = BooksEditor.new(:book_id => book.id, :editor_id => 2)
book.books_editors << be
book.save

book.reload
book.editors.size.should == 1
end
end

it "should automatically added necessary through class" do
repository(ADAPTER) do
book = Book.get(3)
book.editors << Editor.get(1)
book.editors << Editor.new(:name => "Jimmy John")
book.save
book.editors.size.should == 3
end
repository(ADAPTER) do
Book.get(3).editors.size.should == 3
end
end

it "should react correctly to a new record" do
repository(ADAPTER) do
book = Book.new(:title => "Finnegan's Wake")
book.editors << Editor.get(2)
book.save
book.editors.size.should == 1
Editor.get(2).books.size.should == 3
end
end
end
2 changes: 1 addition & 1 deletion spec/unit/resource_spec.rb
Expand Up @@ -372,7 +372,7 @@ def self.default_storage_name

describe "anonymity" do
it "should require a default storage name and accept a block" do
pluto = DataMapper::Resource.new("planet") do
pluto = DataMapper::Resource.new("planets") do
property :name, String, :key => true
end

Expand Down

0 comments on commit 574b0f6

Please sign in to comment.