sequel 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -9,16 +9,10 @@ include FileUtils
9
9
  # Configuration
10
10
  ##############################################################################
11
11
  NAME = "sequel"
12
- VERS = "1.4.0"
12
+ VERS = "1.5.0"
13
+ SEQUEL_CORE_VERS= "1.5.0"
13
14
  CLEAN.include ["**/.*.sw?", "pkg/*", ".config", "doc/*", "coverage/*"]
14
- RDOC_OPTS = [
15
- "--quiet",
16
- "--title", "Sequel: The Database Toolkit for Ruby",
17
- "--opname", "index.html",
18
- "--line-numbers",
19
- "--main", "README",
20
- "--inline-source"
21
- ]
15
+ RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source"]
22
16
 
23
17
  ##############################################################################
24
18
  # RDoc
@@ -29,15 +23,14 @@ Rake::RDocTask.new do |rdoc|
29
23
  rdoc.rdoc_dir = "doc/rdoc"
30
24
  rdoc.options += RDOC_OPTS
31
25
  rdoc.main = "README"
32
- rdoc.title = "Sequel: The Database Toolkit for Ruby"
33
- rdoc.rdoc_files.add ["README", "COPYING", "lib/sequel_model.rb", "lib/**/*.rb"]
26
+ rdoc.title = "Sequel: The Database Toolkit for Ruby: Model Classes"
27
+ rdoc.rdoc_files.add ["README", "COPYING", "lib/**/*.rb"]
34
28
  end
35
29
 
36
30
  ##############################################################################
37
31
  # Gem packaging
38
32
  ##############################################################################
39
33
  desc "Packages up Sequel."
40
- task :default => [:package]
41
34
  task :package => [:clean]
42
35
 
43
36
  spec = Gem::Specification.new do |s|
@@ -47,8 +40,7 @@ spec = Gem::Specification.new do |s|
47
40
  s.platform = Gem::Platform::RUBY
48
41
  s.has_rdoc = true
49
42
  s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
50
- s.rdoc_options += RDOC_OPTS +
51
- ["--exclude", "^(examples|extras)\/", "--exclude", "lib/sequel_model.rb"]
43
+ s.rdoc_options += RDOC_OPTS + ["--exclude", "^(examples|extras)\/"]
52
44
  s.summary = "The Database Toolkit for Ruby: Model Classes"
53
45
  s.description = s.summary
54
46
  s.author = "Jeremy Evans"
@@ -63,8 +55,7 @@ spec = Gem::Specification.new do |s|
63
55
  s.platform = Gem::Platform::RUBY
64
56
  end
65
57
 
66
- s.add_dependency("assistance", '>= 0.1.2')
67
- s.add_dependency("sequel_core", '= 1.4.0')
58
+ s.add_dependency("sequel_core", "= #{SEQUEL_CORE_VERS}")
68
59
 
69
60
  s.files = %w(COPYING README Rakefile) + Dir.glob("{doc,spec,lib}/**/*")
70
61
 
@@ -119,6 +110,7 @@ Spec::Rake::SpecTask.new("spec") do |t|
119
110
  end
120
111
 
121
112
  desc "Run specs without coverage"
113
+ task :default => [:spec_no_cov]
122
114
  Spec::Rake::SpecTask.new("spec_no_cov") do |t|
123
115
  fixRUBYLIB.call
124
116
  t.spec_files = FileList["spec/**/*_spec.rb"]
data/lib/sequel_model.rb CHANGED
@@ -7,239 +7,15 @@ module Sequel
7
7
  end
8
8
 
9
9
  files = %w[
10
- base hooks record schema associations
11
- caching plugins validations eager_loading
10
+ inflector inflections base hooks record schema associations
11
+ caching plugins validations eager_loading deprecated
12
12
  ]
13
13
  dir = File.join(File.dirname(__FILE__), "sequel_model")
14
14
  files.each {|f| require(File.join(dir, f))}
15
15
 
16
16
  module Sequel
