sequel 1.3 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +127 -0
- data/COPYING +1 -0
- data/README +5 -4
- data/Rakefile +78 -25
- data/lib/sequel.rb +1 -2
- data/lib/sequel_model.rb +324 -0
- data/lib/sequel_model/associations.rb +351 -0
- data/lib/sequel_model/base.rb +120 -0
- data/lib/sequel_model/caching.rb +42 -0
- data/lib/sequel_model/eager_loading.rb +169 -0
- data/lib/sequel_model/hooks.rb +55 -0
- data/lib/sequel_model/plugins.rb +47 -0
- data/lib/sequel_model/pretty_table.rb +73 -0
- data/lib/sequel_model/record.rb +336 -0
- data/lib/sequel_model/schema.rb +48 -0
- data/lib/sequel_model/validations.rb +15 -0
- data/spec/associations_spec.rb +712 -0
- data/spec/base_spec.rb +239 -0
- data/spec/caching_spec.rb +150 -0
- data/spec/deprecated_relations_spec.rb +153 -0
- data/spec/eager_loading_spec.rb +260 -0
- data/spec/hooks_spec.rb +269 -0
- data/spec/model_spec.rb +543 -0
- data/spec/plugins_spec.rb +74 -0
- data/spec/rcov.opts +4 -0
- data/spec/record_spec.rb +593 -0
- data/spec/schema_spec.rb +69 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/validations_spec.rb +246 -0
- metadata +90 -56
@@ -0,0 +1,351 @@
|
|
1
|
+
# Associations are used in order to specify relationships between model classes
|
2
|
+
# that reflect relations between tables in the database using foreign keys.
|
3
|
+
#
|
4
|
+
# Each kind of association adds a number of methods to the model class which
|
5
|
+
# are specialized according to the association type and optional parameters
|
6
|
+
# given in the definition. Example:
|
7
|
+
#
|
8
|
+
# class Project < Sequel::Model
|
9
|
+
# many_to_one :portfolio
|
10
|
+
# one_to_many :milestones
|
11
|
+
# end
|
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
|
17
|
+
#
|
18
|
+
# By default the classes for the associations are inferred from the association
|
19
|
+
# name, so for example the Project#portfolio will return an instance of
|
20
|
+
# Portfolio, and Project#milestones will return an array of Milestone
|
21
|
+
# instances, in similar fashion to how ActiveRecord infers class names.
|
22
|
+
#
|
23
|
+
# Association definitions are also reflected by the class, e.g.:
|
24
|
+
#
|
25
|
+
# >> Project.associations
|
26
|
+
# => [:portfolio, :milestones]
|
27
|
+
# >> Project.association_reflection(:portfolio)
|
28
|
+
# => {:type => :many_to_one, :name => :portfolio, :class_name => "Portfolio"}
|
29
|
+
#
|
30
|
+
# Associations can be defined by either using the associate method, or by
|
31
|
+
# calling one of the three methods: many_to_one, one_to_many, many_to_many.
|
32
|
+
# Sequel::Model also provides aliases for these methods that conform to
|
33
|
+
# ActiveRecord conventions: belongs_to, has_many, has_and_belongs_to_many.
|
34
|
+
# For example, the following three statements are equivalent:
|
35
|
+
#
|
36
|
+
# associate :one_to_many, :attributes
|
37
|
+
# one_to_many :attributes
|
38
|
+
# has_many :attributes
|
39
|
+
module Sequel::Model::Associations
|
40
|
+
# Array of all association reflections
|
41
|
+
def all_association_reflections
|
42
|
+
association_reflections.values
|
43
|
+
end
|
44
|
+
|
45
|
+
# Associates a related model with the current model. The following types are
|
46
|
+
# supported:
|
47
|
+
#
|
48
|
+
# * :many_to_one - Foreign key in current model's table points to
|
49
|
+
# associated model's primary key. Each associated model object can
|
50
|
+
# be associated with more than one current model objects. Each current
|
51
|
+
# model object can be associated with only one associated model object.
|
52
|
+
# Similar to ActiveRecord/DataMapper's belongs_to.
|
53
|
+
# * :one_to_many - Foreign key in associated model's table points to this
|
54
|
+
# model's primary key. Each current model object can be associated with
|
55
|
+
# more than one associated model objects. Each associated model object
|
56
|
+
# can be associated with only one current model object.
|
57
|
+
# Similar to ActiveRecord/DataMapper's has_many.
|
58
|
+
# * :many_to_many - A join table is used that has a foreign key that points
|
59
|
+
# to this model's primary key and a foreign key that points to the
|
60
|
+
# associated model's primary key. Each current model object can be
|
61
|
+
# associated with many associated model objects, and each associated
|
62
|
+
# model object can be associated with many current model objects.
|
63
|
+
# Similar to ActiveRecord/DataMapper's has_and_belongs_to_many.
|
64
|
+
#
|
65
|
+
# The following options can be supplied:
|
66
|
+
# * *ALL types*:
|
67
|
+
# - :class - The associated class or its name. If not
|
68
|
+
# given, uses the association's name, which is camelized (and
|
69
|
+
# singularized if type is :{one,many}_to_many)
|
70
|
+
# - :eager - The associations to eagerly load when loading the associated object.
|
71
|
+
# For many_to_one associations, this is ignored unless this association is
|
72
|
+
# being eagerly loaded, as it doesn't save queries unless multiple objects
|
73
|
+
# can be loaded at once.
|
74
|
+
# * :many_to_one:
|
75
|
+
# - :key - foreign_key in current model's table that references
|
76
|
+
# associated model's primary key, as a symbol. Defaults to :"#{name}_id".
|
77
|
+
# * :one_to_many:
|
78
|
+
# - :key - foreign key in associated model's table that references
|
79
|
+
# current model's primary key, as a symbol. Defaults to
|
80
|
+
# :"#{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
|
+
# * :many_to_many:
|
88
|
+
# - :join_table - name of table that includes the foreign keys to both
|
89
|
+
# the current model and the associated model, as a symbol. Defaults to the name
|
90
|
+
# of current model and name of associated model, pluralized,
|
91
|
+
# underscored, sorted, and joined with '_'.
|
92
|
+
# - :left_key - foreign key in join table that points to current model's
|
93
|
+
# primary key, as a symbol.
|
94
|
+
# - :right_key - foreign key in join table that points to associated
|
95
|
+
# model's primary key, as a symbol.
|
96
|
+
# - :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
|
98
|
+
# join table. If you want to include the join table attributes, you can
|
99
|
+
# use this option, but beware that the join table attributes can clash with
|
100
|
+
# attributes from the model table, so you should alias any attributes that have
|
101
|
+
# 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
|
+
def associate(type, name, opts = {}, &block)
|
105
|
+
# check arguments
|
106
|
+
raise ArgumentError unless [:many_to_one, :one_to_many, :many_to_many].include?(type) && Symbol === name
|
107
|
+
|
108
|
+
# merge early so we don't modify opts
|
109
|
+
opts = opts.merge(:type => type, :name => name, :block => block, :cache => true)
|
110
|
+
|
111
|
+
# deprecation
|
112
|
+
if opts[:from]
|
113
|
+
STDERR << "The :from option is deprecated, please use the :class option instead.\r\n"
|
114
|
+
opts[:class] = opts[:from]
|
115
|
+
end
|
116
|
+
|
117
|
+
# find class
|
118
|
+
case opts[:class]
|
119
|
+
when String, Symbol
|
120
|
+
# Delete :class to allow late binding
|
121
|
+
opts[:class_name] ||= opts.delete(:class).to_s
|
122
|
+
when Class
|
123
|
+
opts[:class_name] ||= opts[:class].name
|
124
|
+
end
|
125
|
+
|
126
|
+
send(:"def_#{type}", name, opts)
|
127
|
+
|
128
|
+
# don't add to association_reflections until we are sure there are no errors
|
129
|
+
association_reflections[name] = opts
|
130
|
+
end
|
131
|
+
|
132
|
+
# The association reflection hash for the association of the given name.
|
133
|
+
def association_reflection(name)
|
134
|
+
association_reflections[name]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Array of association name symbols
|
138
|
+
def associations
|
139
|
+
association_reflections.keys
|
140
|
+
end
|
141
|
+
|
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
|
+
# Shortcut for adding a one_to_many association, see associate
|
149
|
+
def one_to_many(*args, &block)
|
150
|
+
associate(:one_to_many, *args, &block)
|
151
|
+
end
|
152
|
+
alias_method :has_many, :one_to_many
|
153
|
+
|
154
|
+
# Shortcut for adding a many_to_one association, see associate
|
155
|
+
def many_to_one(*args, &block)
|
156
|
+
associate(:many_to_one, *args, &block)
|
157
|
+
end
|
158
|
+
alias_method :belongs_to, :many_to_one
|
159
|
+
|
160
|
+
# Shortcut for adding a many_to_many association, see associate
|
161
|
+
def many_to_many(*args, &block)
|
162
|
+
associate(:many_to_many, *args, &block)
|
163
|
+
end
|
164
|
+
alias_method :has_and_belongs_to_many, :many_to_many
|
165
|
+
|
166
|
+
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
|
+
|
172
|
+
# Name symbol for add association method
|
173
|
+
def association_add_method_name(name)
|
174
|
+
:"add_#{name.to_s.singularize}"
|
175
|
+
end
|
176
|
+
|
177
|
+
# Name symbol of association instance variable
|
178
|
+
def association_ivar(name)
|
179
|
+
:"@#{name}"
|
180
|
+
end
|
181
|
+
|
182
|
+
# Name symbol for remove_method_name
|
183
|
+
def association_remove_method_name(name)
|
184
|
+
:"remove_#{name.to_s.singularize}"
|
185
|
+
end
|
186
|
+
|
187
|
+
# Hash storing the association reflections. Keys are association name
|
188
|
+
# symbols, values are association reflection hashes.
|
189
|
+
def association_reflections
|
190
|
+
@association_reflections ||= {}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Defines an association
|
194
|
+
def def_association_dataset_methods(name, opts, &block)
|
195
|
+
dataset_method = :"#{name}_dataset"
|
196
|
+
helper_method = :"#{name}_helper"
|
197
|
+
dataset_block = opts[:block]
|
198
|
+
ivar = association_ivar(name)
|
199
|
+
|
200
|
+
# define a method returning the association dataset (with optional order)
|
201
|
+
if order = opts[:order]
|
202
|
+
class_def(dataset_method) {instance_eval(&block).order(order)}
|
203
|
+
else
|
204
|
+
class_def(dataset_method, &block)
|
205
|
+
end
|
206
|
+
|
207
|
+
# If a block is given, define a helper method for it, because it takes
|
208
|
+
# an argument. This is unnecessary in Ruby 1.9, as that has instance_exec.
|
209
|
+
if dataset_block
|
210
|
+
class_def(helper_method, &dataset_block)
|
211
|
+
end
|
212
|
+
|
213
|
+
class_def(name) do |*reload|
|
214
|
+
if !reload[0] && obj = instance_variable_get(ivar)
|
215
|
+
obj
|
216
|
+
else
|
217
|
+
ds = send(dataset_method)
|
218
|
+
# if the a dataset block was specified, we need to call it and use
|
219
|
+
# the result as the dataset to fetch records from.
|
220
|
+
if dataset_block
|
221
|
+
ds = send(helper_method, ds)
|
222
|
+
end
|
223
|
+
if eager = opts[:eager]
|
224
|
+
ds = ds.eager(eager)
|
225
|
+
end
|
226
|
+
objs = ds.all
|
227
|
+
if reciprocal = self.class.send(:reciprocal_association, opts)
|
228
|
+
objs.each{|o| o.instance_variable_set(reciprocal, self)}
|
229
|
+
end
|
230
|
+
instance_variable_set(ivar, objs)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Defines an association getter method, caching the block result in an
|
236
|
+
# instance variable. The defined method takes an optional reload parameter
|
237
|
+
# that can be set to true in order to bypass the cache.
|
238
|
+
def def_association_getter(name, &block)
|
239
|
+
ivar = association_ivar(name)
|
240
|
+
class_def(name) do |*reload|
|
241
|
+
if !reload[0] && obj = instance_variable_get(ivar)
|
242
|
+
obj
|
243
|
+
else
|
244
|
+
instance_variable_set(ivar, instance_eval(&block))
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Adds many_to_many association instance methods
|
250
|
+
def def_many_to_many(name, opts)
|
251
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
252
|
+
ivar = association_ivar(name)
|
253
|
+
left = (opts[:left_key] ||= default_remote_key)
|
254
|
+
right = (opts[:right_key] ||= :"#{name.to_s.singularize}_id")
|
255
|
+
opts[:class_name] ||= name.to_s.singularize.camelize
|
256
|
+
join_table = (opts[:join_table] ||= default_join_table_name(opts))
|
257
|
+
database = db
|
258
|
+
|
259
|
+
def_association_dataset_methods(name, opts) do
|
260
|
+
klass = assoc_class[opts]
|
261
|
+
key = (opts[:right_primary_key] ||= :"#{klass.table_name}__#{klass.primary_key}")
|
262
|
+
selection = (opts[:select] ||= klass.table_name.all)
|
263
|
+
klass.select(selection).inner_join(join_table, right => key, left => pk)
|
264
|
+
end
|
265
|
+
|
266
|
+
class_def(association_add_method_name(name)) do |o|
|
267
|
+
database[join_table].insert(left => pk, right => o.pk)
|
268
|
+
if arr = instance_variable_get(ivar)
|
269
|
+
arr.push(o)
|
270
|
+
end
|
271
|
+
o
|
272
|
+
end
|
273
|
+
class_def(association_remove_method_name(name)) do |o|
|
274
|
+
database[join_table].filter(left => pk, right => o.pk).delete
|
275
|
+
if arr = instance_variable_get(ivar)
|
276
|
+
arr.delete(o)
|
277
|
+
end
|
278
|
+
o
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Adds many_to_one association instance methods
|
283
|
+
def def_many_to_one(name, opts)
|
284
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
285
|
+
ivar = association_ivar(name)
|
286
|
+
|
287
|
+
key = (opts[:key] ||= :"#{name}_id")
|
288
|
+
opts[:class_name] ||= name.to_s.camelize
|
289
|
+
|
290
|
+
def_association_getter(name) {(fk = send(key)) ? assoc_class[opts][fk] : nil}
|
291
|
+
class_def(:"#{name}=") do |o|
|
292
|
+
instance_variable_set(ivar, o)
|
293
|
+
send(:"#{key}=", (o.pk if o))
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Adds one_to_many association instance methods
|
298
|
+
def def_one_to_many(name, opts)
|
299
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
300
|
+
ivar = association_ivar(name)
|
301
|
+
key = (opts[:key] ||= default_remote_key)
|
302
|
+
opts[:class_name] ||= name.to_s.singularize.camelize
|
303
|
+
|
304
|
+
def_association_dataset_methods(name, opts) {assoc_class[opts].filter(key => pk)}
|
305
|
+
|
306
|
+
class_def(association_add_method_name(name)) do |o|
|
307
|
+
o.send(:"#{key}=", pk)
|
308
|
+
o.save!
|
309
|
+
if arr = instance_variable_get(ivar)
|
310
|
+
arr.push(o)
|
311
|
+
end
|
312
|
+
o
|
313
|
+
end
|
314
|
+
class_def(association_remove_method_name(name)) do |o|
|
315
|
+
o.send(:"#{key}=", nil)
|
316
|
+
o.save!
|
317
|
+
if arr = instance_variable_get(ivar)
|
318
|
+
arr.delete(o)
|
319
|
+
end
|
320
|
+
o
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Name symbol for default join table
|
325
|
+
def default_join_table_name(opts)
|
326
|
+
([opts[:class_name], self.name.demodulize]. \
|
327
|
+
map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
|
328
|
+
end
|
329
|
+
|
330
|
+
# Name symbol for default foreign key
|
331
|
+
def default_remote_key
|
332
|
+
:"#{name.demodulize.underscore}_id"
|
333
|
+
end
|
334
|
+
|
335
|
+
# Sets the reciprocal association variable in the reflection, if one exists
|
336
|
+
def reciprocal_association(reflection)
|
337
|
+
if reflection[:type] != :one_to_many
|
338
|
+
nil
|
339
|
+
elsif reflection.include?(:reciprocal)
|
340
|
+
reflection[:reciprocal]
|
341
|
+
else
|
342
|
+
key = reflection[:key]
|
343
|
+
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
|
346
|
+
end
|
347
|
+
end
|
348
|
+
reflection[:reciprocal] = nil
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
# Returns the database associated with the Model class.
|
4
|
+
def self.db
|
5
|
+
@db ||= (superclass != Object) && superclass.db or
|
6
|
+
raise Error, "No database associated with #{self}"
|
7
|
+
end
|
8
|
+
|
9
|
+
# Sets the database associated with the Model class.
|
10
|
+
def self.db=(db)
|
11
|
+
@db = db
|
12
|
+
if @dataset
|
13
|
+
set_dataset(db[table_name])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Called when a database is opened in order to automatically associate the
|
18
|
+
# first opened database with model classes.
|
19
|
+
def self.database_opened(db)
|
20
|
+
@db = db if (self == Model) && !@db
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the implicit table name for the model class.
|
24
|
+
def self.implicit_table_name
|
25
|
+
name.demodulize.underscore.pluralize.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the dataset associated with the Model class.
|
29
|
+
def self.dataset
|
30
|
+
unless @dataset
|
31
|
+
if ds = super_dataset
|
32
|
+
set_dataset(ds.clone)
|
33
|
+
elsif !name.empty?
|
34
|
+
set_dataset(db[implicit_table_name])
|
35
|
+
else
|
36
|
+
raise Error, "No dataset associated with #{self}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@dataset
|
40
|
+
end
|
41
|
+
|
42
|
+
# def self.dataset
|
43
|
+
# @dataset ||= super_dataset ||
|
44
|
+
# (!(n = name).empty? && db[n.underscore.pluralize.to_sym]) ||
|
45
|
+
# (raise Error, "No dataset associated with #{self}")
|
46
|
+
# end
|
47
|
+
|
48
|
+
def self.super_dataset # :nodoc:
|
49
|
+
superclass.dataset if (superclass != Sequel::Model) && superclass.respond_to?(:dataset)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the columns in the result set in their original order.
|
53
|
+
#
|
54
|
+
# See Dataset#columns for more information.
|
55
|
+
def self.columns
|
56
|
+
@columns ||= dataset.columns or
|
57
|
+
raise Error, "Could not fetch columns for #{self}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sets the dataset associated with the Model class.
|
61
|
+
def self.set_dataset(ds)
|
62
|
+
@db = ds.db
|
63
|
+
@dataset = ds
|
64
|
+
@dataset.set_model(self)
|
65
|
+
@dataset.extend(Associations::EagerLoading)
|
66
|
+
@dataset.transform(@transform) if @transform
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the database assoiated with the object's Model class.
|
70
|
+
def db
|
71
|
+
@db ||= model.db
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the dataset assoiated with the object's Model class.
|
75
|
+
#
|
76
|
+
# See Dataset for more information.
|
77
|
+
def dataset
|
78
|
+
model.dataset
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the columns associated with the object's Model class.
|
82
|
+
def columns
|
83
|
+
model.columns
|
84
|
+
end
|
85
|
+
|
86
|
+
# Serializes column with YAML or through marshalling.
|
87
|
+
def self.serialize(*columns)
|
88
|
+
format = columns.pop[:format] if Hash === columns.last
|
89
|
+
format ||= :yaml
|
90
|
+
|
91
|
+
@transform = columns.inject({}) do |m, c|
|
92
|
+
m[c] = format
|
93
|
+
m
|
94
|
+
end
|
95
|
+
@dataset.transform(@transform) if @dataset
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Lets you create a Model class with its table name already set or reopen
|
100
|
+
# an existing Model.
|
101
|
+
#
|
102
|
+
# Makes given dataset inherited.
|
103
|
+
#
|
104
|
+
# === Example:
|
105
|
+
# class Comment < Sequel::Model(:something)
|
106
|
+
# table_name # => :something
|
107
|
+
#
|
108
|
+
# # ...
|
109
|
+
#
|
110
|
+
# end
|
111
|
+
def self.Model(source)
|
112
|
+
@models ||= {}
|
113
|
+
@models[source] ||= Class.new(Sequel::Model) do
|
114
|
+
meta_def(:inherited) do |c|
|
115
|
+
c.set_dataset(source.is_a?(Dataset) ? source : c.db[source])
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|