sequel 1.5.1 → 2.0.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/CHANGELOG CHANGED
@@ -1,3 +1,33 @@
1
+ === 2.0.0 (2008-06-01)
2
+
3
+ * Comprehensive update of all documentation (jeremyevans)
4
+
5
+ * Remove methods deprecated in 1.5.0 (jeremyevans)
6
+
7
+ * Add typecasting on attribute assignment to Sequel::Model objects, optional but enabled by default (jeremyevans)
8
+
9
+ * Returning false in one of the before_ hooks now causes the appropriate method(s) to immediately return false (jeremyevans)
10
+
11
+ * Add remove_all_* association method for *_to_many associations, which removes the association with all currently associated objects (jeremyevans)
12
+
13
+ * Add Model.lazy_load_schema=, when set to true, it loads the schema on first instantiation (jeremyevans)
14
+
15
+ * Add before_validation and after_validation hooks, called whenever the model is validated (jeremyevans)
16
+
17
+ * Add Model.default_foreign_key, a private class method that allows changing the default foreign key that Sequel will use in associations (jeremyevans)
18
+
19
+ * Cache negative lookup when eagerly loading many_to_one associations (jeremyevans)
20
+
21
+ * Make all associations support the :select option, not just many_to_many (jeremyevans)
22
+
23
+ * Allow the use of blocks when eager loading, and add the :eager_block and :allow_eager association options for configuration (jeremyevans)
24
+
25
+ * Add the :graph_join_type, :graph_conditions, and :graph_join_table_conditions association options, used when eager graphing (jeremyevans)
26
+
27
+ * Add AssociationReflection class (subclass of Hash), to make calling a couple of private Model methods unnecessary (jeremyevans)
28
+
29
+ * Change hook methods so that if a tag/method is specified it overwrites an existing hook block with the same tag/method (jeremyevans)
30
+
1
31
  === 1.5.1 (2008-04-30)
2
32
 
3
33
  * Fix Dataset#eager_graph when not all objects have associated objects (jeremyevans)
data/README CHANGED
@@ -16,18 +16,13 @@ You can, however, explicitly set the table name or even the dataset used:
16
16
 
17
17
  class Post < Sequel::Model(:my_posts)
18
18
  end
19
-
20
19
  # or:
21
-
22
20
  Post.set_dataset :my_posts
23
-
24
21
  # or:
25
-
26
22
  Post.set_dataset DB[:my_posts].where(:category => 'ruby')
27
23
 
28
24
  === Resources
29
25
 