17
- # == Sequel Models
18
- #
19
- # Models in Sequel are based on the Active Record pattern described by Martin Fowler (http://www.martinfowler.com/eaaCatalog/activeRecord.html). A model class corresponds to a table or a dataset, and an instance of that class wraps a single record in the model's underlying dataset.
20
- #
21
- # Model classes are defined as regular Ruby classes:
22
- #
23
- # DB = Sequel('sqlite:/blog.db')
24
- # class Post < Sequel::Model
25
- # set_dataset DB[:posts]
26
- # end
27
- #
28
- # You can also use the shorthand form:
29
- #
30
- # DB = Sequel('sqlite:/blog.db')
31
- # class Post < Sequel::Model
32
- # end
33
- #
34
- # === Model instances
35
- #
36
- # Model instance are identified by a primary key. By default, Sequel assumes the primary key column to be :id. The Model#[] method can be used to fetch records by their primary key:
37
- #
38
- # post = Post[123]
39
- #
40
- # The Model#pk method is used to retrieve the record's primary key value:
41
- #
42
- # post.pk #=> 123
43
- #
44
- # Sequel models allow you to use any column as a primary key, and even composite keys made from multiple columns:
45
- #
46
- # class Post < Sequel::Model
47
- # set_primary_key [:category, :title]
48
- # end
49
- #
50
- # post = Post['ruby', 'hello world']
51
- # post.pk #=> ['ruby', 'hello world']
52
- #
53
- # You can also define a model class that does not have a primary key, but then you lose the ability to update records.
54
- #
55
- # A model instance can also be fetched by specifying a condition:
56
- #
57
- # post = Post[:title => 'hello world']
58
- # post = Post.find {:stamp < 10.days.ago}
59
- #
60
- # === Iterating over records
61
- #
62
- # A model class lets you iterate over specific records by acting as a proxy to the underlying dataset. This means that you can use the entire Dataset API to create customized queries that return model instances, e.g.:
63
- #
64
- # Post.filter(:category => 'ruby').each {|post| p post}
65
- #
66
- # You can also manipulate the records in the dataset:
67
- #
68
- # Post.filter {:stamp < 7.days.ago}.delete
69
- # Post.filter {:title =~ /ruby/}.update(:category => 'ruby')
70
- #
71
- # === Accessing record values
72
- #
73
- # A model instances stores its values as a hash:
74
- #
75
- # post.values #=> {:id => 123, :category => 'ruby', :title => 'hello world'}
76
- #
77
- # You can read the record values as object attributes:
78
- #
79
- # post.id #=> 123
80
- # post.title #=> 'hello world'
81
- #
82
- # You can also change record values:
83
- #
84
- # post.title = 'hey there'
85
- # post.save
86
- #
87
- # Another way to change values by using the #set method:
88
- #
89
- # post.set(:title => 'hey there')
90
- #
91
- # === Creating new records
92
- #
93
- # New records can be created by calling Model.create:
94
- #
95
- # post = Post.create(:title => 'hello world')
96
- #
97
- # Another way is to construct a new instance and save it:
98
- #
99
- # post = Post.new
100
- # post.title = 'hello world'
101
- # post.save
102
- #
103
- # You can also supply a block to Model.new and Model.create:
104
- #
105
- # post = Post.create {|p| p.title = 'hello world'}
106
- #
107
- # post = Post.new do |p|
108
- # p.title = 'hello world'
109
- # p.save
110
- # end
111
- #
112
- # === Hooks
113
- #
114
- # 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.
115
- #
116
- # Hooks are defined by supplying a block:
117
- #
118
- # class Post < Sequel::Model
119
- # after_create do
120
- # set(:created_at => Time.now)
121
- # end
122
- #
123
- # after_destroy do
124
- # author.update_post_count
125
- # end
126
- # end
127
- #
128
- # === Deleting records
129
- #
130
- # You can delete individual records by calling #delete or #destroy. The only difference between the two methods is that #destroy invokes before_destroy and after_destroy hooks, while #delete does not:
131
- #
132
- # post.delete #=> bypasses hooks
133
- # post.destroy #=> runs hooks
134
- #
135
- # Records can also be deleted en-masse by invoking Model.delete and Model.destroy. As stated above, you can specify filters for the deleted records:
136
- #
137
- # Post.filter(:category => 32).delete #=> bypasses hooks
138
- # Post.filter(:category => 32).destroy #=> runs hooks
139
- #
140
- # Please note that if Model.destroy is called, each record is deleted
141
- # separately, but Model.delete deletes all relevant records with a single
142
- # SQL statement.
143
- #
144
- # === Associations
145
- #
146
- # Sequel provides macros for the three most common types of associations:
147
- # many_to_one, one_to_many and many_to_many (equivalent to ActiveRecord's
148
- # belongs_to, has_many and has_and_belongs_to_many).
149
- #
150
- # Associations are defined in similar fashion to ActiveRecord:
151
- #
152
- # class Post < Sequel::Model
153
- # belongs_to :author
154
- # end
155
- #
156
- # class Author < Sequel::Model
157
- # has_many :posts
158
- # end
159
- #
160
- # Another way to define an association in a Sequel model is as a regular
161
- # instance method:
162
- #
163
- # class Post < Sequel::Model
164
- # def author; Author[author_id]; end
165
- # end
166
- #
167
- # class Author < Sequel::Model
168
- # def posts; Post.filter(:author_id => pk); end
169
- # end
170
- #
171
- # === Caching model instances with memcached
172
- #
173
- # Sequel models can be cached using memcached based on their primary keys. The use of memcached can significantly reduce database load by keeping model instances in memory. The set_cache method is used to specify caching:
174
- #
175
- # require 'memcache'
176
- # CACHE = MemCache.new 'localhost:11211', :namespace => 'blog'
177
- #
178
- # class Author < Sequel::Model
179
- # set_cache CACHE, :ttl => 3600
180
- # end
181
- #
182
- # Author[333] # database hit
183
- # Author[333] # cache hit
184
- #
185
- # === Extending the underlying dataset
186
- #
187
- # The obvious way to add table-wide logic is to define class methods to the model class definition. That way you can define subsets of the underlying dataset, change the ordering, or perform actions on multiple records:
188
- #
189
- # class Post < Sequel::Model
190
- # def self.old_posts
191
- # filter {:stamp < 30.days.ago}
192
- # end
193
- #
194
- # def self.clean_old_posts
195
- # old_posts.delete
196
- # end
197
- # end
198
- #
199
- # You can also implement table-wide logic by defining methods on the dataset:
200
- #
201
- # class Post < Sequel::Model
202
- # def dataset.old_posts
203
- # filter {:stamp < 30.days.ago}
204
- # end
205
- #
206
- # def dataset.clean_old_posts
207
- # old_posts.delete
208
- # end
209
- # end
210
- #
211
- # This is the recommended way of implementing table-wide operations, and allows you to have access to your model API from filtered datasets as well:
212
- #
213
- # Post.filter(:category => 'ruby').clean_old_posts
214
- #
215
- # Sequel models also provide a short hand notation for filters:
216
- #
217
- # class Post < Sequel::Model
218
- # subset(:old_posts) {:stamp < 30.days.ago}
219
- # subset :invisible, :visible => false
220
- # end
221
- #
222
- # === Defining the underlying schema
223
- #
224
- # Model classes can also be used as a place to define your table schema and control it. The schema DSL is exactly the same provided by Sequel::Schema::Generator:
225
- #
226
- # class Post < Sequel::Model
227
- # set_schema do
228
- # primary_key :id
229
- # text :title
230
- # text :category
231
- # foreign_key :author_id, :table => :authors
232
- # end
233
- # end
234
- #
235
- # You can then create the underlying table, drop it, or recreate it:
236
- #
237
- # Post.table_exists?
238
- # Post.create_table
239
- # Post.drop_table
240
- # Post.create_table! # drops the table if it exists and then recreates it
241
- #
242
17
  class Model
