sequel 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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