colincasey-sequel 2.10.0 → 2.10.1

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