18
+ extend Enumerable
243
19
  extend Associations
244
20
  # Returns a string representation of the model instance including
245
21
  # the class name and values.
@@ -249,7 +25,7 @@ module Sequel
249
25
 
250
26
  # Defines a method that returns a filtered dataset.
251
27
  def self.subset(name, *args, &block)
252
- dataset.meta_def(name) {filter(*args, &block)}
28
+ def_dataset_method(name){filter(*args, &block)}
253
29
  end
254
30
 
255
31
  # Finds a single record according to the supplied filter, e.g.:
@@ -281,8 +57,6 @@ module Sequel
281
57
  find(cond) || create(cond)
282
58
  end
283
59
 
284
- ############################################################################
285
-
286
60
  # Deletes all records in the model's table.
287
61
  def self.delete_all
288
62
  dataset.delete
@@ -292,33 +66,17 @@ module Sequel
292
66
  def self.destroy_all
293
67
  dataset.destroy
294
68
  end
295
-
296
- def self.is_dataset_magic_method?(m)
297
- method_name = m.to_s
298
- Sequel::Dataset::MAGIC_METHODS.each_key do |r|
299
- return true if method_name =~ r
300
- end
301
- false
302
- end
303
-
304
- def self.method_missing(m, *args, &block) #:nodoc:
305
- Thread.exclusive do
306
- if dataset.respond_to?(m) || is_dataset_magic_method?(m)
307
- instance_eval("def #{m}(*args, &block); dataset.#{m}(*args, &block); end")
308
- end
309
- end
310
- respond_to?(m) ? send(m, *args, &block) : super(m, *args)
311
- end
312
69
 