30
- * {Project page}[http://code.google.com/p/ruby-sequel/]
31
26
  * {Source code}[http://github.com/jeremyevans/sequel]
32
27
  * {Bug tracking}[http://code.google.com/p/ruby-sequel/issues/list]
33
28
  * {Google group}[http://groups.google.com/group/sequel-talk]
@@ -70,7 +65,7 @@ You can also define a model class that does not have a primary key, but then you
70
65
  A model instance can also be fetched by specifying a condition:
71
66
 
72
67
  post = Post[:title => 'hello world']
73
- post = Post.find{:num_comments < 10}
68
+ post = Post.find(:num_comments < 10)
74
69
 
75
70
  === Iterating over records
76
71
 
@@ -80,8 +75,8 @@ A model class lets you iterate over specific records by acting as a proxy to the
80
75
 
81
76
  You can also manipulate the records in the dataset:
82
77
 
83
- Post.filter{:num_comments < 7}.delete
84
- Post.filter{:title =~ /ruby/}.update(:category => 'ruby')
78
+ Post.filter(:num_comments < 7).delete
79
+ Post.filter(:title.like(/ruby/)).update(:category => 'ruby')
85
80
 
86
81
  === Accessing record values
87
82
 
@@ -126,7 +121,7 @@ You can also supply a block to Model.new and Model.create:
126
121
 
127
122
  === Hooks
128
123
 
129
- You can execute custom code when creating, updating, or deleting records by using hooks. The before_create and after_create hooks wrap record creation. The before_update and after_update wrap record updating. The before_save and after_save wrap record creation and updating. The before_destroy and after_destroy wrap destruction.
124
+ You can execute custom code when creating, updating, or deleting records by using hooks. The before_create and after_create hooks wrap record creation. The before_update and after_update wrap record updating. The before_save and after_save wrap record creation and updating. The before_destroy and after_destroy wrap destruction. The before_validation and after_validation hooks wrap validation.
130
125
 
131
126
  Hooks are defined by supplying a block:
132
127
 
@@ -174,7 +169,7 @@ You can also use the ActiveRecord names for these associations:
174
169
  has_and_belongs_to_many :tags
175
170
  end
176
171
 
177
- many_to_one/belongs_to creates a getter and setter for each model object:
172
+ many_to_one creates a getter and setter for each model object:
178
173
 
179
174
  class Post < Sequel::Model
180
175
  many_to_one :author
@@ -184,7 +179,7 @@ many_to_one/belongs_to creates a getter and setter for each model object:
184
179
  post.author = Author[:name => 'Sharon']
185
180
  post.author
186
181
 
187
- one_to_many/has_many and many_to_many/has_and_belongs_to_many create a getter method, a method for adding an object to the association, and a method for removing an object from the association:
182
+ one_to_many and many_to_many create a getter method, a method for adding an object to the association, a method for removing an object from the association, and a method for removing all associated objected from the association:
188
183
 
189
184
  class Post < Sequel::Model
190
185
  one_to_many :comments
@@ -196,9 +191,11 @@ one_to_many/has_many and many_to_many/has_and_belongs_to_many create a getter me
196
191
  comment = Comment.create(:text=>'hi')
197
192
  post.add_comment(comment)
198
193
  post.remove_comment(comment)
194
+ post.remove_all_comments
199
195
  tag = Tag.create(:tag=>'interesting')
200
196
  post.add_tag(tag)
201
197
  post.remove_tag(tag)
198
+ post.remove_all_tags
202
199
 
203
200
  === Eager Loading
204
201
 
@@ -229,7 +226,7 @@ Associations can be eagerly loaded via .eager and the :eager association option.
229
226
  Post.eager(:person).all
230
227
 
231
228
  # eager is a dataset method, so it works with filters/orders/limits/etc.
232
- Post.filter("topic > 'M'").order(:date).limit(5).eager(:person).all
229
+ Post.filter(:topic > 'M').order(:date).limit(5).eager(:person).all
233
230
 
234
231
  person = Person.first
235
232
  # Eager loading via :eager (will eagerly load the tags for this person's posts)
@@ -273,7 +270,7 @@ The obvious way to add table-wide logic is to define class methods to the model
273
270
 
274
271
  class Post < Sequel::Model
275
272
  def self.posts_with_few_comments
276
- filter{:num_comments < 30}
273
+ filter(:num_comments < 30)
277
274
  end
278
275
 
279
276
  def self.clean_posts_with_few_comments
@@ -285,7 +282,7 @@ You can also implement table-wide logic by defining methods on the dataset:
285
282
 
286
283
  class Post < Sequel::Model
287
284
  def_dataset_method(:posts_with_few_comments) do
288
- filter{:num_comments < 30}
285
+ filter(:num_comments < 30)
289
286
  end
290
287
 
291
288
  def_dataset_method(:clean_posts_with_few_comments) do
@@ -300,7 +297,7 @@ This is the recommended way of implementing table-wide operations, and allows yo
300
297
  Sequel models also provide a short hand notation for filters:
301
298
 
302
299
  class Post < Sequel::Model
303
- subset(:posts_with_few_comments){:num_comments < 30}
300
+ subset(:posts_with_few_comments, :num_comments < 30)
304
301
  subset :invisible, :visible => false
305
302
  end
306
303
 
data/Rakefile CHANGED
@@ -9,20 +9,19 @@ include FileUtils
9
9
  # Configuration
10
10
  ##############################################################################
11
11
  NAME = "sequel"
12
- VERS = "1.5.1"
13
- SEQUEL_CORE_VERS= "1.5.1"
14
- CLEAN.include ["**/.*.sw?", "pkg/*", ".config", "doc/*", "coverage/*"]
15
- RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source"]
12
+ VERS = "2.0.0"
13
+ SEQUEL_CORE_VERS= "2.0.0"
14
+ CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
15
+ RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
16
+ 'Sequel: The Database Toolkit for Ruby: Model Classes', '--main', 'README']
16
17
 
17
18
  ##############################################################################
18
19
  # RDoc
19
20
  ##############################################################################
20
21
  Rake::RDocTask.new do |rdoc|
21
- rdoc.rdoc_dir = "doc/rdoc"
22
+ rdoc.rdoc_dir = "rdoc"
22
23
  rdoc.options += RDOC_OPTS
23
- rdoc.main = "README"
24
- rdoc.title = "Sequel: The Database Toolkit for Ruby: Model Classes"
25
- rdoc.rdoc_files.add ["README", "COPYING", "lib/**/*.rb"]
24
+ rdoc.rdoc_files.add ["README", "COPYING", "doc/*.rdoc", "lib/**/*.rb"]
26
25
  end
27
26
 
28
27
  ##############################################################################
@@ -37,7 +36,7 @@ spec = Gem::Specification.new do |s|
37
36
  s.version = VERS
38
37
  s.platform = Gem::Platform::RUBY
39
38
  s.has_rdoc = true
40
- s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
39
+ s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"] + Dir["doc/*.rdoc"]
41
40
  s.rdoc_options += RDOC_OPTS + ["--exclude", "^(examples|extras)\/"]
42
41
  s.summary = "The Database Toolkit for Ruby: Model Classes"
43
42
  s.description = s.summary
@@ -45,18 +44,8 @@ spec = Gem::Specification.new do |s|
45
44
  s.email = "code@jeremyevans.net"
46
45
  s.homepage = "http://sequel.rubyforge.org"
47
46
  s.required_ruby_version = ">= 1.8.4"
48
-
49
- case RUBY_PLATFORM
50
- when /java/
51
- s.platform = "jruby"
52
- else
53
- s.platform = Gem::Platform::RUBY
54
- end
55
-
56
47
  s.add_dependency("sequel_core", "= #{SEQUEL_CORE_VERS}")
57
-
58
48
  s.files = %w(COPYING README Rakefile) + Dir.glob("{doc,spec,lib}/**/*")
59
-
60
49
  s.require_path = "lib"
61
50
  end
62
51
 
@@ -135,7 +124,7 @@ STATS_DIRECTORIES = [
135
124
 
136
125
  desc "Report code statistics (KLOCs, etc) from the application"
137
126
  task :stats do
138
- require "extra/stats"
127
+ require "../extra/stats"
139
128
  verbose = true
140
129
  CodeStatistics.new(*STATS_DIRECTORIES).to_s
141
130
  end
data/lib/sequel_model.rb CHANGED
@@ -1,82 +1,57 @@
1
1
  require 'sequel_core'
2
+ %w"inflector base hooks record schema association_reflection
3
+ associations caching plugins validations eager_loading".each do |f|
4
+ require "sequel_model/#{f}"
5
+ end
2
6
 
3
7
  module Sequel
4
- class Model
5
- alias_method :model, :class
8
+ # Holds the nameless subclasses that are created with
9
+ # Sequel::Model(), necessary for reopening subclasses with the
10
+ # Sequel::Model() superclass specified.
11
+ @models = {}
12
+
13
+ # Lets you create a Model subclass with its dataset already set.
14
+ # source can be an existing dataset or a symbol (in which case
15
+ # it will create a dataset using the default database with
16
+ # source as the table name.
17
+ #
18
+ # Example:
19
+ # class Comment < Sequel::Model(:something)
20
+ # table_name # => :something
21
+ # end
22
+ def self.Model(source)
23
+ return @models[source] if @models[source]
24
+ klass = Class.new(Model)
25
+ klass.set_dataset(source.is_a?(Dataset) ? source : Model.db[source])
26
+ @models[source] = klass
6
27
  end
7
- end
8
28
 
9
- files = %w[
10
- inflector inflections base hooks record schema associations
11
- caching plugins validations eager_loading deprecated
12
- ]
13
- dir = File.join(File.dirname(__FILE__), "sequel_model")
14
- files.each {|f| require(File.join(dir, f))}
15
-
16
- module Sequel
29
+ # Model has some methods that are added via metaprogramming:
30
+ #
31
+ # * All of the methods in DATASET_METHODS have class methods created that call
32
+ # the Model's dataset with the method of the same name with the given
33
+ # arguments.
34
+ # * All of the methods in HOOKS have class methods created that accept
35
+ # either a method name symbol or an optional tag and a block. These
36
+ # methods run the code as a callback at the specified time. For example:
37
+ #
38
+ # Model.before_save :do_something
39
+ # Model.before_save(:do_something_else){ self.something_else = 42}
40
+ # object = Model.new
41
+ # object.save
42
+ #
43
+ # Would run the object's :do_something method following by the code
44
+ # block related to :do_something_else. Note that if you specify a
45
+ # block, a tag is optional. If the tag is not nil, it will overwrite
46
+ # a previous block with the same tag. This allows hooks to work with
47
+ # systems that reload code.
48
+ # * All of the methods in HOOKS also create instance methods, but you
49
+ # should not override these instance methods.
50
+ # * The following instance_methods all call the class method of the same
51
+ # name: columns, dataset, db, primary_key, str_columns.
17
52
  class Model
18
53
  extend Enumerable
19
54
  extend Associations
20
- # Returns a string representation of the model instance including
21
- # the class name and values.
22
- def inspect
23
- "#<%s @values=%s>" % [self.class.name, @values.inspect]
24
- end
25
-
26
- # Defines a method that returns a filtered dataset.
27
- def self.subset(name, *args, &block)
28
- def_dataset_method(name){filter(*args, &block)}
29
- end
30
-
31
- # Finds a single record according to the supplied filter, e.g.:
32
- #
33
- # Ticket.find :author => 'Sharon' # => record
34
- # Ticket.find {:price == 17} # => Dataset
35
- #
36
- def self.find(*args, &block)
37
- dataset.filter(*args, &block).first
38
- end
39
-
40
- # TODO: doc
41
- def self.[](*args)
42
- args = args.first if (args.size == 1)
43
- if args === true || args === false
44
- raise Error::InvalidFilter, "Did you mean to supply a hash?"
45
- end
46
- dataset[(Hash === args) ? args : primary_key_hash(args)]
47
- end
48
-
49
- # TODO: doc
50
- def self.fetch(*args)
51
- db.fetch(*args).set_model(self)
52
- end
53
-
54
- # Like find but invokes create with given conditions when record does not
55
- # exists.
56
- def self.find_or_create(cond)
57
- find(cond) || create(cond)
58
- end
59
-
60
- # Deletes all records in the model's table.
61
- def self.delete_all
62
- dataset.delete
63
- end
64
-
65
- # Like delete_all, but invokes before_destroy and after_destroy hooks if used.
66
- def self.destroy_all
67
- dataset.destroy
68
- end
69
-
70
- # Add dataset methods via metaprogramming
71
- DATASET_METHODS = %w'all avg count delete distinct eager eager_graph each each_page
72
- empty? except exclude filter first from_self full_outer_join graph
73
- group group_and_count group_by having import inner_join insert
74
- insert_multiple intersect interval invert_order join join_table last
75
- left_outer_join limit multi_insert naked order order_by order_more
76
- paginate print query range reverse_order right_outer_join select
77
- select_all select_more set set_graph_aliases single_value size to_csv
78
- transform union uniq unordered update where'
79
-
80
- def_dataset_method *DATASET_METHODS
55
+ include Validation
81
56
  end
82
57
  end
@@ -0,0 +1,59 @@
1
+ module Sequel
2
+ class Model
3
+ module Associations
4
+ # AssociationReflection is a Hash subclass that keeps information on Sequel::Model associations. It
5
+ # provides a few methods to reduce the amount of internal code duplication. It should not
6
+ # be instantiated by the user.
7
+ class AssociationReflection < Hash
8
+ RECIPROCAL_ASSOCIATIONS = {:many_to_one=>:one_to_many, :one_to_many=>:many_to_one, :many_to_many=>:many_to_many}
9
+
10
+ # The class associated to the current model class via this association
11
+ def associated_class
12
+ self[:class] ||= self[:class_name].constantize
13
+ end
14
+
15
+ # The associated class's primary key (used for caching)
16
+ def associated_primary_key
17
+ self[:associated_primary_key] ||= associated_class.primary_key
18
+ end
19
+
20
+ # Returns/sets the reciprocal association variable, if one exists
21
+ def reciprocal
22
+ return self[:reciprocal] if include?(:reciprocal)
23
+ reciprocal_type = RECIPROCAL_ASSOCIATIONS[self[:type]]
24
+ if reciprocal_type == :many_to_many
25
+ left_key = self[:left_key]
26
+ right_key = self[:right_key]
27
+ join_table = self[:join_table]
28
+ associated_class.all_association_reflections.each do |assoc_reflect|
29
+ if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_key] == right_key \
30
+ && assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table
31
+ return self[:reciprocal] = association_ivar(assoc_reflect[:name])
32
+ end
33
+ end
34
+ else
35
+ key = self[:key]
36
+ associated_class.all_association_reflections.each do |assoc_reflect|
37
+ if assoc_reflect[:type] == reciprocal_type && assoc_reflect[:key] == key
38
+ return self[:reciprocal] = association_ivar(assoc_reflect[:name])
39
+ end
40
+ end
41
+ end
42
+ self[:reciprocal] = nil
43
+ end
44
+
45
+ # The columns to select when loading the association
46
+ def select
47
+ self[:select] ||= associated_class.table_name.*
48
+ end
49
+
50
+ private
51
+
52
+ # Name symbol of association instance variable
53
+ def association_ivar(name)
54
+ :"@#{name}"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -19,6 +19,7 @@
19
19
  # milestones, allowing for further filtering/limiting/etc.
20
20
  # * add_milestone(obj) - Associates the passed milestone with this object
21
21
  # * remove_milestone(obj) - Removes the association with the passed milestone
22
+ # * remove_all_milestones - Removes associations with all associated milestones
22
23
  #
23
24
  # By default the classes for the associations are inferred from the association
24
25
  # name, so for example the Project#portfolio will return an instance of
@@ -27,9 +28,9 @@
27
28
  #
28
29
  # Association definitions are also reflected by the class, e.g.:
29
30
  #
30
- # >> Project.associations
31
+ # Project.associations
31
32
  # => [:portfolio, :milestones]
32
- # >> Project.association_reflection(:portfolio)
33
+ # Project.association_reflection(:portfolio)
33
34
  # => {:type => :many_to_one, :name => :portfolio, :class_name => "Portfolio"}
34
35
  #
35
36
  # Associations can be defined by either using the associate method, or by
@@ -42,9 +43,7 @@
42
43
  # one_to_many :attributes
43
44
  # has_many :attributes
44
45
  module Sequel::Model::Associations
45
- RECIPROCAL_ASSOCIATIONS = {:many_to_one=>:one_to_many, :one_to_many=>:many_to_one, :many_to_many=>:many_to_many}
46
-
47
- # Array of all association reflections
46
+ # Array of all association reflections for this model class
48
47
  def all_association_reflections
49
48
  association_reflections.values
50
49
  end
@@ -71,6 +70,8 @@ module Sequel::Model::Associations
71
70
  #
72
71
  # The following options can be supplied:
73
72
  # * *ALL types*:
73
+ # - :allow_eager - If set to false, you cannot load the association eagerly
74
+ # via eager or eager_graph
74
75
  # - :class - The associated class or its name. If not
75
76
  # given, uses the association's name, which is camelized (and
76
77
  # singularized unless the type is :many_to_one)
@@ -78,13 +79,25 @@ module Sequel::Model::Associations
78
79
  # For many_to_one associations, this is ignored unless this association is
79
80
  # being eagerly loaded, as it doesn't save queries unless multiple objects
80
81
  # can be loaded at once.
82
+ # - :eager_block - If given, use the block instead of the default block when
83
+ # eagerly loading. To not use a block when eager loading (when one is used normally),
84
+ # set to nil.
85
+ # - :graph_conditions - The conditions to use on the SQL join when eagerly loading
86
+ # the association via eager_graph
87
+ # - :graph_join_type - The type of SQL join to use when eagerly loading the association via
88
+ # eager_graph
89
+ # - :order - the column(s) by which to order the association dataset. Can be a
90
+ # singular column or an array.
81
91
  # - :reciprocal - the symbol name of the instance variable of the reciprocal association,
82
92
  # if it exists. By default, sequel will try to determine it by looking at the
83
93
  # associated model's assocations for a association that matches
84
94
  # the current association's key(s). Set to nil to not use a reciprocal.
85
- # * :one_to_many/:many_to_many:
86
- # - :order - the column(s) by which to order the association dataset. Can be a
87
- # singular column or an array.
95
+ # - :select - the attributes to select. Defaults to the associated class's
96
+ # table_name.*, which means it doesn't include the attributes from the
97
+ # join table in a many_to_many association. If you want to include the join table attributes, you can
98
+ # use this option, but beware that the join table attributes can clash with
99
+ # attributes from the model table, so you should alias any attributes that have
100
+ # the same name in both the join table and the associated table.
88
101
  # * :many_to_one:
89
102
  # - :key - foreign_key in current model's table that references
90
103
  # associated model's primary key, as a symbol. Defaults to :"#{name}_id".
@@ -98,27 +111,21 @@ module Sequel::Model::Associations
98
111
  # of current model and name of associated model, pluralized,
99
112
  # underscored, sorted, and joined with '_'.
100
113
  # - :left_key - foreign key in join table that points to current model's
101
- # primary key, as a symbol.
114
+ # primary key, as a symbol. Defaults to :"#{self.name.underscore}_id".
102
115
  # - :right_key - foreign key in join table that points to associated
103
- # model's primary key, as a symbol.
104
- # - :select - the attributes to select. Defaults to the associated class's
105
- # table_name.*, which means it doesn't include the attributes from the
106
- # join table. If you want to include the join table attributes, you can
107
- # use this option, but beware that the join table attributes can clash with
108
- # attributes from the model table, so you should alias any attributes that have
109
- # the same name in both the join table and the associated table.
116
+ # model's primary key, as a symbol. Defaults to Defaults to :"#{name.to_s.singularize}_id".
117
+ # - :graph_join_table_conditions - The conditions to use on the SQL join for the join table when eagerly loading
118
+ # the association via eager_graph
110
119
  def associate(type, name, opts = {}, &block)
111
120
  # check arguments
112
121
  raise ArgumentError unless [:many_to_one, :one_to_many, :many_to_many].include?(type) && Symbol === name
113
122
 
114
123
  # merge early so we don't modify opts
115
- opts = opts.merge(:type => type, :name => name, :block => block, :cache => true)
116
-
117
- # deprecation
118
- if opts[:from]
119
- Sequel::Deprecation.deprecate("The :from option to Sequel::Model.associate is deprecated and will be removed in Sequel 2.0. Use the :class option.")
120
- opts[:class] = opts[:from]
121
- end
124
+ opts = opts.merge(:type => type, :name => name, :block => block, :cache => true, :model => self)
125
+ opts = AssociationReflection.new.merge!(opts)
126
+ opts[:eager_block] = block unless opts.include?(:eager_block)
127
+ opts[:graph_join_type] ||= :left_outer
128
+ opts[:graph_conditions] = opts[:graph_conditions] ? opts[:graph_conditions].to_a : []
122
129
 
123
130
  # find class
124
131
  case opts[:class]
@@ -164,10 +171,6 @@ module Sequel::Model::Associations
164
171
  alias_method :has_and_belongs_to_many, :many_to_many
165
172
 
166
173
  private
167
- # The class related to the given association reflection
168
- def associated_class(opts)
169
- opts[:class] ||= opts[:class_name].constantize
170
- end
171
174
 
172
175
  # Name symbol for add association method
173
176
  def association_add_method_name(name)
@@ -179,7 +182,12 @@ module Sequel::Model::Associations
179
182
  :"@#{name}"
180
183
  end
181
184
 
182
- # Name symbol for remove_method_name
185
+ # Name symbol for remove_all association method
186
+ def association_remove_all_method_name(name)
187
+ :"remove_all_#{name}"
188
+ end
189
+
190
+ # Name symbol for remove association method
183
191
  def association_remove_method_name(name)
184
192
  :"remove_#{name.to_s.singularize}"
185
193
  end
@@ -190,7 +198,7 @@ module Sequel::Model::Associations
190
198
  @association_reflections ||= {}
191
199
  end
192
200
 
193
- # Defines an association
201
+ # Adds association methods to the model for *_to_many associations.
194
202
  def def_association_dataset_methods(name, opts, &block)
195
203
  dataset_method = :"#{name}_dataset"
196
204
  helper_method = :"#{name}_helper"
@@ -199,7 +207,7 @@ module Sequel::Model::Associations
199
207
 
200
208
  # define a method returning the association dataset (with optional order)
201
209
  if order = opts[:order]
202
- class_def(dataset_method) {instance_eval(&block).order(order)}
210
+ class_def(dataset_method) {instance_eval(&block).order(*order)}
203
211
  else
204
212
  class_def(dataset_method, &block)
205
213
  end
@@ -225,83 +233,88 @@ module Sequel::Model::Associations
225
233
  end
226
234
  objs = ds.all
227
235
  # Only one_to_many associations should set the reciprocal object
228
- if (opts[:type] == :one_to_many) && (reciprocal = model.send(:reciprocal_association, opts))
236
+ if (opts[:type] == :one_to_many) && (reciprocal = opts.reciprocal)
229
237
  objs.each{|o| o.instance_variable_set(reciprocal, self)}
230
238
  end
231
239
  instance_variable_set(ivar, objs)
232
240
  end
233
241
  end
234
242
  end
235
-
236
- # Defines an association getter method, caching the block result in an
237
- # instance variable. The defined method takes an optional reload parameter
238
- # that can be set to true in order to bypass the cache.
239
- def def_association_getter(name, &block)
240
- ivar = association_ivar(name)
241
- class_def(name) do |*reload|
242
- if !reload[0] && obj = instance_variable_get(ivar)
243
- obj == :null ? nil : obj
244
- else
245
- obj = instance_eval(&block)
246
- instance_variable_set(ivar, obj || :null)
247
- obj
248
- end
249
- end
250
- end
251
243
 
252
244
  # Adds many_to_many association instance methods
253
245
  def def_many_to_many(name, opts)
254
- assoc_class = method(:associated_class) # late binding of association dataset
255
- recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
256
246
  ivar = association_ivar(name)
257
247
  left = (opts[:left_key] ||= default_remote_key)
258
- right = (opts[:right_key] ||= :"#{name.to_s.singularize}_id")
248
+ right = (opts[:right_key] ||= default_foreign_key(opts))
259
249
  opts[:class_name] ||= name.to_s.singularize.camelize
260
250
  join_table = (opts[:join_table] ||= default_join_table_name(opts))
251
+ opts[:left_key_alias] ||= :"x_foreign_key_x"
252
+ opts[:left_key_select] ||= :"#{join_table}__#{left}___#{opts[:left_key_alias]}"
253
+ opts[:graph_join_table_conditions] = opts[:graph_join_table_conditions] ? opts[:graph_join_table_conditions].to_a : []
261
254
  database = db
262
255
 
263
256
  def_association_dataset_methods(name, opts) do
264
- klass = assoc_class[opts]
265
- key = (opts[:right_primary_key] ||= :"#{klass.table_name}__#{klass.primary_key}")
266
- selection = (opts[:select] ||= klass.table_name.*)
267
- klass.select(selection).inner_join(join_table, right => key, left => pk)
257
+ opts.associated_class.select(*opts.select).inner_join(join_table, [[right, opts.associated_primary_key], [left, pk]])
268
258
  end
269
259
 
270
260
  class_def(association_add_method_name(name)) do |o|
271
- database[join_table].insert(left => pk, right => o.pk)
261
+ database[join_table].insert(left=>pk, right=>o.pk)
272
262
  if arr = instance_variable_get(ivar)
273
263
  arr.push(o)
274
264
  end
275
- if (reciprocal = recip_assoc[opts]) && (list = o.instance_variable_get(reciprocal)) \
265
+ if (reciprocal = opts.reciprocal) && (list = o.instance_variable_get(reciprocal)) \
276
266
  && !(list.include?(self))
277
267
  list.push(self)
278
268
  end
279
269
  o
280
270
  end
281
271
  class_def(association_remove_method_name(name)) do |o|
282
- database[join_table].filter(left => pk, right => o.pk).delete
272
+ database[join_table].filter([[left, pk], [right, o.pk]]).delete
283
273
  if arr = instance_variable_get(ivar)
284
274
  arr.delete(o)
285
275
  end
286
- if (reciprocal = recip_assoc[opts]) && (list = o.instance_variable_get(reciprocal))
276
+ if (reciprocal = opts.reciprocal) && (list = o.instance_variable_get(reciprocal))
287
277
  list.delete(self)
288
278
  end
289
279
  o
290
280
  end
281
+ class_def(association_remove_all_method_name(name)) do
282
+ database[join_table].filter(left=>pk).delete
283
+ if arr = instance_variable_get(ivar)
284
+ reciprocal = opts.reciprocal
285
+ ret = arr.dup
286
+ arr.each do |o|
287
+ if reciprocal && (list = o.instance_variable_get(reciprocal))
288
+ list.delete(self)
289
+ end
290
+ end
291
+ end
292
+ instance_variable_set(ivar, [])
293
+ ret
294
+ end
291
295
  end
292
296
 
293
297
  # Adds many_to_one association instance methods
294
298
  def def_many_to_one(name, opts)
295
- assoc_class = method(:associated_class) # late binding of association dataset
296
- recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
297
299
  ivar = association_ivar(name)
298
300
 
299
- key = (opts[:key] ||= :"#{name}_id")
301
+ key = (opts[:key] ||= default_foreign_key(opts))
300
302
  opts[:class_name] ||= name.to_s.camelize
301
303
 
302
- def_association_getter(name) {(fk = send(key)) ? assoc_class[opts][fk] : nil}
304
+ class_def(name) do |*reload|
305
+ if !reload[0] && obj = instance_variable_get(ivar)
306
+ obj == :null ? nil : obj
307
+ else
308
+ obj = if fk = send(key)
309
+ opts.associated_class.select(*opts.select).filter(opts.associated_primary_key=>fk).first
310
+ end
311
+ instance_variable_set(ivar, obj || :null)
312
+ obj
313
+ end
314
+ end
315
+
303
316
  class_def(:"#{name}=") do |o|
304
- old_val = instance_variable_get(ivar) if reciprocal = recip_assoc[opts]
317
+ old_val = instance_variable_get(ivar) if reciprocal = opts.reciprocal
305
318
  instance_variable_set(ivar, o)
306
319
  send(:"#{key}=", (o.pk if o))
307
320
  if reciprocal && (old_val != o)
@@ -318,13 +331,11 @@ module Sequel::Model::Associations
318
331
 
319
332
  # Adds one_to_many association instance methods
320
333
  def def_one_to_many(name, opts)
321
- assoc_class = method(:associated_class) # late binding of association dataset
322
- recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
323
334
  ivar = association_ivar(name)
324
335
  key = (opts[:key] ||= default_remote_key)
325
336
  opts[:class_name] ||= name.to_s.singularize.camelize
326
337
 
327
- def_association_dataset_methods(name, opts) {assoc_class[opts].filter(key => pk)}
338
+ def_association_dataset_methods(name, opts) {opts.associated_class.select(*opts.select).filter(key => pk)}
328
339
 
329
340
  class_def(association_add_method_name(name)) do |o|
330
341
  o.send(:"#{key}=", pk)
@@ -332,7 +343,7 @@ module Sequel::Model::Associations
332
343
  if arr = instance_variable_get(ivar)
333
344
  arr.push(o)
334
345
  end
335
- if reciprocal = recip_assoc[opts]
346
+ if reciprocal = opts.reciprocal
336
347
  o.instance_variable_set(reciprocal, self)
337
348
  end
338
349
  o
@@ -343,46 +354,40 @@ module Sequel::Model::Associations
343
354
  if arr = instance_variable_get(ivar)
344
355
  arr.delete(o)
345
356
  end
346
- if reciprocal = recip_assoc[opts]
357
+ if reciprocal = opts.reciprocal
347
358
  o.instance_variable_set(reciprocal, :null)
348
359
  end
349
360
  o
350
361
  end
362
+ class_def(association_remove_all_method_name(name)) do
363
+ opts.associated_class.filter(key=>pk).update(key=>nil)
364
+ if arr = instance_variable_get(ivar)
365
+ ret = arr.dup
366
+ if reciprocal = opts.reciprocal
367
+ arr.each{|o| o.instance_variable_set(reciprocal, :null)}
368
+ end
369
+ end
370
+ instance_variable_set(ivar, [])
371
+ ret
372
+ end
351
373
  end
352
374
 
375
+ # Default foreign key name symbol for foreign key in current model's table that points to
376
+ # the given association's table's primary key.
377
+ def default_foreign_key(reflection)
378
+ name = reflection[:name]
379
+ :"#{reflection[:type] == :many_to_one ? name : name.to_s.singularize}_id"
380
+ end
381
+
353
382
  # Name symbol for default join table
354
383
  def default_join_table_name(opts)
355
384
  ([opts[:class_name].demodulize, name.demodulize]. \
356
385
  map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
357
386
  end
358
387
 
359
- # Name symbol for default foreign key
388
+ # Default foreign key name symbol for key in associated table that points to
389
+ # current table's primary key.
360
390
  def default_remote_key
361
391
  :"#{name.demodulize.underscore}_id"
362
392
  end
363
-
364
- # Sets the reciprocal association variable in the reflection, if one exists
365
- def reciprocal_association(reflection)
366
- return reflection[:reciprocal] if reflection.include?(:reciprocal)
367
- reciprocal_type = ::Sequel::Model::Associations::RECIPROCAL_ASSOCIATIONS[reflection[:type]]
368
- if reciprocal_type == :many_to_many
369
- left_key = reflection[:left_key]
370
- right_key = reflection[:right_key]
371
- join_table = reflection[:join_table]
372
- associated_class(reflection).all_association_reflections.each do |assoc_reflect|
373
- if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_key] == right_key \
374
- && assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table
375
- return reflection[:reciprocal] = association_ivar(assoc_reflect[:name]).to_s.freeze
376
- end
377
- end
378
- else
379
- key = reflection[:key]
380
- associated_class(reflection).all_association_reflections.each do |assoc_reflect|
381
- if assoc_reflect[:type] == reciprocal_type && assoc_reflect[:key] == key
382
- return reflection[:reciprocal] = association_ivar(assoc_reflect[:name])
383
- end
384
- end
385
- end
386
- reflection[:reciprocal] = nil
387
- end
388
393
  end