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/CHANGELOG +56 -0
- data/README +257 -154
- data/Rakefile +8 -16
- data/lib/sequel_model.rb +15 -257
- data/lib/sequel_model/associations.rb +70 -33
- data/lib/sequel_model/base.rb +80 -35
- data/lib/sequel_model/caching.rb +3 -3
- data/lib/sequel_model/deprecated.rb +81 -0
- data/lib/sequel_model/eager_loading.rb +303 -43
- data/lib/sequel_model/hooks.rb +29 -25
- data/lib/sequel_model/inflections.rb +112 -0
- data/lib/sequel_model/inflector.rb +279 -0
- data/lib/sequel_model/plugins.rb +15 -14
- data/lib/sequel_model/record.rb +87 -75
- data/lib/sequel_model/schema.rb +2 -0
- data/lib/sequel_model/validations.rb +300 -2
- data/spec/associations_spec.rb +175 -9
- data/spec/base_spec.rb +37 -18
- data/spec/caching_spec.rb +7 -4
- data/spec/deprecated_relations_spec.rb +3 -43
- data/spec/eager_loading_spec.rb +295 -7
- data/spec/hooks_spec.rb +7 -4
- data/spec/inflector_spec.rb +34 -0
- data/spec/model_spec.rb +30 -53
- data/spec/record_spec.rb +191 -33
- data/spec/spec_helper.rb +17 -2
- data/spec/validations_spec.rb +414 -15
- metadata +7 -22
- data/lib/sequel_model/pretty_table.rb +0 -73
data/Rakefile
CHANGED
@@ -9,16 +9,10 @@ include FileUtils
|
|
9
9
|
# Configuration
|
10
10
|
##############################################################################
|
11
11
|
NAME = "sequel"
|
12
|
-
VERS = "1.
|
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
|
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("
|
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
|
-
|
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
|
-
#
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
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
|
-
# *
|
15
|
-
# *
|
16
|
-
#
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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],
|
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
|
-
|
338
|
-
|
339
|
-
|
340
|
-
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
|
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] ==
|
345
|
-
return reflection[:reciprocal] =
|
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
|