colincasey-sequel 2.10.0 → 2.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. data/CHANGELOG +7 -1
  2. data/doc/advanced_associations.rdoc +614 -0
  3. data/doc/cheat_sheet.rdoc +223 -0
  4. data/doc/dataset_filtering.rdoc +158 -0
  5. data/doc/prepared_statements.rdoc +104 -0
  6. data/doc/release_notes/1.0.txt +38 -0
  7. data/doc/release_notes/1.1.txt +143 -0
  8. data/doc/release_notes/1.3.txt +101 -0
  9. data/doc/release_notes/1.4.0.txt +53 -0
  10. data/doc/release_notes/1.5.0.txt +155 -0
  11. data/doc/release_notes/2.0.0.txt +298 -0
  12. data/doc/release_notes/2.1.0.txt +271 -0
  13. data/doc/release_notes/2.10.0.txt +328 -0
  14. data/doc/release_notes/2.2.0.txt +253 -0
  15. data/doc/release_notes/2.3.0.txt +88 -0
  16. data/doc/release_notes/2.4.0.txt +106 -0
  17. data/doc/release_notes/2.5.0.txt +137 -0
  18. data/doc/release_notes/2.6.0.txt +157 -0
  19. data/doc/release_notes/2.7.0.txt +166 -0
  20. data/doc/release_notes/2.8.0.txt +171 -0
  21. data/doc/release_notes/2.9.0.txt +97 -0
  22. data/doc/schema.rdoc +29 -0
  23. data/doc/sharding.rdoc +113 -0
  24. data/lib/sequel.rb +1 -0
  25. data/lib/sequel_core/adapters/ado.rb +89 -0
  26. data/lib/sequel_core/adapters/db2.rb +143 -0
  27. data/lib/sequel_core/adapters/dbi.rb +112 -0
  28. data/lib/sequel_core/adapters/do/mysql.rb +38 -0
  29. data/lib/sequel_core/adapters/do/postgres.rb +92 -0
  30. data/lib/sequel_core/adapters/do/sqlite.rb +31 -0
  31. data/lib/sequel_core/adapters/do.rb +205 -0
  32. data/lib/sequel_core/adapters/firebird.rb +298 -0
  33. data/lib/sequel_core/adapters/informix.rb +85 -0
  34. data/lib/sequel_core/adapters/jdbc/h2.rb +69 -0
  35. data/lib/sequel_core/adapters/jdbc/mysql.rb +66 -0
  36. data/lib/sequel_core/adapters/jdbc/oracle.rb +23 -0
  37. data/lib/sequel_core/adapters/jdbc/postgresql.rb +113 -0
  38. data/lib/sequel_core/adapters/jdbc/sqlite.rb +43 -0
  39. data/lib/sequel_core/adapters/jdbc.rb +491 -0
  40. data/lib/sequel_core/adapters/mysql.rb +369 -0
  41. data/lib/sequel_core/adapters/odbc.rb +174 -0
  42. data/lib/sequel_core/adapters/openbase.rb +68 -0
  43. data/lib/sequel_core/adapters/oracle.rb +107 -0
  44. data/lib/sequel_core/adapters/postgres.rb +456 -0
  45. data/lib/sequel_core/adapters/shared/ms_access.rb +110 -0
  46. data/lib/sequel_core/adapters/shared/mssql.rb +102 -0
  47. data/lib/sequel_core/adapters/shared/mysql.rb +325 -0
  48. data/lib/sequel_core/adapters/shared/oracle.rb +61 -0
  49. data/lib/sequel_core/adapters/shared/postgres.rb +715 -0
  50. data/lib/sequel_core/adapters/shared/progress.rb +31 -0
  51. data/lib/sequel_core/adapters/shared/sqlite.rb +265 -0
  52. data/lib/sequel_core/adapters/sqlite.rb +248 -0
  53. data/lib/sequel_core/connection_pool.rb +258 -0
  54. data/lib/sequel_core/core_ext.rb +217 -0
  55. data/lib/sequel_core/core_sql.rb +202 -0
  56. data/lib/sequel_core/database/schema.rb +164 -0
  57. data/lib/sequel_core/database.rb +691 -0
  58. data/lib/sequel_core/dataset/callback.rb +13 -0
  59. data/lib/sequel_core/dataset/convenience.rb +237 -0
  60. data/lib/sequel_core/dataset/pagination.rb +96 -0
  61. data/lib/sequel_core/dataset/prepared_statements.rb +220 -0
  62. data/lib/sequel_core/dataset/query.rb +41 -0
  63. data/lib/sequel_core/dataset/schema.rb +15 -0
  64. data/lib/sequel_core/dataset/sql.rb +1010 -0
  65. data/lib/sequel_core/dataset/stored_procedures.rb +75 -0
  66. data/lib/sequel_core/dataset/unsupported.rb +43 -0
  67. data/lib/sequel_core/dataset.rb +511 -0
  68. data/lib/sequel_core/deprecated.rb +26 -0
  69. data/lib/sequel_core/exceptions.rb +44 -0
  70. data/lib/sequel_core/migration.rb +212 -0
  71. data/lib/sequel_core/object_graph.rb +230 -0
  72. data/lib/sequel_core/pretty_table.rb +71 -0
  73. data/lib/sequel_core/schema/generator.rb +320 -0
  74. data/lib/sequel_core/schema/sql.rb +325 -0
  75. data/lib/sequel_core/schema.rb +2 -0
  76. data/lib/sequel_core/sql.rb +887 -0
  77. data/lib/sequel_core/version.rb +11 -0
  78. data/lib/sequel_core.rb +172 -0
  79. data/lib/sequel_model/association_reflection.rb +267 -0
  80. data/lib/sequel_model/associations.rb +499 -0
  81. data/lib/sequel_model/base.rb +523 -0
  82. data/lib/sequel_model/caching.rb +82 -0
  83. data/lib/sequel_model/dataset_methods.rb +26 -0
  84. data/lib/sequel_model/eager_loading.rb +370 -0
  85. data/lib/sequel_model/exceptions.rb +7 -0
  86. data/lib/sequel_model/hooks.rb +101 -0
  87. data/lib/sequel_model/inflector.rb +281 -0
  88. data/lib/sequel_model/plugins.rb +62 -0
  89. data/lib/sequel_model/record.rb +568 -0
  90. data/lib/sequel_model/schema.rb +49 -0
  91. data/lib/sequel_model/validations.rb +429 -0
  92. data/lib/sequel_model.rb +91 -0
  93. data/spec/adapters/ado_spec.rb +46 -0
  94. data/spec/adapters/firebird_spec.rb +376 -0
  95. data/spec/adapters/informix_spec.rb +96 -0
  96. data/spec/adapters/mysql_spec.rb +881 -0
  97. data/spec/adapters/oracle_spec.rb +244 -0
  98. data/spec/adapters/postgres_spec.rb +687 -0
  99. data/spec/adapters/spec_helper.rb +10 -0
  100. data/spec/adapters/sqlite_spec.rb +555 -0
  101. data/spec/integration/dataset_test.rb +134 -0
  102. data/spec/integration/eager_loader_test.rb +696 -0
  103. data/spec/integration/prepared_statement_test.rb +130 -0
  104. data/spec/integration/schema_test.rb +180 -0
  105. data/spec/integration/spec_helper.rb +58 -0
  106. data/spec/integration/type_test.rb +96 -0
  107. data/spec/rcov.opts +6 -0
  108. data/spec/sequel_core/connection_pool_spec.rb +526 -0
  109. data/spec/sequel_core/core_ext_spec.rb +156 -0
  110. data/spec/sequel_core/core_sql_spec.rb +522 -0
  111. data/spec/sequel_core/database_spec.rb +1188 -0
  112. data/spec/sequel_core/dataset_spec.rb +3481 -0
  113. data/spec/sequel_core/expression_filters_spec.rb +363 -0
  114. data/spec/sequel_core/migration_spec.rb +261 -0
  115. data/spec/sequel_core/object_graph_spec.rb +272 -0
  116. data/spec/sequel_core/pretty_table_spec.rb +58 -0
  117. data/spec/sequel_core/schema_generator_spec.rb +167 -0
  118. data/spec/sequel_core/schema_spec.rb +780 -0
  119. data/spec/sequel_core/spec_helper.rb +55 -0
  120. data/spec/sequel_core/version_spec.rb +7 -0
  121. data/spec/sequel_model/association_reflection_spec.rb +93 -0
  122. data/spec/sequel_model/associations_spec.rb +1767 -0
  123. data/spec/sequel_model/base_spec.rb +419 -0
  124. data/spec/sequel_model/caching_spec.rb +215 -0
  125. data/spec/sequel_model/dataset_methods_spec.rb +78 -0
  126. data/spec/sequel_model/eager_loading_spec.rb +1165 -0
  127. data/spec/sequel_model/hooks_spec.rb +485 -0
  128. data/spec/sequel_model/inflector_spec.rb +119 -0
  129. data/spec/sequel_model/model_spec.rb +588 -0
  130. data/spec/sequel_model/plugins_spec.rb +80 -0
  131. data/spec/sequel_model/record_spec.rb +1184 -0
  132. data/spec/sequel_model/schema_spec.rb +90 -0
  133. data/spec/sequel_model/spec_helper.rb +78 -0
  134. data/spec/sequel_model/validations_spec.rb +1067 -0
  135. data/spec/spec.opts +0 -0
  136. data/spec/spec_config.rb.example +10 -0
  137. metadata +177 -3
