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 +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
|