sequel 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,4 +1,40 @@
1
- === HEAD
1
+ === 2.2.0 (2008-07-05)
2
+
3
+ * Add :extend association option, extending the dataset with module(s) (jeremyevans)
4
+
5
+ * Add :after_load association callback option, called after associated objects have been loaded from the database (jeremyevans)
6
+
7
+ * Make validation methods support a :tag option, to work correctly with source reloading (jeremyevans)
8
+
9
+ * Add :before_add, :after_add, :before_remove, :after_remove association callback options (jeremyevans)
10
+
11
+ * Break many_to_one association setter method in two parts, for easier overriding (jeremyevans)
12
+
13
+ * Model.validates_presence_of now considers false as present instead of absent (jeremyevans)
14
+
15
+ * Add Model.raise_on_save_failure, raising errors on save failure instead of return false (now nil), default to true (jeremyevans)
16
+
17
+ * Add :eager_loader association option, to specify code to be run when eager loading (jeremyevans)
18
+
19
+ * Make :many_to_one associations support :dataset, :order, :limit association options, as well as block arguments (jeremyevans)
20
+
21
+ * Add :dataset association option, which overrides the default base dataset to use (jeremyevans)
22
+
23
+ * Add :eager_graph association option, works just like :eager except it uses #eager_graph (jeremyevans)
24
+
25
+ * Add :graph_join_table_join_type association option (jeremyevans)
26
+
27
+ * Add :graph_only_conditions and :graph_join_table_only_conditions association options (jeremyevans)
28
+
29
+ * Add :graph_block and :graph_join_table_block association options (jeremyevans)
30
+
31
+ * Set the model's dataset's columns in addition to the model's columns when loading the schema for a model (jeremyevans)
32
+
33
+ * Make caching work correctly with subclasses (jeremyevans)
34
+
35
+ * Add the Model.to_hash dataset method (jeremyevans)
36
+
37
+ === 2.1.0 (2008-06-17)
2
38
 
3
39
  * Break association add_/remove_/remove_all_ methods into two parts, for easier overriding (jeremyevans)
4
40
 
data/Rakefile CHANGED
@@ -9,8 +9,8 @@ include FileUtils
9
9
  # Configuration
10
10
  ##############################################################################
11
11
  NAME = "sequel"
12
- VERS = "2.1.0"
13
- SEQUEL_CORE_VERS= "2.1.0"
12
+ VERS = "2.2.0"
13
+ SEQUEL_CORE_VERS= "2.2.0"
14
14
  CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
15
15
  RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
16
16
  'Sequel: The Database Toolkit for Ruby: Model Classes', '--main', 'README']