313
- # TODO: Comprehensive description goes here!
314
- def self.join(*args)
315
- table_name = dataset.opts[:from].first
316
- dataset.join(*args).select(table_name.to_sym.ALL)
317
- end
318
-
319
- # Returns an array containing all of the models records.
320
- def self.all
321
- dataset.all
322
- end
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
323
81
  end
324
82
  end
@@ -10,10 +10,15 @@
10
10
  # one_to_many :milestones
11
11
  # end
12
12
  #
13
- # The project class now has the following methods:
14
- # * Project#portfolio, Project#portfolio=
15
- # * Project#milestones, Project#add_milestone, Project#remove_milestone,
16
- # Project#milestones_dataset
13
+ # The project class now has the following instance methods:
14
+ # * portfolio - Returns the associated portfolio
15
+ # * portfolio=(obj) - Sets the associated portfolio to the object,
16
+ # but the change is not persisted until you save the record.
17
+ # * milestones - Returns an array of associated milestones
18
+ # * milestones_dataset - Returns a dataset that would return the associated
19
+ # milestones, allowing for further filtering/limiting/etc.
20
+ # * add_milestone(obj) - Associates the passed milestone with this object
21
+ # * remove_milestone(obj) - Removes the association with the passed milestone
17
22
  #
18
23
  # By default the classes for the associations are inferred from the association
19
24
  # name, so for example the Project#portfolio will return an instance of
@@ -37,6 +42,8 @@
37
42
  # one_to_many :attributes
38
43
  # has_many :attributes
39
44
  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
+
40
47
  # Array of all association reflections
41
48
  def all_association_reflections
42
49
  association_reflections.values
@@ -66,11 +73,18 @@ module Sequel::Model::Associations
66
73
  # * *ALL types*:
67
74
  # - :class - The associated class or its name. If not
68
75
  # given, uses the association's name, which is camelized (and
69
- # singularized if type is :{one,many}_to_many)
76
+ # singularized unless the type is :many_to_one)
70
77
  # - :eager - The associations to eagerly load when loading the associated object.
71
78
  # For many_to_one associations, this is ignored unless this association is
72
79
  # being eagerly loaded, as it doesn't save queries unless multiple objects
73
80
  # can be loaded at once.
81
+ # - :reciprocal - the symbol name of the instance variable of the reciprocal association,
82
+ # if it exists. By default, sequel will try to determine it by looking at the
83
+ # associated model's assocations for a association that matches
84
+ # 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.
74
88
  # * :many_to_one:
75
89
  # - :key - foreign_key in current model's table that references
76
90
  # associated model's primary key, as a symbol. Defaults to :"#{name}_id".
@@ -78,12 +92,6 @@ module Sequel::Model::Associations
78
92
  # - :key - foreign key in associated model's table that references
79
93
  # current model's primary key, as a symbol. Defaults to
80
94
  # :"#{self.name.underscore}_id".
81
- # - :reciprocal - the string name of the instance variable of the reciprocal many_to_one association,
82
- # if it exists. By default, sequel will try to determine it by looking at the
83
- # associated model's assocations for a many_to_one association that matches
84
- # the current association's key. Set to nil to not use a reciprocal.
85
- # - :order - the column(s) by which to order the association dataset. Can be a
86
- # singular column or an array.
87
95
  # * :many_to_many:
88
96
  # - :join_table - name of table that includes the foreign keys to both
89
97
  # the current model and the associated model, as a symbol. Defaults to the name
