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 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: Concise ORM for Ruby
1
+ == Sequel: The Database Toolkit for Ruby
2
2
 
3
- Sequel is an ORM framework for Ruby. Sequel provides thread safety, connection pooling, and a concise DSL for constructing queries and table schemas.
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 was 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.
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.4.2"
12
+ VERS = "0.5"
13
13
  CLEAN.include ["**/.*.sw?", "pkg/*", ".config", "doc/*", "coverage/*"]
14
14
  RDOC_OPTS = [
15
15
  "--quiet",
16
- "--title", "Sequel Model: Lightweight ORM for Ruby",
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: Lightweight ORM for Ruby"
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 = "Lightweight ORM for Ruby"
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"
@@ -6,7 +6,7 @@ end
6
6
 
7
7
  # TODO: add relationships when complete:
8
8
  files = %w[
9
- base hooks record schema relations
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 separately, but Model.delete deletes all relevant records with a single SQL statement.
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
- # The most straightforward way to define an association in a Sequel model is as a regular instance method:
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
- # def author; Author[author_id]; end
152
+ # belongs_to :author
147
153
  # end
148
- #
154
+ #
149
155
  # class Author < Sequel::Model
150
- # def posts; Post.filter(:author_id => pk); end
156
+ # has_many :posts
151
157
  # end
152
158
  #
153
- # Sequel also provides two macros to assist with common types of associations. The one_to_one macro is roughly equivalent to ActiveRecord?'s belongs_to macro. It defines both getter and setter methods for the association:
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
- # one_to_one :author, :from => Author
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
- # one_to_many :posts, :from => Post, :key => :author_id
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