From 8281ea076289903f2b025ca450962c72c52cf1d8 Mon Sep 17 00:00:00 2001 From: "Wayne E. Seguin" Date: Tue, 5 Feb 2008 10:06:38 -0500 Subject: [PATCH 1/3] Revert "Removed new relationships code." This reverts commit c9567c39c9c2d9a70097c06cac760af749d2e964. --- lib/sequel_model.rb | 2 +- lib/sequel_model/relationships.rb | 122 +++++++++++++++ lib/sequel_model/relationships/block.rb | 30 ++++ lib/sequel_model/relationships/has_many.rb | 26 ++++ lib/sequel_model/relationships/has_one.rb | 26 ++++ lib/sequel_model/relationships/join_table.rb | 91 ++++++++++++ .../relationships/relationship.rb | 94 ++++++++++++ lib/sequel_model/relationships/scoping.rb | 60 ++++++++ .../abstract_relationship_spec.rb | 102 +++++++++++++ spec/relationships/join_table_spec.rb | 139 ++++++++++++++++++ spec/relationships_spec.rb | 83 +++++++++++ 11 files changed, 774 insertions(+), 1 deletion(-) create mode 100644 lib/sequel_model/relationships.rb create mode 100644 lib/sequel_model/relationships/block.rb create mode 100644 lib/sequel_model/relationships/has_many.rb create mode 100644 lib/sequel_model/relationships/has_one.rb create mode 100644 lib/sequel_model/relationships/join_table.rb create mode 100644 lib/sequel_model/relationships/relationship.rb create mode 100644 lib/sequel_model/relationships/scoping.rb create mode 100644 spec/relationships/abstract_relationship_spec.rb create mode 100644 spec/relationships/join_table_spec.rb create mode 100644 spec/relationships_spec.rb diff --git a/lib/sequel_model.rb b/lib/sequel_model.rb index 9d3d0ff..8863e80 100644 --- a/lib/sequel_model.rb +++ b/lib/sequel_model.rb @@ -233,7 +233,7 @@ class Model # TODO: add relationships when complete: files = %w[ base hooks record schema relations - caching plugins validations + caching plugins validations relationships ] dir = File.join(File.dirname(__FILE__), "sequel_model") files.each {|f| require(File.join(dir, f))} diff --git a/lib/sequel_model/relationships.rb b/lib/sequel_model/relationships.rb new file mode 100644 index 0000000..45710e6 --- /dev/null +++ b/lib/sequel_model/relationships.rb @@ -0,0 +1,122 @@ +files = %w{ scoping relationship block join_table } +dir = File.join(File.dirname(__FILE__), "relationships") +files.each {|f| require(File.join(dir, f))} + +# = Sequel Relationships +# Database modelling is generally done with an ER (Entity Relationship) diagram. +# Shouldn't ORM's facilitate simlilar specification? + +# class Post < Sequel::Model +# relationships do +# # Specify the relationships that exist with the User model (users table) +# # These relationships are precisely the ER diagram connecting arrows. +# end +# end + +# +# = Relationships +# +# are specifications of the ends of the ER diagrams connectors that are touching +# the current model. +# +# one_to_one, has_one +# many_to_one, belongs_to +# many_to_many, has_many + +# ?parameters may be :zero, :one, :many which specifies the cardinality of the connection + +# Example: +# class Post < Sequel::Model +# relationships do +# has :one, :blog, :required => true, :normalized => false # uses a blog_id field, which cannot be null, in the Post model +# has :one, :account # uses a join table called accounts_posts to link the post with it's account. +# has :many, :comments # uses a comments_posts join table +# has :many, :authors, :required => true # authors_posts join table, requires at least one author +# end +# end +# +# +# Relationship API Details +# + +# +# == belongs_to +# + +# Defines an blog and blog= method +# belongs_to :blog + +# Same, but uses "b_id" as the blog's id field. +# belongs_to :blog, :key => :b_id + +# has_many :comments +# * Defines comments method which will query the join table appropriately. +# * Checks to see if a "comments_posts" join table exists (alphabetical order) +# ** If it does not exist, will create the join table. +# ** If options are passed in these will be used to further define the join table. + + +# Benefits: +# * Normalized DB +# * Easy to define join objects +# * Efficient queries, database gets to use indexed fields (pkeys) instead of a string field and an id. +# +# For example, polymorphic associations now become: +# [user] 1-* [addresses_users] *-1 [addresses] +# [companies] 1-* [addresses_companies] *-1 [addresses] +# [clients] 1-* [addresses_clients] *-1 [addresses] +# it is automatically polymorphic by specifying the has relationship inside the 2User and Company tables to addresses. Addresses themselves don't care. so we have by default polymorphism. +# If you need to talk about a 'Company Address' then you can subclass, CompanyAddress < Address and do has :many, :company_addresses + +module Sequel + + class Model + + class << self + @relationships = [] + + # has arity, model + # has :one, :blog, :required => true # blog_id field, cannot be null + # has :one, :account # account_id field + # has :many, :comments # comments_posts join table + # has :many, :comment # comments_posts join table + def has(arity, relation, options = {}) + # Create and store the relationship + case arity + when :one + @relationships << HasOne.new(self, relation, options) + when :many + @relationships << HasMany.new(self, relation, options) + else + raise Sequel::Error, "Arity must be specified {:one, :many}." + end + + #unless normalized + # :required => true # The relationship must be populated to save + # can only be used with normalized => false : + #end + # save the relationship + end + + # the proxy methods has_xxx ... , simply pass thru to to has :xxx, ... + def has_one(relation, options = {}) + has :one, relation, options + end + + def has_many(relation, options = {}) + has :many, relation, options + end + + def belongs_to(relation, options = {}) + @relationships << BelongsTo.new(self, relation, options) + end + + #def primary_key_string + # "#{self.to_s.tableize.singularize}_id" + #end + + end + + end # Model + +end # Sequel diff --git a/lib/sequel_model/relationships/block.rb b/lib/sequel_model/relationships/block.rb new file mode 100644 index 0000000..a69e51b --- /dev/null +++ b/lib/sequel_model/relationships/block.rb @@ -0,0 +1,30 @@ +module Sequel + class Model + + # Creates the relationships block which helps you organize your relationships in your model + # + # class Post + # relationships do + # has :many, :comments + # end + # end + def self.relationships(&block) + RelationshipsBlock::Generator.new(self, &block) if block_given? + @relationships + end + + module RelationshipsBlock + class Generator + def initialize(model_class, &block) + @model_class = model_class + instance_eval(&block) + end + + def method_missing(method, *args) + @model_class.send(method, *args) + end + end + end + + end +end diff --git a/lib/sequel_model/relationships/has_many.rb b/lib/sequel_model/relationships/has_many.rb new file mode 100644 index 0000000..27bc04e --- /dev/null +++ b/lib/sequel_model/relationships/has_many.rb @@ -0,0 +1,26 @@ +module Sequel + class Model + class HasMany < Relationship + + def arity ; :many ; end + + # Post.comments.create(:body => "") + def create(*args) + self.<< @destination.create(*args) + end + + # Post.comments << @comment + # inserts the class into the join table + # sets the other's foreign key field if options[:simple] + def <<(other) + other.save if other.new? + # add the other object to the relationship set by inserting into the join table + end + + def define_relationship_accessor(options = {}) + klass.class_eval "def #{@relation} ; #{reader(options[:type])} ; end" + end + + end + end +end \ No newline at end of file diff --git a/lib/sequel_model/relationships/has_one.rb b/lib/sequel_model/relationships/has_one.rb new file mode 100644 index 0000000..5c60e98 --- /dev/null +++ b/lib/sequel_model/relationships/has_one.rb @@ -0,0 +1,26 @@ +module Sequel + class Model + class HasOne < Relationship + + def arity ; :one ; end + + # Post.author = @author + def set(other) + other.save if other.new? + unless options[:type] == :simple + # store in foreign key of other table + else + # store in join table + end + end + + def define_relationship_accessor(options = {}) + klass.class_eval "def #{@relation} ; #{reader(options[:type])} ; end" + klass.class_eval "def #{@relation}=(value) ; #{writer(options[:type])} ; end" + end + + end + + class BelongsTo < HasOne ; end + end +end \ No newline at end of file diff --git a/lib/sequel_model/relationships/join_table.rb b/lib/sequel_model/relationships/join_table.rb new file mode 100644 index 0000000..85ea592 --- /dev/null +++ b/lib/sequel_model/relationships/join_table.rb @@ -0,0 +1,91 @@ +module Sequel + class Model + + # Handles join tables. + # Parameters are the first class and second class: + # + # @join_table = JoinTable.new :post, :comment + # + # The join table class object is available via + # + # @join_table.class #=> PostComment + # + class JoinTable + + attr_accessor :join_class + attr_accessor :source + attr_accessor :destination + + def self.keys(klass) + singular_klass = Inflector.singularize(klass.table_name) + [klass.primary_key].flatten.map do |key| + [ singular_klass, key.to_s ].join("_") + end + end + + def initialize(source, destination) + @source = Inflector.constantize(Inflector.classify(source)) + @destination = Inflector.constantize(Inflector.classify(destination)) + + # Automatically Define the JoinClass if it does not exist + instance_eval <<-JOINCLASS + unless defined?(::#{@source}#{@destination}) + @join_class = + class ::#{@source}#{@destination} < Sequel::Model + set_primary_key [:#{(self.class.keys(@source) + self.class.keys(@destination)).join(", :")}] + end + else + @join_class = ::#{@source}#{@destination} + end + JOINCLASS + end + + # Outputs the join table name + # which is sorted alphabetically with each table name pluralized + # Examples: + # join_table(user, post) #=> :posts_users + # join_table(users, posts) #=> :posts_users + def name + [@source.table_name.to_s, @destination.table_name.to_s].sort.join("_") + end + + def create(hash = {}) + @join_class.new(hash).save + end + + # creates a join table + def create_table + if !exists? + # tablename_key1, tablename_key2,... + # TODO: Inflect!, define a method to return primary_key as an array + instance_eval <<-JOINTABLE + db.create_table name.to_sym do + #{@source.primary_key_def.reverse.join(" :#{Inflector.singularize(@source.table_name)}_")}, :null => false + #{@destination.primary_key_def.reverse.join(" :#{Inflector.singularize(@destination.table_name)}_")}, :null => false + end + JOINTABLE + true + else + false + end + end + + # drops the the table if it exists and creates a new one + def create_table! + db.drop_table name if exists? + create_table + end + + # returns true if exists, false if not + def exists? + self.db[name.to_sym].table_exists? + end + + def db + @source.db + end + + end + + end +end diff --git a/lib/sequel_model/relationships/relationship.rb b/lib/sequel_model/relationships/relationship.rb new file mode 100644 index 0000000..96f4ba3 --- /dev/null +++ b/lib/sequel_model/relationships/relationship.rb @@ -0,0 +1,94 @@ +module Sequel + class Model + # Manages relationships between to models + # + # HasMany.new Post, :comments + # HasOne.new Post, :author, :class => "User" + # BelongsTo.new Comment, :post + # @has_one = HasOne.new(Post, :author, :class => 'User').create + class Relationship + + attr_reader :klass, :relation, :options, :join_table #, :arity + + def initialize(klass, relation, options = {}) + @klass = klass + @relation = relation + @options = options + setup options + end + + def setup(options = {}) + setup_join_table(options) + define_relationship_accessor(options) + end + + def setup_join_table(options = {}) + @join_table = JoinTable.new(self.klass.table_name, relation.to_s.pluralize, options) + @join_table.send((@join_table.exists? && options[:force] == true) ? :create_table! : :create_table) + end + + def relation_class + Inflector.constantize(options[:class] ||= Inflector.classify(@relation)) + end + + def define_relationship_accessor(options = {}) + if arity == :one + klass.class_eval "def #{@relation} ; #{reader(options[:type])} ; end" + else + klass.class_eval "def #{@relation} ; #{reader(options[:type])} ; end" + end + end + + private + + def reader(type = nil) + [:embeded, :foreign].include?(type) ? foreign_reader : join_reader + end + + def foreign_reader + "self.dataset.select(:#{relation.to_s.pluralize}.all)." << + "join(:#{join_table.name}, :#{@klass.to_s.foreign_key} => :id)." << + "join(:#{@relation.to_s.pluralize}, :id => :#{@relation.to_s.classify.foreign_key})." << + "filter(:#{klass.to_s.tableize}__id => self.id)" + end + + def join_reader + # The 'general' idea: + #"self.dataset.select(:#{relation.to_s.pluralize}.all)" << + #"join(:#{join_table.name}, :#{table_name.to_s.singularize}_#{join_table.primary_key} => :#{primary_key})" << + #"join(:#{relation.to_s.pluralize}, :#{relation.primary_key} => :#{relation.to_s.pluralize}_#{relation.primary_key})" << + #"where(:#{table_name}__id => self.#{primary_key.to_s})" + + # TEMPORARY, getting the simple case working: + "self.dataset.select(:#{relation.to_s.pluralize}.all)." << + "join(:#{join_table.name}, :#{@klass.to_s.foreign_key} => :id)." << + "join(:#{@relation.to_s.pluralize}, :id => :#{@relation.to_s.classify.foreign_key})." << + "filter(:#{klass.to_s.tableize}__id => self.id)" + end + + def writer(type = nil) + [:embeded, :foreign].include?(type) ? foreign_writer : join_writer + end + + # insert into foreign table + # Post: has :one, :author + # @post.author = @author + # + # Post: has :many, :comments + # @post.comments << @comment + def embeded_writer + "@source" + end + + # insert into join table + # eg CommentPost.create(key1,key2) + def join_writer + "@join_table.create(@source.id,@destination.id)" + end + + end + + + end + +end diff --git a/lib/sequel_model/relationships/scoping.rb b/lib/sequel_model/relationships/scoping.rb new file mode 100644 index 0000000..679e0f1 --- /dev/null +++ b/lib/sequel_model/relationships/scoping.rb @@ -0,0 +1,60 @@ +# Authors: +# Mike Ferrier (http://www.mikeferrier.ca) +# Hampton Catlin (http://www.hamptoncatlin.com) + +module ScopedStruct + + module ClassMethods + def scope(scope_name, &block) + MethodCarrier.set_scoped_methods(scope_name, block) + self.extend MethodCarrier + self.send(:define_method, scope_name) do + ProxyObject.new(self, scope_name) + end + end + end + + class ProxyObject + def initialize(parent, scope_name) + @parent, @scope_name = parent, scope_name + end + + def method_missing(name, *args, &block) + @parent.send(@scope_name.to_s + "_" + name.to_s, *args, &block) + end + end + + module MethodCarrier + def self.extend_object(base) + @@method_names.each do |method_name| + base.class_eval %Q( + alias #{@@scope_name + '_' + method_name} #{method_name} + undef #{method_name} + ) + end + end + + def self.set_scoped_methods(scope_name, method_declarations) + raise SyntaxError.new("No block passed to scope command.") if method_declarations.nil? + @@scope_name = scope_name.to_s + @@method_names = extract_method_names(method_declarations).collect{|m| m.to_s} + raise SyntaxError.new("No methods defined in scope block.") unless @@method_names.any? + method_declarations.call + end + + def self.extract_method_names(method_declarations) + cls = BlankSlate.new + original_methods = cls.methods + cls.extend(Module.new(&method_declarations)) + cls.methods - original_methods + end + + # Jim Weirich's BlankSlate class from http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc + # We use a slightly modified version of it to figure out what methods were defined in the scope block + class BlankSlate + instance_methods.each { |m| undef_method m unless m =~ /^(__|methods|extend)/ } + end + end +end + +Object.extend(ScopedStruct::ClassMethods) diff --git a/spec/relationships/abstract_relationship_spec.rb b/spec/relationships/abstract_relationship_spec.rb new file mode 100644 index 0000000..4138a37 --- /dev/null +++ b/spec/relationships/abstract_relationship_spec.rb @@ -0,0 +1,102 @@ +require File.join(File.dirname(__FILE__), "../spec_helper") + +describe Sequel::Model::AbstractRelationship do + + describe "intance methods" do + + before :each do + class Post < Sequel::Model(:posts); end + class People < Sequel::Model(:people); end + class Comment < Sequel::Model(:comments); end + @one = Sequel::Model::HasOneRelationship.new Post, :author, {:class => "People"} + @many = Sequel::Model::HasManyRelationship.new Post, :comments, {:force => true} + @belong = Sequel::Model::BelongsToRelationship.new Post, :author, {:class => "People"} + @join_table = mock(Sequel::Model::JoinTable) + end + + describe "create" do + it "should call the create join table method" do + @one.should_receive(:create_join_table).and_return(true) + @one.should_receive(:define_relationship_accessor) + @one.create + end + end + + describe "create_join_table" do + before :each do + @one.stub!(:define_accessor) + @many.stub!(:define_accessor) + end + + it "should create the table if it doesn't exist" do + Post.should_receive(:table_name).and_return('posts') + Sequel::Model::JoinTable.should_receive(:new).with('posts', 'authors').and_return(@join_table) + @join_table.should_receive(:exists?).and_return(false) + @join_table.should_receive(:create) + @one.create_join_table + @one.join_table.should == @join_table + end + + it "should force create the table when the option is specified" do + Post.should_receive(:table_name).and_return('posts') + Sequel::Model::JoinTable.should_receive(:new).with('posts', 'comments').and_return(@join_table) + @join_table.should_receive(:exists?).and_return(true) + @join_table.should_receive(:create!) + @many.create_join_table + @many.join_table.should == @join_table + end + end + + describe "define_relationship_accessor" do + + describe "reader" do + + it "should return a dataset for a has :one relationship" do + @one.stub!(:create_table) + @one.should_receive(:join_table).and_return(@join_table) + @join_table.should_receive(:name).and_return(:authors_posts) + @one.define_relationship_accessor + @post = Post.new(:id => 1) + @post.author.sql.should == "SELECT authors.* FROM posts INNER JOIN authors_posts ON (authors_posts.post_id = posts.id) INNER JOIN authors ON (authors.id = authors_posts.author_id) WHERE (posts.id = #{@post.id})" + end + + it "should return a dataset for a has :many relationship" do + @many.should_receive(:join_table).and_return(@join_table) + @join_table.should_receive(:name).and_return(:posts_comments) + @many.define_relationship_accessor + @post = Post.new(:id => 1) + @post.comments.sql.should == "SELECT comments.* FROM posts INNER JOIN posts_comments ON (posts_comments.post_id = posts.id) INNER JOIN comments ON (comments.id = posts_comments.comment_id) WHERE (posts.id = #{@post.id})" + end + + end + + describe "writer" do + + it "should define a writer method 'relation=' for the relation model when the relationship is has :one" do + + end + + it "should define the append method '<<' for the relation model when the relationship is has :many" do + + end + + end + + end + + describe "arity" do + it "should return :one when an instance of HasOneRelationship" do + @one.arity.should == :one + end + + it "should return :many when an instance of HasManyRelationship" do + @many.arity.should == :many + end + + it "should return :one when an instance of BelongsToRelationship" do + @belong.arity.should == :one + end + end + + end +end diff --git a/spec/relationships/join_table_spec.rb b/spec/relationships/join_table_spec.rb new file mode 100644 index 0000000..9ae2208 --- /dev/null +++ b/spec/relationships/join_table_spec.rb @@ -0,0 +1,139 @@ +__END__ + + +describe Sequel::Model::JoinTable do + + describe "class methods" do + + before(:all) do + class Person < Sequel::Model + set_primary_key [:first_name, :last_name, :middle_name] + end + class Address < Sequel::Model + set_primary_key [:street,:suite,:zip] + end + class Monkey < Sequel::Model + # primary key should be :id + end + end + + describe "keys" do + + it "should return an array of the primary keys for a complex primary key" do + # @join_table = Sequel::Model::JoinTable.new :person, :address + Sequel::Model::JoinTable.keys(Person).should eql(["person_first_name", "person_last_name", "person_middle_name"]) + Sequel::Model::JoinTable.keys(Address).should eql(["address_street", "address_suite", "address_zip"]) + Sequel::Model::JoinTable.keys(Monkey).should eql(["monkey_id"]) + end + + end + + end + + describe "instance methods" do + + before(:each) do + class Post < Sequel::Model(:posts); end + class Comment < Sequel::Model(:comments); end + class Article < Sequel::Model(:articles); end + @join_table = Sequel::Model::JoinTable.new :post, :comment + @join_table_plural = Sequel::Model::JoinTable.new :posts, :comments + @join_table_string = Sequel::Model::JoinTable.new "posts", "comments" + @db = mock("db instance") + end + + describe "name" do + + it "should have a proper join table name" do + @join_table.name.should == "comments_posts" + @join_table_plural.name.should == "comments_posts" + @join_table_string.name.should == "comments_posts" + end + + end + + describe "join class" do + + it "should define the join class if it does not exist" do + class Foo < Sequel::Model(:foos); end + class Bar < Sequel::Model(:bars); end + Sequel::Model::JoinTable.new :foos, :bars + defined?(FooBar).should_not be_nil + end + + it "should not redefine the join class if it already exists" do + undef ArticleComment if defined?(ArticleComment) + class ArticleComment < Sequel::Model + set_primary_key :id + end + @join_table = Sequel::Model::JoinTable.new :article, :comment + ArticleComment.primary_key.should == :id + end + + it "should return the join class" do + @join_table.join_class.should eql(PostComment) + end + + end + + describe "exists?" do + + before :each do + @join_table.should_receive(:db).and_return(@db) + @db.should_receive(:[]).with("comments_posts").and_return(@db) + end + + it "should indicate if the table exists" do + @db.should_receive(:table_exists?).and_return(true) + @join_table.exists?.should == true + end + + it "should indicate if the table does not exist" do + @db.should_receive(:table_exists?).and_return(false) + @join_table.exists?.should == false + end + + end + + describe "create" do + + it "should create the table if it doesn't exist" do + @join_table.should_receive(:exists?).and_return(false) + @join_table.should_receive(:db).and_return(@db) + @db.should_receive(:create_table).with(:comments_posts) + @join_table.create.should be_true + end + + it "should fail to create the table if it does exist" do + @join_table.should_receive(:exists?).and_return(true) + @join_table.create.should be_false + end + + end + + describe "create!" do + + it "should force the creation of the table it exists" do + @join_table.should_receive(:exists?).and_return(true) + @join_table.should_receive(:db).and_return(@db) + @db.should_receive(:drop_table).with("comments_posts") + @join_table.should_receive(:create).and_return(true) + @join_table.create!.should be_true + end + + end + + describe "db" do + + it "should have access to the db object" do + class Post; end + + Post.should_receive(:db).and_return(@db) + @join_table.db.should == @db + end + + end + + end + +end diff --git a/spec/relationships_spec.rb b/spec/relationships_spec.rb new file mode 100644 index 0000000..8c73ab2 --- /dev/null +++ b/spec/relationships_spec.rb @@ -0,0 +1,83 @@ +require File.join(File.dirname(__FILE__), "spec_helper") + +__END__ + +# class Post < Sequel::Model +# relationships do +# has :one, :blog, :required => true, :normalized => false # uses a blog_id field, which cannot be null, in the Post model +# has :one, :account # uses a join table called accounts_posts to link the post with it's account. +# has :many, :comments # uses a comments_posts join table +# has :many, :authors, :required => true # authors_posts join table, requires at least one author +# end +# end + +class User < Sequel::Model; end + +describe Sequel::Model, "relationships" do + + describe "has" do + before(:each) do + @one = mock(Sequel::Model::HasOneRelationship) + @many = mock(Sequel::Model::HasManyRelationship) + end + + it "should allow for arity :one with options" do + @one.should_receive(:create) + Sequel::Model::HasOneRelationship.should_receive(:new).with(User, :site, {}).and_return(@one) + User.send(:has, :one, :site) + end + + it "should allow for arity :many with options" do + @many.should_receive(:create) + Sequel::Model::HasManyRelationship.should_receive(:new).with(User, :sites, {}).and_return(@many) + User.send(:has, :many, :sites) + end + + it "should raise an error Sequel::Error, \"Arity must be specified {:one, :many}.\" if arity was not specified." do + lambda { User.send(:has, :so_many, :sites) }.should raise_error Sequel::Error, "Arity must be specified {:one, :many}." + end + end + + describe "has_one" do + it "should pass arguments to has :one" do + User.should_receive(:has).with(:one, :boss, {}).and_return(true) + User.send(:has_one, :boss) + end + end + + describe "has_many" do + it "should pass arguments to has :many" do + User.should_receive(:has).with(:many, :addresses, {}).and_return(true) + User.send(:has_many, :addresses) + end + end + + describe "belongs_to" do + it "should pass arguments to has :one" do + @belongs_to_relationship = mock(Sequel::Model::BelongsToRelationship) + @belongs_to_relationship.should_receive(:create) + Sequel::Model::BelongsToRelationship.should_receive(:new).with(User, :boss, {}).and_return(@belongs_to_relationship) + User.send(:belongs_to, :boss) + end + end + + describe "relationships block" do + + it "should store the relationship" do + User.should_receive(:has).with(:one, :boss).and_return(true) + class User + relationships do + has :one, :boss + end + end + # User.model_relationships.should eql(?) + end + it "should create relationship methods on the model" + it "should allow for has :one relationship" + it "should allow for has :many relationship" + it "should allow for has_one relationship" + it "should allow for has_many relationship" + it "should allow for belongs_to" + end + +end From 838d0b5ea69830544da3fd0cc566b96cf4cf4e7d Mon Sep 17 00:00:00 2001 From: "Wayne E. Seguin" Date: Wed, 6 Feb 2008 16:59:56 -0500 Subject: [PATCH 2/3] Now refactoring join table slightly. We need to delay the setup. --- lib/sequel_model/relationships.rb | 2 +- lib/sequel_model/relationships/join_table.rb | 34 +++++++++++++------ .../relationships/relationship.rb | 3 +- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/sequel_model/relationships.rb b/lib/sequel_model/relationships.rb index 45710e6..62fe2f5 100644 --- a/lib/sequel_model/relationships.rb +++ b/lib/sequel_model/relationships.rb @@ -1,4 +1,4 @@ -files = %w{ scoping relationship block join_table } +files = %w{ scoping relationship has_one has_many block join_table } dir = File.join(File.dirname(__FILE__), "relationships") files.each {|f| require(File.join(dir, f))} diff --git a/lib/sequel_model/relationships/join_table.rb b/lib/sequel_model/relationships/join_table.rb index 85ea592..7cae0c3 100644 --- a/lib/sequel_model/relationships/join_table.rb +++ b/lib/sequel_model/relationships/join_table.rb @@ -15,6 +15,7 @@ class JoinTable attr_accessor :join_class attr_accessor :source attr_accessor :destination + attr_accessor :options def self.keys(klass) singular_klass = Inflector.singularize(klass.table_name) @@ -23,19 +24,30 @@ def self.keys(klass) end end - def initialize(source, destination) - @source = Inflector.constantize(Inflector.classify(source)) - @destination = Inflector.constantize(Inflector.classify(destination)) + def initialize(source, destination, options = {}) + @source = source + @destination = destination + @options = options + end + + def source_class + @source_class ||= Inflector.constantize(Inflector.classify(@source)) + end + + def destination_class + @destination_class ||= Inflector.constantize(Inflector.classify(@destination)) + end + def join_class # Automatically Define the JoinClass if it does not exist instance_eval <<-JOINCLASS - unless defined?(::#{@source}#{@destination}) + unless defined?(::#{source_class}#{destination_class}) @join_class = - class ::#{@source}#{@destination} < Sequel::Model - set_primary_key [:#{(self.class.keys(@source) + self.class.keys(@destination)).join(", :")}] + class ::#{source_class}#{destination_class} < Sequel::Model + set_primary_key [:#{(self.class.keys(source_class) + self.class.keys(destination_class)).join(", :")}] end else - @join_class = ::#{@source}#{@destination} + @join_class = ::#{source_class}#{destination_class} end JOINCLASS end @@ -46,7 +58,7 @@ class ::#{@source}#{@destination} < Sequel::Model # join_table(user, post) #=> :posts_users # join_table(users, posts) #=> :posts_users def name - [@source.table_name.to_s, @destination.table_name.to_s].sort.join("_") + [source_class.table_name.to_s, destination_class.table_name.to_s].sort.join("_") end def create(hash = {}) @@ -60,8 +72,8 @@ def create_table # TODO: Inflect!, define a method to return primary_key as an array instance_eval <<-JOINTABLE db.create_table name.to_sym do - #{@source.primary_key_def.reverse.join(" :#{Inflector.singularize(@source.table_name)}_")}, :null => false - #{@destination.primary_key_def.reverse.join(" :#{Inflector.singularize(@destination.table_name)}_")}, :null => false + #{source_class.primary_key_def.reverse.join(" :#{Inflector.singularize(source_class.table_name)}_")}, :null => false + #{destination_class.primary_key_def.reverse.join(" :#{Inflector.singularize(destination_class.table_name)}_")}, :null => false end JOINTABLE true @@ -82,7 +94,7 @@ def exists? end def db - @source.db + source_class.db end end diff --git a/lib/sequel_model/relationships/relationship.rb b/lib/sequel_model/relationships/relationship.rb index 96f4ba3..1d0afb7 100644 --- a/lib/sequel_model/relationships/relationship.rb +++ b/lib/sequel_model/relationships/relationship.rb @@ -14,7 +14,8 @@ def initialize(klass, relation, options = {}) @klass = klass @relation = relation @options = options - setup options + # TODO: move the setup somewhere else: + #setup options end def setup(options = {}) From 2962821e8d8017ad7a11fcabd8576410ae563b70 Mon Sep 17 00:00:00 2001 From: "Wayne E. Seguin" Date: Sat, 9 Feb 2008 11:03:28 -0500 Subject: [PATCH 3/3] Defined primary_keys_hash --- lib/sequel_model/record.rb | 8 +++++++- lib/sequel_model/relationships/join_table.rb | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/sequel_model/record.rb b/lib/sequel_model/record.rb index d25a245..620ed19 100644 --- a/lib/sequel_model/record.rb +++ b/lib/sequel_model/record.rb @@ -49,7 +49,13 @@ def self.primary_key def self.primary_key_hash(value) {:id => value} end - + + # returns the primary keys and their types as a hash + def self.primary_keys_hash + # TODO: make work for compound primary keys + {:id => "integer"} + end + # Sets primary key, regular and composite are possible. # # == Example: diff --git a/lib/sequel_model/relationships/join_table.rb b/lib/sequel_model/relationships/join_table.rb index 7cae0c3..9351422 100644 --- a/lib/sequel_model/relationships/join_table.rb +++ b/lib/sequel_model/relationships/join_table.rb @@ -72,8 +72,8 @@ def create_table # TODO: Inflect!, define a method to return primary_key as an array instance_eval <<-JOINTABLE db.create_table name.to_sym do - #{source_class.primary_key_def.reverse.join(" :#{Inflector.singularize(source_class.table_name)}_")}, :null => false - #{destination_class.primary_key_def.reverse.join(" :#{Inflector.singularize(destination_class.table_name)}_")}, :null => false + #{source_class.primary_keys_hash.reverse.join(" :#{Inflector.singularize(source_class.table_name)}_")}, :null => false + #{destination_class.primary_keys_hash.reverse.join(" :#{Inflector.singularize(destination_class.table_name)}_")}, :null => false end JOINTABLE true