@@ -94,13 +102,11 @@ module Sequel::Model::Associations
94
102
  # - :right_key - foreign key in join table that points to associated
95
103
  # model's primary key, as a symbol.
96
104
  # - :select - the attributes to select. Defaults to the associated class's
97
- # table_name.*, which means it doesn't include the attributes from the join
105
+ # table_name.*, which means it doesn't include the attributes from the
98
106
  # join table. If you want to include the join table attributes, you can
99
107
  # use this option, but beware that the join table attributes can clash with
100
108
  # attributes from the model table, so you should alias any attributes that have
101
109
  # the same name in both the join table and the associated table.
102
- # - :order - the column(s) by which to order the association dataset. Can be a
103
- # singular column or an array.
104
110
  def associate(type, name, opts = {}, &block)
105
111
  # check arguments
106
112
  raise ArgumentError unless [:many_to_one, :one_to_many, :many_to_many].include?(type) && Symbol === name
@@ -110,7 +116,7 @@ module Sequel::Model::Associations
110
116
 
111
117
  # deprecation
112
118
  if opts[:from]
113
- STDERR << "The :from option is deprecated, please use the :class option instead.\r\n"
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.")
114
120
  opts[:class] = opts[:from]
115
121
  end
116
122
 
@@ -139,12 +145,6 @@ module Sequel::Model::Associations
139
145
  association_reflections.keys
140
146
  end
141
147
 
142
- # deprecated, please use many_to_one instead
143
- def one_to_one(*args, &block)
144
- STDERR << "one_to_one relation definitions are deprecated, please use many_to_one instead.\r\n"
145
- many_to_one(*args, &block)
146
- end
147
-
148
148
  # Shortcut for adding a one_to_many association, see associate
149
149
  def one_to_many(*args, &block)
150
150
  associate(:one_to_many, *args, &block)
@@ -224,7 +224,8 @@ module Sequel::Model::Associations
224
224
  ds = ds.eager(eager)
225
225
  end
226
226
  objs = ds.all
227
- if reciprocal = self.class.send(:reciprocal_association, opts)
227
+ # Only one_to_many associations should set the reciprocal object
228
+ if (opts[:type] == :one_to_many) && (reciprocal = model.send(:reciprocal_association, opts))
228
229
  objs.each{|o| o.instance_variable_set(reciprocal, self)}
229
230
  end
230
231
  instance_variable_set(ivar, objs)
@@ -239,9 +240,11 @@ module Sequel::Model::Associations
239
240
  ivar = association_ivar(name)
240
241
  class_def(name) do |*reload|
241
242
  if !reload[0] && obj = instance_variable_get(ivar)
242
- obj
243
+ obj == :null ? nil : obj
243
244
  else
244
- instance_variable_set(ivar, instance_eval(&block))
245
+ obj = instance_eval(&block)
246
+ instance_variable_set(ivar, obj || :null)
247
+ obj
245
248
  end
246
249
  end
247
250
  end
@@ -249,6 +252,7 @@ module Sequel::Model::Associations
249
252
  # Adds many_to_many association instance methods
250
253
  def def_many_to_many(name, opts)
251
254
  assoc_class = method(:associated_class) # late binding of association dataset
255
+ recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
252
256
  ivar = association_ivar(name)
253
257
  left = (opts[:left_key] ||= default_remote_key)
254
258
  right = (opts[:right_key] ||= :"#{name.to_s.singularize}_id")
@@ -259,7 +263,7 @@ module Sequel::Model::Associations
259
263
  def_association_dataset_methods(name, opts) do
260
264
  klass = assoc_class[opts]
261
265
  key = (opts[:right_primary_key] ||= :"#{klass.table_name}__#{klass.primary_key}")
262
- selection = (opts[:select] ||= klass.table_name.all)
266
+ selection = (opts[:select] ||= klass.table_name.*)
263
267
  klass.select(selection).inner_join(join_table, right => key, left => pk)
264
268
  end
265
269
 
@@ -268,6 +272,10 @@ module Sequel::Model::Associations
268
272
  if arr = instance_variable_get(ivar)
269
273
  arr.push(o)
270
274
  end
275
+ if (reciprocal = recip_assoc[opts]) && (list = o.instance_variable_get(reciprocal)) \
276
+ && !(list.include?(self))
277
+ list.push(self)
278
+ end
271
279
  o
