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 +37 -1
- data/Rakefile +2 -2
- data/doc/advanced_associations.rdoc +624 -0
- data/lib/sequel_model.rb +7 -2
- data/lib/sequel_model/association_reflection.rb +112 -2
- data/lib/sequel_model/associations.rb +171 -222
- data/lib/sequel_model/base.rb +28 -7
- data/lib/sequel_model/eager_loading.rb +12 -71
- data/lib/sequel_model/record.rb +143 -14
- data/lib/sequel_model/validations.rb +108 -17
- data/spec/association_reflection_spec.rb +10 -2
- data/spec/associations_spec.rb +449 -94
- data/spec/caching_spec.rb +72 -11
- data/spec/eager_loading_spec.rb +168 -21
- data/spec/hooks_spec.rb +53 -15
- data/spec/model_spec.rb +12 -11
- data/spec/spec_helper.rb +0 -7
- data/spec/validations_spec.rb +39 -16
- metadata +6 -4
data/CHANGELOG
CHANGED
@@ -1,4 +1,40 @@
|
|
1
|
-
===
|
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.
|
13
|
-
SEQUEL_CORE_VERS= "2.
|
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
|