sequel 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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