sequel_model 0.4.2 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +26 -0
- data/README +3 -3
- data/Rakefile +4 -4
- data/lib/sequel_model.rb +17 -16
- data/lib/sequel_model/associations.rb +317 -0
- data/lib/sequel_model/record.rb +2 -2
- data/spec/associations_spec.rb +616 -0
- data/spec/deprecated_relations_spec.rb +153 -0
- data/spec/spec_helper.rb +4 -0
- metadata +8 -7
- data/lib/sequel_model/relations.rb +0 -154
- data/spec/relations_spec.rb +0 -149
data/CHANGELOG
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
=== 0.5 (2008-03-08)
|
2
|
+
|
3
|
+
* Merged new associations branch into trunk.
|
4
|
+
|
5
|
+
* Rewrote RDoc for associations.
|
6
|
+
|
7
|
+
* Added has_and_belongs_to_many alias for many_to_many.
|
8
|
+
|
9
|
+
* Added support for optional dataset block.
|
10
|
+
|
11
|
+
* Added :order option to order association datasets.
|
12
|
+
|
13
|
+
* Added :cache option to return and cache array of objects for association.
|
14
|
+
|
15
|
+
* Changed one_to_many, many_to_many associations to return dataset by default.
|
16
|
+
|
17
|
+
* Added has_many, belongs_to aliases.
|
18
|
+
|
19
|
+
* Refactored associations code.
|
20
|
+
|
21
|
+
* Added deprecations for old-style relations.
|
22
|
+
|
23
|
+
* Completed specs for new associations code.
|
24
|
+
|
25
|
+
* New associations code by Jeremy Evans (replaces relations code.)
|
26
|
+
|
1
27
|
=== 0.4.2 (2008-02-29)
|
2
28
|
|
3
29
|
* Fixed one_to_many implicit key to work correctly for namespaced classes (#167).
|
data/README
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
== Sequel:
|
1
|
+
== Sequel: The Database Toolkit for Ruby
|
2
2
|
|
3
|
-
Sequel is
|
3
|
+
Sequel is a database access toolkit for Ruby. Sequel provides thread safety, connection pooling, and a concise DSL for constructing queries and table schemas.
|
4
4
|
|
5
5
|
Sequel makes it easy to deal with multiple records without having to break your teeth on SQL.
|
6
6
|
|
@@ -49,7 +49,7 @@ You get an IRB session with the database object stored in DB.
|
|
49
49
|
|
50
50
|
== An Introduction
|
51
51
|
|
52
|
-
Sequel
|
52
|
+
Sequel is designed to take the hassle away from connecting to databases and manipulating them. Sequel deals with all the boring stuff like maintaining connections, formatting SQL correctly and fetching records so you can concentrate on your application.
|
53
53
|
|
54
54
|
Sequel uses the concept of datasets to retrieve data. A Dataset object encapsulates an SQL query and supports chainability, letting you fetch data using a convenient Ruby DSL that is both concise and infinitely flexible.
|
55
55
|
|
data/Rakefile
CHANGED
@@ -9,11 +9,11 @@ include FileUtils
|
|
9
9
|
# Configuration
|
10
10
|
##############################################################################
|
11
11
|
NAME = "sequel_model"
|
12
|
-
VERS = "0.
|
12
|
+
VERS = "0.5"
|
13
13
|
CLEAN.include ["**/.*.sw?", "pkg/*", ".config", "doc/*", "coverage/*"]
|
14
14
|
RDOC_OPTS = [
|
15
15
|
"--quiet",
|
16
|
-
"--title", "Sequel
|
16
|
+
"--title", "Sequel: The Database Toolkit for Ruby",
|
17
17
|
"--opname", "index.html",
|
18
18
|
"--line-numbers",
|
19
19
|
"--main", "README",
|
@@ -29,7 +29,7 @@ Rake::RDocTask.new do |rdoc|
|
|
29
29
|
rdoc.rdoc_dir = "doc/rdoc"
|
30
30
|
rdoc.options += RDOC_OPTS
|
31
31
|
rdoc.main = "README"
|
32
|
-
rdoc.title = "Sequel:
|
32
|
+
rdoc.title = "Sequel: The Database Toolkit for Ruby"
|
33
33
|
rdoc.rdoc_files.add ["README", "COPYING", "lib/sequel_model.rb", "lib/**/*.rb"]
|
34
34
|
end
|
35
35
|
|
@@ -49,7 +49,7 @@ spec = Gem::Specification.new do |s|
|
|
49
49
|
s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
|
50
50
|
s.rdoc_options += RDOC_OPTS +
|
51
51
|
["--exclude", "^(examples|extras)\/", "--exclude", "lib/sequel_model.rb"]
|
52
|
-
s.summary = "
|
52
|
+
s.summary = "Model classes for Sequel"
|
53
53
|
s.description = s.summary
|
54
54
|
s.author = "Sharon Rosner"
|
55
55
|
s.email = "ciconia@gmail.com"
|
data/lib/sequel_model.rb
CHANGED
@@ -6,7 +6,7 @@ end
|
|
6
6
|
|
7
7
|
# TODO: add relationships when complete:
|
8
8
|
files = %w[
|
9
|
-
base hooks record schema
|
9
|
+
base hooks record schema associations
|
10
10
|
caching plugins validations
|
11
11
|
]
|
12
12
|
dir = File.join(File.dirname(__FILE__), "sequel_model")
|
@@ -136,37 +136,37 @@ module Sequel
|
|
136
136
|
# Post.filter(:category => 32).delete #=> bypasses hooks
|
137
137
|
# Post.filter(:category => 32).destroy #=> runs hooks
|
138
138
|
#
|
139
|
-
# Please note that if Model.destroy is called, each record is deleted
|
139
|
+
# Please note that if Model.destroy is called, each record is deleted
|
140
|
+
# separately, but Model.delete deletes all relevant records with a single
|
141
|
+
# SQL statement.
|
140
142
|
#
|
141
143
|
# === Associations
|
142
144
|
#
|
143
|
-
#
|
145
|
+
# Sequel provides macros for the three most common types of associations:
|
146
|
+
# many_to_one, one_to_many and many_to_many (equivalent to ActiveRecord's
|
147
|
+
# belongs_to, has_many and has_and_belongs_to_many).
|
144
148
|
#
|
149
|
+
# Associations are defined in similar fashion to ActiveRecord:
|
150
|
+
#
|
145
151
|
# class Post < Sequel::Model
|
146
|
-
#
|
152
|
+
# belongs_to :author
|
147
153
|
# end
|
148
|
-
#
|
154
|
+
#
|
149
155
|
# class Author < Sequel::Model
|
150
|
-
#
|
156
|
+
# has_many :posts
|
151
157
|
# end
|
152
158
|
#
|
153
|
-
#
|
159
|
+
# Another way to define an association in a Sequel model is as a regular
|
160
|
+
# instance method:
|
154
161
|
#
|
155
162
|
# class Post < Sequel::Model
|
156
|
-
#
|
163
|
+
# def author; Author[author_id]; end
|
157
164
|
# end
|
158
|
-
#
|
159
|
-
# post = Post.create(:name => 'hi!')
|
160
|
-
# post.author = Author[:name => 'Sharon']
|
161
|
-
#
|
162
|
-
# The one_to_many macro is roughly equivalent to ActiveRecord's has_many macro:
|
163
165
|
#
|
164
166
|
# class Author < Sequel::Model
|
165
|
-
#
|
167
|
+
# def posts; Post.filter(:author_id => pk); end
|
166
168
|
# end
|
167
169
|
#
|
168
|
-
# You will have noticed that in some cases the association macros are actually more verbose than hand-coding instance methods. The one_to_one and one_to_many macros also make assumptions (just like ActiveRecord macros) about the database schema which may not be relevant in many cases.
|
169
|
-
#
|
170
170
|
# === Caching model instances with memcached
|
171
171
|
#
|
172
172
|
# 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:
|
@@ -239,6 +239,7 @@ module Sequel
|
|
239
239
|
# Post.create_table! # drops the table if it exists and then recreates it
|
240
240
|
#
|
241
241
|
class Model
|
242
|
+
extend Associations
|
242
243
|
# Returns a string representation of the model instance including
|
243
244
|
# the class name and values.
|
244
245
|
def inspect
|
@@ -0,0 +1,317 @@
|
|
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
|
+
# belongs_to :portfolio
|
10
|
+
# has_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
|
+
#
|
17
|
+
# By default the classes for the associations are inferred from the association
|
18
|
+
# name, so for example the Project#portfolio will return an instance of
|
19
|
+
# Portfolio, and Project#milestones will return a dataset of Milestone
|
20
|
+
# instances, in similar fashion to how ActiveRecord infers class names.
|
21
|
+
#
|
22
|
+
# Association definitions are also reflected by the class, e.g.:
|
23
|
+
#
|
24
|
+
# >> Project.associations
|
25
|
+
# => [:portfolio, :milestones]
|
26
|
+
# >> Project.association_reflection(:portfolio)
|
27
|
+
# => {:kind => :many_to_one, :name => :portfolio, :class_name => "Portfolio"}
|
28
|
+
#
|
29
|
+
# The following association kinds are supported:
|
30
|
+
# * :many_to_one - Foreign key in current model's table points to
|
31
|
+
# associated model's primary key. Each associated model object can
|
32
|
+
# be associated with more than one current model objects. Each current
|
33
|
+
# model object can be associated with only one associated model object.
|
34
|
+
# Similar to ActiveRecord/DataMapper's belongs_to.
|
35
|
+
# * :one_to_many - Foreign key in associated model's table points to this
|
36
|
+
# model's primary key. Each current model object can be associated with
|
37
|
+
# more than one associated model objects. Each associated model object
|
38
|
+
# can be associated with only one current model object.
|
39
|
+
# Similar to ActiveRecord/DataMapper's has_many.
|
40
|
+
# * :many_to_many - A join table is used that has a foreign key that points
|
41
|
+
# to this model's primary key and a foreign key that points to the
|
42
|
+
# associated model's primary key. Each current model object can be
|
43
|
+
# associated with many associated model objects, and each associated
|
44
|
+
# model object can be associated with many current model objects.
|
45
|
+
# Similar to ActiveRecord/DataMapper's has_and_belongs_to_many.
|
46
|
+
#
|
47
|
+
# Associations can be defined by either using the associate method, or by
|
48
|
+
# calling one of the three methods: many_to_one, one_to_many, many_to_many.
|
49
|
+
# Sequel::Model also provides aliases for these methods that conform to
|
50
|
+
# ActiveRecord conventions: belongs_to, has_many, has_and_belongs_to_many.
|
51
|
+
# For example, the following two statements are equivalent:
|
52
|
+
#
|
53
|
+
# associate :one_to_many, :attributes
|
54
|
+
# one_to_many :attributes
|
55
|
+
module Sequel::Model::Associations
|
56
|
+
# Array of all association reflections
|
57
|
+
def all_association_reflections
|
58
|
+
association_reflections.values
|
59
|
+
end
|
60
|
+
|
61
|
+
# Associates a related model with the current model. The following types are
|
62
|
+
# supported:
|
63
|
+
#
|
64
|
+
# * :many_to_one - Foreign key in current model's table points to
|
65
|
+
# associated model's primary key. Each associated model object can
|
66
|
+
# be associated with more than one current model objects. Each current
|
67
|
+
# model object can be associated with only one associated model object.
|
68
|
+
# Similar to ActiveRecord/DataMapper's belongs_to.
|
69
|
+
# * :one_to_many - Foreign key in associated model's table points to this
|
70
|
+
# model's primary key. Each current model object can be associated with
|
71
|
+
# more than one associated model objects. Each associated model object
|
72
|
+
# can be associated with only one current model object.
|
73
|
+
# Similar to ActiveRecord/DataMapper's has_many.
|
74
|
+
# * :many_to_many - A join table is used that has a foreign key that points
|
75
|
+
# to this model's primary key and a foreign key that points to the
|
76
|
+
# associated model's primary key. Each current model object can be
|
77
|
+
# associated with many associated model objects, and each associated
|
78
|
+
# model object can be associated with many current model objects.
|
79
|
+
# Similar to ActiveRecord/DataMapper's has_and_belongs_to_many.
|
80
|
+
#
|
81
|
+
# The following options can be supplied:
|
82
|
+
# * *ALL types*:
|
83
|
+
# - :class_name - The name of the associated class as a string. If not
|
84
|
+
# given, uses the association's name, which is camelized (and
|
85
|
+
# singularized if type is :{one,many}_to_many)
|
86
|
+
# - :class - The associated class itself. Simpler than using
|
87
|
+
# :class_name, but can't be used always due to dependencies not being
|
88
|
+
# loaded.
|
89
|
+
# * :many_to_one:
|
90
|
+
# - :key - foreign_key in current model's table that references
|
91
|
+
# associated model's primary key, as a symbol. Defaults to :"#{name}_id".
|
92
|
+
# * :one_to_many:
|
93
|
+
# - :key - foreign key in associated model's table that references
|
94
|
+
# current model's primary key, as a symbol. Defaults to
|
95
|
+
# :"#{self.name.underscore}_id".
|
96
|
+
# - :order - the column by which to order the association dataset.
|
97
|
+
# - :cache - set to true to cache and return an array of objects instead of a dataset.
|
98
|
+
# * :many_to_many:
|
99
|
+
# - :join_table - name of table that includes the foreign keys to both
|
100
|
+
# the current model and the associated model, as a symbol. Defaults to the name
|
101
|
+
# of current model and name of associated model, pluralized,
|
102
|
+
# underscored, sorted, and joined with '_'.
|
103
|
+
# - :left_key - foreign key in join table that points to current model's
|
104
|
+
# primary key, as a symbol.
|
105
|
+
# - :right_key - foreign key in join table that points to associated
|
106
|
+
# model's primary key, as a symbol.
|
107
|
+
# - :order - the column by which to order the association dataset.
|
108
|
+
# - :cache - set to true to cache and return an array of objects instead of a dataset.
|
109
|
+
def associate(type, name, opts = {}, &block)
|
110
|
+
# check arguments
|
111
|
+
raise ArgumentError unless [:many_to_one, :one_to_many, :many_to_many].include?(type) && Symbol === name
|
112
|
+
|
113
|
+
# deprecation
|
114
|
+
if opts[:from]
|
115
|
+
STDERR << "The :from option is deprecated, please use the :class option instead.\r\n"
|
116
|
+
opts[:class] = opts[:from]
|
117
|
+
end
|
118
|
+
|
119
|
+
# prepare options
|
120
|
+
opts[:class_name] ||= opts[:class].name if opts[:class]
|
121
|
+
opts = association_reflections[name] = opts.merge(:type => type, :name => name, :block => block)
|
122
|
+
|
123
|
+
send(:"def_#{type}", name, opts)
|
124
|
+
end
|
125
|
+
|
126
|
+
# The association reflection hash for the association of the given name.
|
127
|
+
def association_reflection(name)
|
128
|
+
association_reflections[name]
|
129
|
+
end
|
130
|
+
|
131
|
+
# Array of association name symbols
|
132
|
+
def associations
|
133
|
+
association_reflections.keys
|
134
|
+
end
|
135
|
+
|
136
|
+
# Shortcut for adding a many_to_one association, see associate
|
137
|
+
def many_to_one(*args, &block)
|
138
|
+
associate(:many_to_one, *args, &block)
|
139
|
+
end
|
140
|
+
alias_method :belongs_to, :many_to_one
|
141
|
+
|
142
|
+
# Shortcut for adding a one_to_many association, see associate
|
143
|
+
def one_to_many(*args, &block)
|
144
|
+
associate(:one_to_many, *args, &block)
|
145
|
+
end
|
146
|
+
alias_method :has_many, :one_to_many
|
147
|
+
|
148
|
+
# deprecated, please use many_to_one instead
|
149
|
+
def one_to_one(*args, &block)
|
150
|
+
STDERR << "one_to_one relation definitions are deprecated, please use many_to_one instead.\r\n"
|
151
|
+
many_to_one(*args, &block)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Shortcut for adding a many_to_many association, see associate
|
155
|
+
def many_to_many(*args, &block)
|
156
|
+
associate(:many_to_many, *args, &block)
|
157
|
+
end
|
158
|
+
alias_method :has_and_belongs_to_many, :many_to_many
|
159
|
+
|
160
|
+
private
|
161
|
+
def association_ivar(name)
|
162
|
+
:"@#{name}"
|
163
|
+
end
|
164
|
+
|
165
|
+
def association_add_method_name(name)
|
166
|
+
:"add_#{name.to_s.singularize}"
|
167
|
+
end
|
168
|
+
|
169
|
+
def association_remove_method_name(name)
|
170
|
+
:"remove_#{name.to_s.singularize}"
|
171
|
+
end
|
172
|
+
|
173
|
+
def default_remote_key
|
174
|
+
:"#{name.demodulize.underscore}_id"
|
175
|
+
end
|
176
|
+
|
177
|
+
# The class related to the given association reflection
|
178
|
+
def associated_class(opts)
|
179
|
+
opts[:class] ||= opts[:class_name].constantize
|
180
|
+
end
|
181
|
+
|
182
|
+
# Hash storing the association reflections. Keys are association name
|
183
|
+
# symbols, values are association reflection hashes.
|
184
|
+
def association_reflections
|
185
|
+
@association_reflections ||= {}
|
186
|
+
end
|
187
|
+
|
188
|
+
def def_many_to_one(name, opts)
|
189
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
190
|
+
ivar = association_ivar(name)
|
191
|
+
|
192
|
+
key = (opts[:key] ||= :"#{name}_id")
|
193
|
+
opts[:class_name] ||= name.to_s.camelize
|
194
|
+
|
195
|
+
def_association_getter(name) {(fk = send(key)) ? assoc_class[opts][fk] : nil}
|
196
|
+
class_def(:"#{name}=") do |o|
|
197
|
+
instance_variable_set(ivar, o)
|
198
|
+
send(:"#{key}=", (o.pk if o))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def def_one_to_many(name, opts)
|
203
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
204
|
+
ivar = association_ivar(name)
|
205
|
+
key = (opts[:key] ||= default_remote_key)
|
206
|
+
opts[:class_name] ||= name.to_s.singularize.camelize
|
207
|
+
|
208
|
+
def_association_dataset_methods(name, opts) {assoc_class[opts].filter(key => pk)}
|
209
|
+
|
210
|
+
# define add_xxx, remove_xxx methods
|
211
|
+
class_def(association_add_method_name(name)) do |o|
|
212
|
+
o.send(:"#{key}=", pk); o.save!
|
213
|
+
if arr = instance_variable_get(ivar)
|
214
|
+
arr.push(o)
|
215
|
+
end
|
216
|
+
o
|
217
|
+
end
|
218
|
+
class_def(association_remove_method_name(name)) do |o|
|
219
|
+
o.send(:"#{key}=", nil); o.save!
|
220
|
+
if arr = instance_variable_get(ivar)
|
221
|
+
arr.delete(o)
|
222
|
+
end
|
223
|
+
o
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def default_join_table_name(opts)
|
228
|
+
([opts[:class_name], self.name.demodulize]. \
|
229
|
+
map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
|
230
|
+
end
|
231
|
+
|
232
|
+
def def_many_to_many(name, opts)
|
233
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
234
|
+
ivar = association_ivar(name)
|
235
|
+
left = (opts[:left_key] ||= default_remote_key)
|
236
|
+
right = (opts[:right_key] ||= :"#{name.to_s.singularize}_id")
|
237
|
+
opts[:class_name] ||= name.to_s.singularize.camelize
|
238
|
+
join_table = (opts[:join_table] ||= default_join_table_name(opts))
|
239
|
+
database = db
|
240
|
+
|
241
|
+
def_association_dataset_methods(name, opts) do
|
242
|
+
klass = assoc_class[opts]
|
243
|
+
key = (opts[:right_primary_key] ||= :"#{klass.table_name}__#{klass.primary_key}")
|
244
|
+
klass.inner_join(join_table, right => key, left => pk)
|
245
|
+
end
|
246
|
+
|
247
|
+
class_def(association_add_method_name(name)) do |o|
|
248
|
+
database[join_table].insert(left => pk, right => o.pk)
|
249
|
+
if arr = instance_variable_get(ivar)
|
250
|
+
arr.push(o)
|
251
|
+
end
|
252
|
+
o
|
253
|
+
end
|
254
|
+
class_def(association_remove_method_name(name)) do |o|
|
255
|
+
database[join_table].filter(left => pk, right => o.pk).delete
|
256
|
+
if arr = instance_variable_get(ivar)
|
257
|
+
arr.delete(o)
|
258
|
+
end
|
259
|
+
o
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Defines an association getter method, caching the block result in an
|
264
|
+
# instance variable. The defined method takes an optional reload parameter
|
265
|
+
# that can be set to true in order to bypass the cache.
|
266
|
+
def def_association_getter(name, &block)
|
267
|
+
ivar = association_ivar(name)
|
268
|
+
class_def(name) do |*reload|
|
269
|
+
if !reload[0] && obj = instance_variable_get(ivar)
|
270
|
+
obj
|
271
|
+
else
|
272
|
+
instance_variable_set(ivar, instance_eval(&block))
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Defines an association
|
278
|
+
def def_association_dataset_methods(name, opts, &block)
|
279
|
+
dataset_method = :"#{name}_dataset"
|
280
|
+
dataset_block = opts[:block]
|
281
|
+
ivar = association_ivar(name)
|
282
|
+
|
283
|
+
# define a method returning the association dataset (with optional order)
|
284
|
+
if order = opts[:order]
|
285
|
+
class_def(dataset_method) {instance_eval(&block).order(order)}
|
286
|
+
else
|
287
|
+
class_def(dataset_method, &block)
|
288
|
+
end
|
289
|
+
|
290
|
+
if opts[:cache]
|
291
|
+
# if the :cache option is set to true, the association method should return
|
292
|
+
# an array of association objects
|
293
|
+
class_def(name) do |*reload|
|
294
|
+
if !reload[0] && obj = instance_variable_get(ivar)
|
295
|
+
obj
|
296
|
+
else
|
297
|
+
ds = send(dataset_method)
|
298
|
+
# if the a dataset block was specified, we need to call it and use
|
299
|
+
# the result as the dataset to fetch records from.
|
300
|
+
if dataset_block
|
301
|
+
ds = dataset_block[ds]
|
302
|
+
end
|
303
|
+
instance_variable_set(ivar, ds.all)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
elsif dataset_block
|
307
|
+
# no cache, but we still need to check if a dataset block was given.
|
308
|
+
# define helper so the supplied block will be instance_eval'ed
|
309
|
+
class_def(:"#{name}_helper", &dataset_block)
|
310
|
+
class_def(name) {send(:"#{name}_helper", send(dataset_method))}
|
311
|
+
else
|
312
|
+
# otherwise (by default), the association method is an alias to the
|
313
|
+
# association dataset method.
|
314
|
+
alias_method name, dataset_method
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|