data/CHANGELOG CHANGED
@@ -1,6 +1,12 @@
1
1
  === HEAD
2
2
 
3
- * Make Dataset#select, #select_more, and #get take a block that yields a SQL::VirtualRow, similar to #filter (jeremyevans)
3
+ * Add Model#set_associated_object, used by the many_to_one setter method, for easier overriding (jeremyevans)
4
+
5
+ * Allow use of database independent types when casting (jeremyevans)
6
+
7
+ * Give association datasets knowledge of the model object that created them and the related association reflection (jeremyevans)
8
+
9
+ * Make Dataset#select, #select_more, #order, #order_more, and #get take a block that yields a SQL::VirtualRow, similar to #filter (jeremyevans)
4
10
 
5
11
  * Fix stored procedures in MySQL adapter when multiple arguments are used (clivecrous)
6
12
 
@@ -0,0 +1,614 @@
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{|o| o.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.
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{|o| o.number < 10}.first
154
+
155
+ You can extend a dataset with a module easily with :extend. You can reference
156
+ the model object that created the association dataset via the dataset's
157
+ model_object method, and the related association reflection via the dataset's
158
+ association_reflection method:
159
+
160
+ module FindOrCreate
161
+ def find_or_create(vals)
162
+ first(vals) || association_reflection.associated_class. \
163
+ create(vals.merge(association_reflection[:key]=>model_object.id))
164
+ end
165
+ end
166
+ class Author < Sequel::Model
167
+ one_to_many :authorships, :extend=>FindOrCreate
168
+ end
169
+ Author.first.authorships_dataset.find_or_create(:name=>'Blah', :number=>10)
170
+
171
+ ===has_many :through associations
172
+
173
+ many_to_many handles the usual case of a has_many :through with a belongs_to in
174
+ the associated model. It doesn't break on the case where the join table is a
175
+ model table, unlike ActiveRecord's has_and_belongs_to_many.
176
+
177
+ ActiveRecord:
178
+
179
+ class Author < ActiveRecord::Base
180
+ has_many :authorships
181
+ has_many :books, :through => :authorships
182
+ end
183
+
184
+ class Authorship < ActiveRecord::Base
185
+ belongs_to :author
186
+ belongs_to :book
187
+ end
188
+
189
+ @author = Author.find :first
190
+ @author.books
191
+
192
+ Sequel::Model:
193
+
194
+ class Author < Sequel::Model
195
+ one_to_many :authorships
196
+ many_to_many :books, :join_table=>:authorships
197
+ end
198
+
199
+ class Authorship < Sequel::Model
200
+ many_to_one :author
201
+ many_to_one :book
202
+ end
203
+
204
+ @author = Author.first
205
+ @author.books
206
+
207
+ If you use an association other than belongs_to in the associated model, things
208
+ are a bit more involved (has_many :through a has_many association):
209
+
210
+ ActiveRecord:
211
+
212
+ class Firm < ActiveRecord::Base
213
+ has_many :clients
214
+ has_many :invoices, :through => :clients
215
+ end
216
+
217
+ class Client < ActiveRecord::Base
218
+ belongs_to :firm
219
+ has_many :invoices
220
+ end
221
+
222
+ class Invoice < ActiveRecord::Base
223
+ belongs_to :client
224
+ has_one :firm, :through => :client
225
+ end
226
+
227
+ Firm.find(:first).invoices
228
+
229
+ Sequel::Model:
230
+
231
+ class Firm < Sequel::Model
232
+ one_to_many :clients
233
+ one_to_many :invoices, :read_only=>true, \
234
+ :dataset=>proc{Invoice.eager_graph(:client).filter(:client__firm_id=>pk)}, \
235
+ :after_load=>(proc do |firm, invs|
236
+ invs.each do |inv|
237
+ inv.client.associations[:firm] = inv.associations[:firm] = firm
238
+ end
239
+ end), \
240
+ :eager_loader=>(proc do |key_hash, firms, associations|
241
+ id_map = key_hash[Firm.primary_key]
242
+ firms.each{|firm| firm.associations[:invoices] = []}
243
+ Invoice.eager_graph(:client).filter(:client__firm_id=>id_map.keys).all do |inv|
244
+ id_map[inv.client.firm_id].each do |firm|
245
+ inv.client.associations[:firm] = inv.associations[:firm] = firm
246
+ firm.associations[:invoices] << inv
247
+ end
248
+ end
249
+ end)
250
+ end
251
+
252
+ class Client < Sequel::Model
253
+ many_to_one :firm
254
+ one_to_many :invoices
255
+ end
256
+
257
+ class Invoice < Sequel::Model
258
+ many_to_one :client
259
+ many_to_one :firm, :key=>nil, :read_only=>true, \
260
+ :dataset=>proc{Firm.eager_graph(:clients).filter(:clients__id=>client_id)}, \
261
+ :after_load=>(proc do |inv, firm|
262
+ # Delete the cached associations from firm, because it only has the
263
+ # client with this invoice, instead of all clients of the firm
264
+ inv.associations[:client] = firm.associations.delete(:clients).first
265
+ end), \
266
+ :eager_loader=>(proc do |key_hash, invoices, associations|
267
+ id_map = {}
268
+ invoices.each do |inv|
269
+ inv.associations[:firm] = nil
270
+ inv.associations[:client] = nil
271
+ (id_map[inv.client_id] ||= []) << inv
272
+ end
273
+ Firm.eager_graph(:clients).filter(:clients__id=>id_map.keys).all do |firm|
274
+ # Delete the cached associations from firm, because it only has the
275
+ # clients related the invoices being eagerly loaded, instead of all
276
+ # clients of the firm.
277
+ firm.associations.delete(:clients).each do |client|
278
+ id_map[client.pk].each do |inv|
279
+ inv.associations[:firm] = firm
280
+ inv.associations[:client] = client
281
+ end
282
+ end
283
+ end
284
+ end)
285
+ end
286
+ Firm.find(:first).invoices
287
+
288
+ It is significantly more code in Sequel Model, but quite a bit of it is setting
289
+ the intermediate associated record (the client) and the reciprocal association
290
+ in the associations cache for each object, which ActiveRecord won't do for you.
291
+ The reason you would want to do this is that then firm.invoices.first.firm or
292
+ firm.invoices.first.client doesn't do another query to get the firm/client.
293
+
294
+ ===Polymorphic Associations
295
+
296
+ Polymorphic associations are really a design flaw. The only advantage
297
+ polymorphic associations offer is that they require fewer join tables.
298
+
299
+ Proof by Reductio ad absurdum: If fewer join tables are preferable, then surely
300
+ fewer tables and columns are preferrable, so you might as well store all of
301
+ your data in a single column in a single table if you think polymorphic
302
+ associations are a good idea.
303
+
304
+ Compelling Argument: Polymorphic associations are more complex than normal
305
+ associations, and they break referential integrity, so the only reason you
306
+ should use them is if you are already stuck with an existing design that
307
+ uses them. You should never use them in new code.
308
+
309
+ ActiveRecord:
310
+
311
+ class Asset < ActiveRecord::Base
312
+ belongs_to :attachable, :polymorphic => true
313
+ end
314
+
315
+ class Post < ActiveRecord::Base
316
+ has_many :assets, :as => :attachable
317
+ end
318
+
319
+ class Note < ActiveRecord::Base
320
+ has_many :assets, :as => :attachable
321
+ end
322
+
323
+ @asset.attachable = @post
324
+ @asset.attachable = @note
325
+
326
+ Sequel::Model:
327
+
328
+ class Asset < Sequel::Model
329
+ many_to_one :attachable, :reciprocal=>:assets, \
330
+ :dataset=>(proc do
331
+ klass = attachable_type.constantize
332
+ klass.filter(klass.primary_key=>attachable_id)
333
+ end), \
334
+ :eager_loader=>(proc do |key_hash, assets, associations|
335
+ id_map = {}
336
+ assets.each do |asset|
337
+ asset.associations[:attachable] = nil
338
+ ((id_map[asset.attachable_type] ||= {})[asset.attachable_id] ||= []) << asset
339
+ end
340
+ id_map.each do |klass_name, id_map|
341
+ klass = klass_name.constantize
342
+ klass.filter(klass.primary_key=>id_map.keys).all do |attach|
343
+ id_map[attach.pk].each do |asset|
344
+ asset.associations[:attachable] = attach
345
+ end
346
+ end
347
+ end
348
+ end)
349
+
350
+ private
351
+
352
+ def _attachable=(attachable)
353
+ self[:attachable_id] = (attachable.pk if attachable)
354
+ self[:attachable_type] = (attachable.class.name if attachable)
355
+ end
356
+ end
357
+
358
+ class Post < Sequel::Model
359
+ one_to_many :assets, :key=>:attachable_id do |ds|
360
+ ds.filter(:attachable_type=>'Post')
361
+ end
362
+
363
+ private
364
+
365
+ def _add_asset(asset)
366
+ asset.attachable_id = pk
367
+ asset.attachable_type = 'Post'
368
+ asset.save
369
+ end
370
+ def _remove_asset(asset)
371
+ asset.attachable_id = nil
372
+ asset.attachable_type = nil
373
+ asset.save
374
+ end
375
+ def _remove_all_assets
376
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Post')\
377
+ .update(:attachable_id=>nil, :attachable_type=>nil)
378
+ end
379
+ end
380
+
381
+ class Note < Sequel::Model
382
+ one_to_many :assets, :key=>:attachable_id do |ds|
383
+ ds.filter(:attachable_type=>'Note')
384
+ end
385
+
386
+ private
387
+
388
+ def _add_asset(asset)
389
+ asset.attachable_id = pk
390
+ asset.attachable_type = 'Note'
391
+ asset.save
392
+ end
393
+ def _remove_asset(asset)
394
+ asset.attachable_id = nil
395
+ asset.attachable_type = nil
396
+ asset.save
397
+ end
398
+ def _remove_all_assets
399
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Note')\
400
+ .update(:attachable_id=>nil, :attachable_type=>nil)
401
+ end
402
+ end
403
+
404
+ @asset.attachable = @post
405
+ @asset.attachable = @note
406
+
407
+ ==More advanced associations
408
+
409
+ So far, we've only shown that Sequel::Model has associations as powerful as
410
+ ActiveRecord's. Now we will show how Sequel::Model's associations are more
411
+ powerful.
412
+
413
+ ===many_to_one/one_to_many not referencing primary key
414
+
415
+ This can now be handled easily in Sequel using the :primary_key association
416
+ option. However, this example shows how the association was possible before
417
+ the introduction of that option.
418
+
419
+ Let's say you have two tables, invoices and clients, where each client is
420
+ associated with many invoices. However, instead of using the client's primary
421
+ key, the invoice is associated to the client by name (this is bad database
422
+ design, but sometimes you have to play with the cards you are dealt).
423
+
424
+ class Client < Sequel::Model
425
+ one_to_many :invoices, :reciprocal=>:client, \
426
+ :dataset=>proc{Invoice.filter(:client_name=>name)}, \
427
+ :eager_loader=>(proc do |key_hash, clients, associations|
428
+ id_map = {}
429
+ clients.each do |client|
430
+ id_map[client.name] = client
431
+ client.associations[:invoices] = []
432
+ end
433
+ Invoice.filter(:client_name=>id_map.keys.sort).all do |inv|
434
+ inv.associations[:client] = client = id_map[inv.client_name]
435
+ client.associations[:invoices] << inv
436
+ end
437
+ end)
438
+
439
+ private
440
+
441
+ def _add_invoice(invoice)
442
+ invoice.client_name = name
443
+ invoice.save
444
+ end
445
+ def _remove_invoice(invoice)
446
+ invoice.client_name = nil
447
+ invoice.save
448
+ end
449
+ def _remove_all_invoices
450
+ Invoice.filter(:client_name=>name).update(:client_name=>nil)
451
+ end
452
+ end
453
+
454
+ class Invoice < Sequel::Model
455
+ many_to_one :client, :key=>:client_name, \
456
+ :dataset=>proc{Client.filter(:name=>client_name)}, \
457
+ :eager_loader=>(proc do |key_hash, invoices, associations|
458
+ id_map = key_hash[:client_name]
459
+ invoices.each{|inv| inv.associations[:client] = nil}
460
+ Client.filter(:name=>id_map.keys).all do |client|
461
+ id_map[client.name].each{|inv| inv.associations[:client] = client}
462
+ end
463
+ end)
464
+
465
+ private
466
+
467
+ def _client=(client)
468
+ self.client_name = (client.name if client)
469
+ end
470
+ end
471
+
472
+ ===Joining on multiple keys
473
+
474
+ Let's say you have two tables that are associated with each other with multiple
475
+ keys. For example:
476
+
477
+ # Both of these models have an album_id, number, and disc_number fields.
478
+ # All FavoriteTracks have an associated track, but not all tracks have an
479
+ # associated favorite track
480
+
481
+ class Track < Sequel::Model
482
+ many_to_one :favorite_track, \
483
+ :dataset=>(proc do
484
+ FavoriteTrack.filter(:disc_number=>disc_number, :number=>number, :album_id=>album_id)
485
+ end), \
486
+ :eager_loader=>(proc do |key_hash, tracks, associations|
487
+ id_map = {}
488
+ tracks.each do |t|
489
+ t.associations[:favorite_track] = nil
490
+ id_map[[t[:album_id], t[:disc_number], t[:number]]] = t
491
+ end
492
+ FavoriteTrack.filter([:album_id, :disc_number, :number]=>id_map.keys).all do |ft|
493
+ if t = id_map[[ft[:album_id], ft[:disc_number], ft[:number]]]
494
+ t.associations[:favorite_track] = ft
495
+ end
496
+ end
497
+ end)
498
+ end
499
+
500
+ class FavoriteTrack < Sequel::Model
501
+ many_to_one :track, \
502
+ :dataset=>(proc do
503
+ Track.filter(:disc_number=>disc_number, :number=>number, :album_id=>album_id)
504
+ end), \
505
+ :eager_loader=>(proc do |key_hash, ftracks, associations|
506
+ id_map = {}
507
+ ftracks.each{|ft| id_map[[ft[:album_id], ft[:disc_number], ft[:number]]] = ft}
508
+ Track.filter([:album_id, :disc_number, :number]=>id_map.keys).all do |t|
509
+ id_map[[t[:album_id], t[:disc_number], t[:number]]].associations[:track] = t
510
+ end
511
+ end)
512
+ end
513
+
514
+ ===Tree - All Ancestors and Descendents
515
+
516
+ Let's say you want to store a tree relationship in your database, it's pretty
517
+ simple:
518
+
519
+ class Node < Sequel::Model
520
+ many_to_one :parent
521
+ one_to_many :children, :key=>:parent_id
522
+ end
523
+
524
+ You can easily get a node's parent with node.parent, and a node's children with
525
+ node.children. You can even eager load the relationship up to a certain depth:
526
+
527
+ # Eager load three generations of generations of children for a given node
528
+ Node.filter(:id=>1).eager(:children=>{:children=>:children}).all.first
529
+ # Load parents and grandparents for a group of nodes
530
+ Node.filter{|o| o.id < 10}.eager(:parent=>:parent).all
531
+
532
+ What if you want to get all ancestors up to the root node, or all descendents,
533
+ without knowing the depth of the tree?
534
+
535
+ class Node < Sequel::Model
536
+ many_to_one :ancestors, :eager_loader=>(proc do |key_hash, nodes, associations|
537
+ # Handle cases where the root node has the same parent_id as primary_key
538
+ # and also when it is NULL
539
+ non_root_nodes = nodes.reject do |n|
540
+ if [nil, n.pk].include?(n.parent_id)
541
+ # Make sure root nodes have their parent association set to nil
542
+ n.associations[:parent] = nil
543
+ true
544
+ else
545
+ false
546
+ end
547
+ end
548
+ unless non_root_nodes.empty?
549
+ id_map = {}
550
+ # Create an map of parent_ids to nodes that have that parent id
551
+ non_root_nodes.each{|n| (id_map[n.parent_id] ||= []) << n}
552
+ # Doesn't cause an infinte loop, because when only the root node
553
+ # is left, this is not called.
554
+ Node.filter(Node.primary_key=>id_map.keys).eager(:ancestors).all do |node|
555
+ # Populate the parent association for each node
556
+ id_map[node.pk].each{|n| n.associations[:parent] = node}
557
+ end
558
+ end
559
+ end)
560
+ many_to_one :descendants, :eager_loader=>(proc do |key_hash, nodes, associations|
561
+ id_map = {}
562
+ nodes.each do |n|
563
+ # Initialize an empty array of child associations for each parent node
564
+ n.associations[:children] = []
565
+ # Populate identity map of nodes
566
+ id_map[n.pk] = n
567
+ end
568
+ # Doesn't cause an infinite loop, because the :eager_loader is not called
569
+ # if no records are returned. Exclude id = parent_id to avoid infinite loop
570
+ # if the root note is one of the returned records and it has parent_id = id
571
+ # instead of parent_id = NULL.
572
+ Node.filter(:parent_id=>id_map.keys).exclude(:id=>:parent_id).eager(:descendants).all do |node|
573
+ # Get the parent from the identity map
574
+ parent = id_map[node.parent_id]
575
+ # Set the child's parent association to the parent
576
+ node.associations[:parent] = parent
577
+ # Add the child association to the array of children in the parent
578
+ parent.associations[:children] << node
579
+ end
580
+ end)
581
+ end
582
+
583
+
584
+ ===Joining multiple keys to a single key, through a third table
585
+
586
+ Let's say you have a database, of songs, lyrics, and artists. Each song
587
+ may or may not have a lyric (most songs are instrumental). The lyric can be
588
+ associated to an artist in each of four ways: composer, arranger, vocalist,
589
+ or lyricist. These may all be the same, or they could all be different, and
590
+ none of them are required. The songs table has a lyric_id field to associate
591
+ it to the lyric, and the lyric table has four fields to associate it to the
592
+ artist (composer_id, arranger_id, vocalist_id, and lyricist_id).
593
+
594
+ What you want to do is get all songs for a given artist, ordered by the song's
595
+ name, with no duplicates?
596
+
597
+ class Artist < Sequel::Model
598
+ one_to_many :songs, :order=>:songs__name, \
599
+ :dataset=>proc{Song.select(:songs.*).join(Lyric, :id=>:lyric_id, id=>[:composer_id, :arranger_id, :vocalist_id, :lyricist_id])}, \
600
+ :eager_loader=>(proc do |key_hash, records, associations|
601
+ h = key_hash[:id]
602
+ ids = h.keys
603
+ records.each{|r| r.associations[:songs] = []}
604
+ Song.select(:songs.*, :lyrics__composer_id, :lyrics__arranger_id, :lyrics__vocalist_id, :lyrics__lyricist_id)\
605
+ .join(Lyric, :id=>:lyric_id){{:composer_id=>ids, :arranger_id=>ids, :vocalist_id=>ids, :lyricist_id=>ids}.sql_or}\
606
+ .order(:songs__name).all do |song|
607
+ [:composer_id, :arranger_id, :vocalist_id, :lyricist_id].each do |x|
608
+ recs = h[song.values.delete(x)]
609
+ recs.each{|r| r.associations[:songs] << song} if recs
610
+ end
611
+ end
612
+ records.each{|r| r.associations[:songs].uniq!}
613
+ end)
614
+ end