@@ -0,0 +1,624 @@
1
+ = Advanced Associations
2
+
3
+ Sequel::Model has the most powerful and flexible associations of any ruby ORM.
4
+
5
+ "Extraordinary claims require extraordinary proof" - Carl Sagan
6
+
7
+ ==Background: Sequel::Model association options
8
+
9
+ There are a bunch of advanced association options that are available to
10
+ handle the other-than-bog-standard cases. First we'll go over some of
11
+ the simpler ones:
12
+
13
+ All associations take a block that can be used to further filter/modify the
14
+ default dataset. There's also an :eager_block option if you want to use
15
+ a different block when eager loading via Dataset#eager. Association blocks are
16
+ useful for things like:
17
+
18
+ Artist.one_to_many :gold_albums, :class=>:Album do |ds|
19
+ ds.filter(:copies_sold > 500000)
20
+ end
21
+
22
+ There are a whole bunch of options for changing how the association is eagerly
23
+ loaded via Dataset#eager_graph: :graph_block, :graph_conditions,
24
+ :graph_only_conditions, :graph_join_type (and :graph_join_table_* ones for
25
+ JOINing to the join table in a many_to_many association).
26
+
27
+ - :graph_join_type - The type of join to do
28
+ - :graph_conditions - Additional conditions to put on join (needs to be a
29
+ hash or array of all two pairs). Automatically assumes unqualified symbols
30
+ as first element of the pair to be columns of the associated model, and
31
+ unqualified symbols of the second element of the pair to be columns of the
32
+ current model.
33
+ - :graph_block - A block passed to join_table, allowing you to specify
34
+ conditions other than equality, or to use OR, or set up any arbitrary
35
+ condition. The block is passed the associated table alias, current model
36
+ alias, and array of previous joins.
37
+ - :graph_only_conditions - Use these conditions instead of the standard
38
+ association conditions. This is necessary when the standard keys it uses are
39
+ not correct for the association (such as an association that doesn't use
40
+ primary keys). You can also use this to have a JOIN USING (array of
41
+ symbols), or a NATURAL or CROSS JOIN (nil, with the appropriate
42
+ :graph_join_type).
43
+
44
+ These can be used like this:
45
+
46
+ # Makes Artist.eager_graph(:required_albums).all not return artists that
47
+ # don't have any albums
48
+ Artist.one_to_many :required_albums, :class=>:Album, :graph_join_type=>:inner
49
+
50
+ # Makes sure all returned albums have the active flag set
51
+ Artist.one_to_many :active_albums, :class=>:Album, \
52
+ :graph_conditions=>{:active=>true}
53
+
54
+ # Only returns albums that have sold more than 500,000 copies
55
+ Artist.one_to_many :gold_albums, :class=>:Album, \
56
+ :graph_block=>proc{|j,lj,js| :copies_sold.qualify(j) > 500000}
57
+
58
+ # Handles the case where the artist is associated to the album by an
59
+ # artist_name column in the albums table, when name is not the primary key
60
+ # of the artists table
61
+ Artist.one_to_many :albums, :key=>:artist_name, \
62
+ :graph_only_conditions=>{:artist_name=>:name}
63
+
64
+ # Handles the above case, but where :artist_name is used in both tables,
65
+ # via a JOIN USING
66
+ Artist.one_to_many :albums, :key=>:artist_name, :graph_only_conditions=>[:artist_name]
67
+
68
+ # Handles the case where all columns in both tables are uniquely named, except
69
+ # for the ones that handle associations
70
+ Artist.one_to_many :albums, :key=>:artist_name, :graph_only_conditions=>nil, \
71
+ :graph_join_type=>:natural
72
+
73
+ Remember, using #eager_graph is generally only necessary when you need to
74
+ filter/order based on columns in an associated table, it is recommended to
75
+ use #eager for eager loading if possible.
76
+
77
+ For lazy loading (e.g. Model[1].association), the :dataset option can be used
78
+ to specify an arbitrary dataset (one that uses different keys, multiple keys,
79
+ joins to other tables, etc.).
80
+
81
+ For eager loading via #eager, the :eager_loader option can be used to specify
82
+ how to eagerly load a complex association. This is an extremely powerful
83
+ option. Though it can often be verbose (compared to other things in Sequel),
84
+ it allows you complete control over how to eagerly load associations for a
85
+ group of objects.
86
+
87
+ :eager_loader should be a proc that takes 3 arguments, a key_hash,
88
+ an array of records, and a hash of dependent associations. Since you
89
+ are given all of the records, you can do things like filter on
90
+ associations that are specified by multiple keys, or do multiple
91
+ queries depending on the content of the records (which would be
92
+ necessary for polymorphic associations). Inside the :eager_loader
93
+ proc, you should get the related objects and populate the
94
+ associations for all objects in the array of records. The hash
95
+ of dependent associations is available for you to cascade the eager
96
+ loading down multiple levels, but it is up to you to use it. The
97
+ key_hash is a performance enhancement that is used by the default
98
+ code and is also available to you. It is a hash with keys being
99
+ foreign/primary key symbols in the current table, and the values
100
+ being hashes where the key is foreign/primary key values and values
101
+ being arrays of current model objects having the foreign/primary key
102
+ value associated with the key. This is hard to visualize, so I'll
103
+ give an example:
104
+
105
+ album1 = Album.load(:id=>1, :artist_id=>2)
106
+ album2 = Album.load(:id=>3, :artist_id=>2)
107
+ Album.many_to_one :artist
108
+ Album.one_to_many :tracks
109
+ Album.eager(:band, :tracks).all
110
+ # The key_hash provided to the :eager_loader proc would be:
111
+ {:id=>{1=>[album1], 3=>[album2]}, :artist_id=>{2=>[album1, album2]}}
112
+
113
+ Using these options, you can build associations Sequel doesn't natively support,
114
+ and still be able to use the same eager loading features that you are used to.
115
+
116
+ ==ActiveRecord associations
117
+
118
+ Sequel supports all of associations that ActiveRecord supports, one way or
119
+ another. Sometimes this requires more code, as Sequel is a toolkit and not
120
+ a swiss army chainsaw.
121
+
122
+ ===Association callbacks
123
+
124
+ Sequel supports the same callbacks that ActiveRecord does: :before_add,
125
+ :before_remove, :after_add, and :after_remove. It also supports a
126
+ callback that ActiveRecord does not, :after_load, which is called
127
+ after the association has been loaded (when lazy loading).
128
+
129
+ Each of these options can be a Symbol specifying an instance method
130
+ that takes one argument (the associated object), or a Proc that takes
131
+ two arguments (the current object and the associated object), or an
132
+ array of Symbols and Procs. For :after_load with a *_to_many association,
133
+ the associated object argument is an array of associated objects.
134
+
135
+ If any of the before callbacks return false, the adding/removing
136
+ does not happen and it either raises an error (the default), or
137
+ returns nil (if raise_on_save_failure is false).
138
+
139
+ All callbacks are also run on many_to_one associations. If there
140
+ was already an existing object for the association, it calls the
141
+ remove callbacks on the existing object and the add callbacks on the
142
+ new object. The remove callback calls are placed around the add
143
+ callback calls.
144
+
145
+ ===Association extensions
146
+
147
+ All associations come with a _dataset method that can be further filtered or
148
+ otherwise modified:
149
+
150
+ class Author < Sequel::Model
151
+ one_to_many :authorships
152
+ end
153
+ Author.first.authorships_dataset.filter(:number < 10).first
154
+
155
+ You can extend a dataset with a module easily with :extend:
156
+
157
+ module FindOrCreate
158
+ def find_or_create(vals)
159
+ first(vals) || @opts[:models][nil].create(vals)
160
+ end
161
+ end
162
+ class Author < Sequel::Model
163
+ one_to_many :authorships, :extend=>FindOrCreate
164
+ end
165
+ Author.first.authorships_dataset.find_or_create(:name=>'Blah', :number=>10)
166
+
167
+ However, note that the dataset doesn't have any knowledge of the model object
168
+ that created it via the association, so if you want to use attributes of the
169
+ model object, you'll have to use a closure:
170
+
171
+ class Author < Sequel::Model
172
+ one_to_many :authorships, :dataset=>(proc do
173
+ key = pk
174
+ ds = Authorship.filter(:author_id=>key)
175
+ ds.meta_def(:find_or_create_by_name) do |name|
176
+ first(:name=>name) || Authorship.create(:name=>name, :author_id=>key)
177
+ end
178
+ ds
179
+ end)
180
+ end
181
+ Author.first.authorships_dataset.find_or_create_by_name('Bob')
182
+
183
+ You can cheat if you want to:
184
+
185
+ module FindOrCreate
186
+ def find_or_create(vals)
187
+ # Exploits the fact that Sequel filters are ruby objects that
188
+ # can be introspected.
189
+ author_id = @opts[:where].args[1]
190
+ first(vals) || \
191
+ @opts[:models][nil].create(vals.merge(:author_id=>author_id))
192
+ end
193
+ end
194
+
195
+ ===has_many :through associations
196
+
197
+ many_to_many handles the usual case of a has_many :through with a belongs_to in
198
+ the associated model. It doesn't break on the case where the join table is a
199
+ model table, unlike ActiveRecord's has_and_belongs_to_many.
200
+
201
+ ActiveRecord:
202
+
203
+ class Author < ActiveRecord::Base
204
+ has_many :authorships
205
+ has_many :books, :through => :authorships
206
+ end
207
+
208
+ class Authorship < ActiveRecord::Base
209
+ belongs_to :author
210
+ belongs_to :book
211
+ end
212
+
213
+ @author = Author.find :first
214
+ @author.books
215
+
216
+ Sequel::Model:
217
+
218
+ class Author < Sequel::Model
219
+ one_to_many :authorships
220
+ many_to_many :books, :join_table=>:authorships
221
+ end
222
+
223
+ class Authorship < Sequel::Model
224
+ many_to_one :author
225
+ many_to_one :book
226
+ end
227
+
228
+ @author = Author.first
229
+ @author.books
230
+
231
+ If you use an association other than belongs_to in the associated model, things
232
+ are a bit more involved:
233
+
234
+ ActiveRecord:
235
+
236
+ class Firm < ActiveRecord::Base
237
+ has_many :clients
238
+ has_many :invoices, :through => :clients
239
+ end
240
+
241
+ class Client < ActiveRecord::Base
242
+ belongs_to :firm
243
+ has_many :invoices
244
+ end
245
+
246
+ class Invoice < ActiveRecord::Base
247
+ belongs_to :client
248
+ has_one :firm, :through => :client
249
+ end
250
+
251
+ Firm.find(:first).invoices
252
+
253
+ Sequel::Model:
254
+
255
+ class Firm < Sequel::Model
256
+ one_to_many :clients
257
+ one_to_many :invoices, :read_only=>true, \
258
+ :dataset=>proc{Invoice.eager_graph(:client).filter(:client__firm_id=>pk)}, \
259
+ :after_load=>(proc do |firm, invs|
260
+ invs.each do |inv|
261
+ inv.client.associations[:firm] = inv.associations[:firm] = firm
262
+ end
263
+ end), \
264
+ :eager_loader=>(proc do |key_hash, firms, associations|
265
+ id_map = key_hash[Firm.primary_key]
266
+ firms.each{|firm| firm.associations[:invoices] = []}
267
+ Invoice.eager_graph(:client).filter(:client__firm_id=>id_map.keys).all do |inv|
268
+ id_map[inv.client.firm_id].each do |firm|
269
+ inv.client.associations[:firm] = inv.associations[:firm] = firm
270
+ firm.associations[:invoices] << inv
271
+ end
272
+ end
273
+ end)
274
+ end
275
+
276
+ class Client < Sequel::Model
277
+ many_to_one :firm
278
+ one_to_many :invoices
279
+ end
280
+
281
+ class Invoice < Sequel::Model
282
+ many_to_one :client
283
+ many_to_one :firm, :key=>nil, :read_only=>true, \
284
+ :dataset=>proc{Firm.eager_graph(:clients).filter(:clients__id=>client_id)}, \
285
+ :after_load=>(proc do |inv, firm|
286
+ # Delete the cached associations from firm, because it only has the
287
+ # client with this invoice, instead of all clients of the firm
288
+ inv.associations[:client] = firm.associations.delete(:clients).first
289
+ end), \
290
+ :eager_loader=>(proc do |key_hash, invoices, associations|
291
+ id_map = {}
292
+ invoices.each do |inv|
293
+ inv.associations[:firm] = nil
294
+ inv.associations[:client] = nil
295
+ (id_map[inv.client_id] ||= []) << inv
296
+ end
297
+ Firm.eager_graph(:clients).filter(:clients__id=>id_map.keys).all do |firm|
298
+ # Delete the cached associations from firm, because it only has the
299
+ # clients related the invoices being eagerly loaded, instead of all
300
+ # clients of the firm.
301
+ firm.associations.delete(:clients).each do |client|
302
+ id_map[client.pk].each do |inv|
303
+ inv.associations[:firm] = firm
304
+ inv.associations[:client] = client
305
+ end
306
+ end
307
+ end
308
+ end)
309
+ end
310
+ Firm.find(:first).invoices
311
+
312
+ It is significantly more code in Sequel Model, but quite a bit of it is setting
313
+ the intermediate associate record (the client) and the reciprocal association
314
+ in the associations cache for each object, which ActiveRecord won't do for you.
315
+ The reason you would want to do this is that then firm.invoices.first.firm or
316
+ firm.invoices.first.client doesn't do another query to get the firm/client.
317
+
318
+ ===Polymorphic Associations
319
+
320
+ Polymorphic associations are really a design flaw, but if you are stuck with
321
+ them, Sequel can handle it.
322
+
323
+ ActiveRecord:
324
+
325
+ class Asset < ActiveRecord::Base
326
+ belongs_to :attachable, :polymorphic => true
327
+ end
328
+
329
+ class Post < ActiveRecord::Base
330
+ has_many :assets, :as => :attachable
331
+ end
332
+
333
+ class Note < ActiveRecord::Base
334
+ has_many :assets, :as => :attachable
335
+ end
336
+
337
+ @asset.attachable = @post
338
+ @asset.attachable = @note
339
+
340
+ Sequel::Model:
341
+
342
+ class Asset < Sequel::Model
343
+ many_to_one :attachable, :reciprocal=>:assets, \
344
+ :dataset=>(proc do
345
+ klass = attachable_type.constantize
346
+ klass.filter(klass.primary_key=>attachable_id)
347
+ end), \
348
+ :eager_loader=>(proc do |key_hash, assets, associations|
349
+ id_map = {}
350
+ assets.each do |asset|
351
+ asset.associations[:attachable] = nil
352
+ ((id_map[asset.attachable_type] ||= {})[asset.attachable_id] ||= []) << asset
353
+ end
354
+ id_map.each do |klass_name, id_map|
355
+ klass = klass_name.constantize
356
+ klass.filter(klass.primary_key=>id_map.keys).all do |attach|
357
+ id_map[attach.pk].each do |asset|
358
+ asset.associations[:attachable] = attach
359
+ end
360
+ end
361
+ end
362
+ end)
363
+
364
+ private
365
+
366
+ def _attachable=(attachable)
367
+ self[:attachable_id] = (attachable.pk if attachable)
368
+ self[:attachable_type] = (attachable.class.name if attachable)
369
+ end
370
+ end
371
+
372
+ class Post < Sequel::Model
373
+ one_to_many :assets, :key=>:attachable_id do |ds|
374
+ ds.filter(:attachable_type=>'Post')
375
+ end
376
+
377
+ private
378
+
379
+ def _add_asset(asset)
380
+ asset.attachable_id = pk
381
+ asset.attachable_type = 'Post'
382
+ asset.save
383
+ end
384
+ def _remove_asset(asset)
385
+ asset.attachable_id = nil
386
+ asset.attachable_type = nil
387
+ asset.save
388
+ end
389
+ def _remove_all_assets
390
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Post')\
391
+ .update(:attachable_id=>nil, :attachable_type=>nil)
392
+ end
393
+ end
394
+
395
+ class Note < Sequel::Model
396
+ one_to_many :assets, :key=>:attachable_id do |ds|
397
+ ds.filter(:attachable_type=>'Note')
398
+ end
399
+
400
+ private
401
+
402
+ def _add_asset(asset)
403
+ asset.attachable_id = pk
404
+ asset.attachable_type = 'Note'
405
+ asset.save
406
+ end
407
+ def _remove_asset(asset)
408
+ asset.attachable_id = nil
409
+ asset.attachable_type = nil
410
+ asset.save
411
+ end
412
+ def _remove_all_assets
413
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Note')\
414
+ .update(:attachable_id=>nil, :attachable_type=>nil)
415
+ end
416
+ end
417
+
418
+ @asset.attachable = @post
419
+ @asset.attachable = @note
420
+
421
+ ==More advanced associations
422
+
423
+ So far, we've only shown that Sequel::Model has associations as powerful as
424
+ ActiveRecord's. Now we will show how Sequel::Model's associations are more
425
+ powerful.
426
+
427
+ ===many_to_one/one_to_many not referencing primary key
428
+
429
+ Let's say you have two tables, invoices and clients, where each client is
430
+ associated with many invoices. However, instead of using the client's primary
431
+ key, the invoice is associated to the client by name (this is bad database
432
+ design, but sometimes you have to play with the cards you are dealt).
433
+
434
+ class Client < Sequel::Model
435
+ one_to_many :invoices, :reciprocal=>:client, \
436
+ :dataset=>proc{Invoice.filter(:client_name=>name)}, \
437
+ :eager_loader=>(proc do |key_hash, clients, associations|
438
+ id_map = {}
439
+ clients.each do |client|
440
+ id_map[client.name] = client
441
+ client.associations[:invoices] = []
442
+ end
443
+ Invoice.filter(:client_name=>id_map.keys.sort).all do |inv|
444
+ inv.associations[:client] = client = id_map[inv.client_name]
445
+ client.associations[:invoices] << inv
446
+ end
447
+ end)
448
+
449
+ private
450
+
451
+ def _add_invoice(invoice)
452
+ invoice.client_name = name
453
+ invoice.save
454
+ end
455
+ def _remove_invoice(invoice)
456
+ invoice.client_name = nil
457
+ invoice.save
458
+ end
459
+ def _remove_all_invoices
460
+ Invoice.filter(:client_name=>name).update(:client_name=>nil)
461
+ end
462
+ end
463
+
464
+ class Invoice < Sequel::Model
465
+ many_to_one :client, :key=>:client_name, \
466
+ :dataset=>proc{Client.filter(:name=>client_name)}, \
467
+ :eager_loader=>(proc do |key_hash, invoices, associations|
468
+ id_map = key_hash[:client_name]
469
+ invoices.each{|inv| inv.associations[:client] = nil}
470
+ Client.filter(:name=>id_map.keys).all do |client|
471
+ id_map[client.name].each{|inv| inv.associations[:client] = client}
472
+ end
473
+ end)
474
+
475
+ private
476
+
477
+ def _client=(client)
478
+ self.client_name = (client.name if client)
479
+ end
480
+ end
481
+
482
+ ===Joining on multiple keys
483
+
484
+ Let's say you have two tables that are associated with each other with multiple
485
+ keys. For example:
486
+
487
+ # Both of these models have an album_id, number, and disc_number fields.
488
+ # All FavoriteTracks have an associated track, but not all tracks have an
489
+ # associated favorite track
490
+
491
+ class Track < Sequel::Model
492
+ many_to_one :favorite_track, \
493
+ :dataset=>(proc do
494
+ FavoriteTrack.filter(:disc_number=>disc_number, :number=>number, :album_id=>album_id)
495
+ end), \
496
+ :eager_loader=>(proc do |key_hash, tracks, associations|
497
+ id_map = {}
498
+ tracks.each do |t|
499
+ t.associations[:favorite_track] = nil
500
+ id_map[[t[:album_id], t[:disc_number], t[:number]]] = t
501
+ end
502
+ FavoriteTrack.filter([:album_id, :disc_number, :number]=>id_map.keys).all do |ft|
503
+ if t = id_map[[ft[:album_id], ft[:disc_number], ft[:number]]]
504
+ t.associations[:favorite_track] = ft
505
+ end
506
+ end
507
+ end)
508
+ end
509
+
510
+ class FavoriteTrack < Sequel::Model
511
+ many_to_one :track, \
512
+ :dataset=>(proc do
513
+ Track.filter(:disc_number=>disc_number, :number=>number, :album_id=>album_id)
514
+ end), \
515
+ :eager_loader=>(proc do |key_hash, ftracks, associations|
516
+ id_map = {}
517
+ ftracks.each{|ft| id_map[[ft[:album_id], ft[:disc_number], ft[:number]]] = ft}
518
+ Track.filter([:album_id, :disc_number, :number]=>id_map.keys).all do |t|
519
+ id_map[[t[:album_id], t[:disc_number], t[:number]]].associations[:track] = t
520
+ end
521
+ end)
522
+ end
523
+
524
+ ===Tree - All Ancestors and Descendents
525
+
526
+ Let's say you want to store a tree relationship in your database, it's pretty
527
+ simple:
528
+
529
+ class Node < Sequel::Model
530
+ many_to_one :parent
531
+ one_to_many :children, :key=>:parent_id
532
+ end
533
+
534
+ You can easily get a node's parent with node.parent, and a node's children with
535
+ node.children. You can even eager load the relationship up to a certain depth:
536
+
537
+ # Eager load three generations of generations of children for a given node
538
+ Node.filter(:id=>1).eager(:children=>{:children=>:children}).all.first
539
+ # Load parents and grandparents for a group of nodes
540
+ Node.filter(:id < 10).eager(:parent=>:parent).all
541
+
542
+ What if you want to get all ancestors up to the root node, or all descendents,
543
+ without knowing the depth of the tree?
544
+
545
+ class Node < Sequel::Model
546
+ many_to_one :ancestors, :eager_loader=>(proc do |key_hash, nodes, associations|
547
+ # Handle cases where the root node has the same parent_id as primary_key
548
+ # and also when it is NULL
549
+ non_root_nodes = nodes.reject do |n|
550
+ if [nil, n.pk].include?(n.parent_id)
551
+ # Make sure root nodes have their parent association set to nil
552
+ n.associations[:parent] = nil
553
+ true
554
+ else
555
+ false
556
+ end
557
+ end
558
+ unless non_root_nodes.empty?
559
+ id_map = {}
560
+ # Create an map of parent_ids to nodes that have that parent id
561
+ non_root_nodes.each{|n| (id_map[n.parent_id] ||= []) << n}
562
+ # Doesn't cause an infinte loop, because when only the root node
563
+ # is left, this is not called.
564
+ Node.filter(Node.primary_key=>id_map.keys).eager(:ancestors).all do |node|
565
+ # Populate the parent association for each node
566
+ id_map[node.pk].each{|n| n.associations[:parent] = node}
567
+ end
568
+ end
569
+ end)
570
+ many_to_one :descendants, :eager_loader=>(proc do |key_hash, nodes, associations|
571
+ id_map = {}
572
+ nodes.each do |n|
573
+ # Initialize an empty array of child associations for each parent node
574
+ n.associations[:children] = []
575
+ # Populate identity map of nodes
576
+ id_map[n.pk] = n
577
+ end
578
+ # Doesn't cause an infinite loop, because the :eager_loader is not called
579
+ # if no records are returned. Exclude id = parent_id to avoid infinite loop
580
+ # if the root note is one of the returned records and it has parent_id = id
581
+ # instead of parent_id = NULL.
582
+ Node.filter(:parent_id=>id_map.keys).exclude(:id=>:parent_id).eager(:descendants).all do |node|
583
+ # Get the parent from the identity map
584
+ parent = id_map[node.parent_id]
585
+ # Set the child's parent association to the parent
586
+ node.associations[:parent] = parent
587
+ # Add the child association to the array of children in the parent
588
+ parent.associations[:children] << node
589
+ end
590
+ end)
591
+ end
592
+
593
+
594
+ ===Joining multiple keys to a single key, through a third table
595
+
596
+ Let's say you have a database, of songs, lyrics, and artists. Each song
597
+ may or may not have a lyric (most songs are instrumental). The lyric can be
598
+ associated to an artist in each of four ways: composer, arranger, vocalist,
599
+ or lyricist. These may all be the same, or they could all be different, and
600
+ none of them are required. The songs table has a lyric_id field to associate
601
+ it to the lyric, and the lyric table has four fields to associate it to the
602
+ artist (composer_id, arranger_id, vocalist_id, and lyricist_id).
603
+
604
+ What you want to do is get all songs for a given artist, ordered by the song's
605
+ name, with no duplicates?
606
+
607
+ class Artist < Sequel::Model
608
+ one_to_many :songs, :order=>:songs__name, \
609
+ :dataset=>proc{Song.select(:songs.*).join(Lyric, :id=>:lyric_id, id=>[:composer_id, :arranger_id, :vocalist_id, :lyricist_id])}, \
610
+ :eager_loader=>(proc do |key_hash, records, associations|
611
+ h = key_hash[:id]
612
+ ids = h.keys
613
+ records.each{|r| r.associations[:songs] = []}
614
+ Song.select(:songs.*, :lyrics__composer_id, :lyrics__arranger_id, :lyrics__vocalist_id, :lyrics__lyricist_id)\
615
+ .join(Lyric, :id=>:lyric_id){{:composer_id=>ids, :arranger_id=>ids, :vocalist_id=>ids, :lyricist_id=>ids}.sql_or}\
616
+ .order(:songs__name).all do |song|
617
+ [:composer_id, :arranger_id, :vocalist_id, :lyricist_id].each do |x|
618
+ recs = h[song.values.delete(x)]
619
+ recs.each{|r| r.associations[:songs] << song} if recs
620
+ end
621
+ end
622
+ records.each{|r| r.associations[:songs].uniq!}
623
+ end)
624
+ end