272
280
  end
273
281
  class_def(association_remove_method_name(name)) do |o|
@@ -275,6 +283,9 @@ module Sequel::Model::Associations
275
283
  if arr = instance_variable_get(ivar)
276
284
  arr.delete(o)
277
285
  end
286
+ if (reciprocal = recip_assoc[opts]) && (list = o.instance_variable_get(reciprocal))
287
+ list.delete(self)
288
+ end
278
289
  o
279
290
  end
280
291
  end
@@ -282,6 +293,7 @@ module Sequel::Model::Associations
282
293
  # Adds many_to_one association instance methods
283
294
  def def_many_to_one(name, opts)
284
295
  assoc_class = method(:associated_class) # late binding of association dataset
296
+ recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
285
297
  ivar = association_ivar(name)
286
298
 
287
299
  key = (opts[:key] ||= :"#{name}_id")
@@ -289,14 +301,25 @@ module Sequel::Model::Associations
289
301
 
290
302
  def_association_getter(name) {(fk = send(key)) ? assoc_class[opts][fk] : nil}
291
303
  class_def(:"#{name}=") do |o|
304
+ old_val = instance_variable_get(ivar) if reciprocal = recip_assoc[opts]
292
305
  instance_variable_set(ivar, o)
293
306
  send(:"#{key}=", (o.pk if o))
307
+ if reciprocal && (old_val != o)
308
+ if old_val && (list = old_val.instance_variable_get(reciprocal))
309
+ list.delete(self)
310
+ end
311
+ if o && (list = o.instance_variable_get(reciprocal)) && !(list.include?(self))
312
+ list.push(self)
313
+ end
314
+ end
315
+ o
294
316
  end
295
317
  end
296
318
 
297
319
  # Adds one_to_many association instance methods
298
320
  def def_one_to_many(name, opts)
299
321
  assoc_class = method(:associated_class) # late binding of association dataset
322
+ recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
300
323
  ivar = association_ivar(name)
301
324
  key = (opts[:key] ||= default_remote_key)
302
325
  opts[:class_name] ||= name.to_s.singularize.camelize
@@ -309,6 +332,9 @@ module Sequel::Model::Associations
309
332
  if arr = instance_variable_get(ivar)
310
333
  arr.push(o)
311
334
  end
335
+ if reciprocal = recip_assoc[opts]
336
+ o.instance_variable_set(reciprocal, self)
337
+ end
312
338
  o
313
339
  end
314
340
  class_def(association_remove_method_name(name)) do |o|
@@ -317,13 +343,16 @@ module Sequel::Model::Associations
317
343
  if arr = instance_variable_get(ivar)
318
344
  arr.delete(o)
319
345
  end
346
+ if reciprocal = recip_assoc[opts]
347
+ o.instance_variable_set(reciprocal, :null)
348
+ end
320
349
  o
321
350
  end
322
351
  end
323
352
 
324
353
  # Name symbol for default join table
325
354
  def default_join_table_name(opts)
326
- ([opts[:class_name], self.name.demodulize]. \
355
+ ([opts[:class_name].demodulize, name.demodulize]. \
327
356
  map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
328
357
  end
329
358
 
@@ -334,18 +363,26 @@ module Sequel::Model::Associations
334
363
 
335
364
  # Sets the reciprocal association variable in the reflection, if one exists
336
365
  def reciprocal_association(reflection)
337
- if reflection[:type] != :one_to_many
338
- nil
339
- elsif reflection.include?(:reciprocal)
340
- reflection[:reciprocal]
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
341
378
  else
342
379
  key = reflection[:key]
343
380
  associated_class(reflection).all_association_reflections.each do |assoc_reflect|
344
- if assoc_reflect[:type] == :many_to_one && assoc_reflect[:key] == key
345
- return reflection[:reciprocal] = "@#{assoc_reflect[:name]}".freeze
381
+ if assoc_reflect[:type] == reciprocal_type && assoc_reflect[:key] == key
382
+ return reflection[:reciprocal] = association_ivar(assoc_reflect[:name])
346
383
  end
347
384
  end
348
- reflection[:reciprocal] = nil
349
385
  end
386
+ reflection[:reciprocal] = nil
350
387
  end
351
388
  end