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.
- data/CHANGELOG +7 -1
- data/doc/advanced_associations.rdoc +614 -0
- data/doc/cheat_sheet.rdoc +223 -0
- data/doc/dataset_filtering.rdoc +158 -0
- data/doc/prepared_statements.rdoc +104 -0
- data/doc/release_notes/1.0.txt +38 -0
- data/doc/release_notes/1.1.txt +143 -0
- data/doc/release_notes/1.3.txt +101 -0
- data/doc/release_notes/1.4.0.txt +53 -0
- data/doc/release_notes/1.5.0.txt +155 -0
- data/doc/release_notes/2.0.0.txt +298 -0
- data/doc/release_notes/2.1.0.txt +271 -0
- data/doc/release_notes/2.10.0.txt +328 -0
- data/doc/release_notes/2.2.0.txt +253 -0
- data/doc/release_notes/2.3.0.txt +88 -0
- data/doc/release_notes/2.4.0.txt +106 -0
- data/doc/release_notes/2.5.0.txt +137 -0
- data/doc/release_notes/2.6.0.txt +157 -0
- data/doc/release_notes/2.7.0.txt +166 -0
- data/doc/release_notes/2.8.0.txt +171 -0
- data/doc/release_notes/2.9.0.txt +97 -0
- data/doc/schema.rdoc +29 -0
- data/doc/sharding.rdoc +113 -0
- data/lib/sequel.rb +1 -0
- data/lib/sequel_core/adapters/ado.rb +89 -0
- data/lib/sequel_core/adapters/db2.rb +143 -0
- data/lib/sequel_core/adapters/dbi.rb +112 -0
- data/lib/sequel_core/adapters/do/mysql.rb +38 -0
- data/lib/sequel_core/adapters/do/postgres.rb +92 -0
- data/lib/sequel_core/adapters/do/sqlite.rb +31 -0
- data/lib/sequel_core/adapters/do.rb +205 -0
- data/lib/sequel_core/adapters/firebird.rb +298 -0
- data/lib/sequel_core/adapters/informix.rb +85 -0
- data/lib/sequel_core/adapters/jdbc/h2.rb +69 -0
- data/lib/sequel_core/adapters/jdbc/mysql.rb +66 -0
- data/lib/sequel_core/adapters/jdbc/oracle.rb +23 -0
- data/lib/sequel_core/adapters/jdbc/postgresql.rb +113 -0
- data/lib/sequel_core/adapters/jdbc/sqlite.rb +43 -0
- data/lib/sequel_core/adapters/jdbc.rb +491 -0
- data/lib/sequel_core/adapters/mysql.rb +369 -0
- data/lib/sequel_core/adapters/odbc.rb +174 -0
- data/lib/sequel_core/adapters/openbase.rb +68 -0
- data/lib/sequel_core/adapters/oracle.rb +107 -0
- data/lib/sequel_core/adapters/postgres.rb +456 -0
- data/lib/sequel_core/adapters/shared/ms_access.rb +110 -0
- data/lib/sequel_core/adapters/shared/mssql.rb +102 -0
- data/lib/sequel_core/adapters/shared/mysql.rb +325 -0
- data/lib/sequel_core/adapters/shared/oracle.rb +61 -0
- data/lib/sequel_core/adapters/shared/postgres.rb +715 -0
- data/lib/sequel_core/adapters/shared/progress.rb +31 -0
- data/lib/sequel_core/adapters/shared/sqlite.rb +265 -0
- data/lib/sequel_core/adapters/sqlite.rb +248 -0
- data/lib/sequel_core/connection_pool.rb +258 -0
- data/lib/sequel_core/core_ext.rb +217 -0
- data/lib/sequel_core/core_sql.rb +202 -0
- data/lib/sequel_core/database/schema.rb +164 -0
- data/lib/sequel_core/database.rb +691 -0
- data/lib/sequel_core/dataset/callback.rb +13 -0
- data/lib/sequel_core/dataset/convenience.rb +237 -0
- data/lib/sequel_core/dataset/pagination.rb +96 -0
- data/lib/sequel_core/dataset/prepared_statements.rb +220 -0
- data/lib/sequel_core/dataset/query.rb +41 -0
- data/lib/sequel_core/dataset/schema.rb +15 -0
- data/lib/sequel_core/dataset/sql.rb +1010 -0
- data/lib/sequel_core/dataset/stored_procedures.rb +75 -0
- data/lib/sequel_core/dataset/unsupported.rb +43 -0
- data/lib/sequel_core/dataset.rb +511 -0
- data/lib/sequel_core/deprecated.rb +26 -0
- data/lib/sequel_core/exceptions.rb +44 -0
- data/lib/sequel_core/migration.rb +212 -0
- data/lib/sequel_core/object_graph.rb +230 -0
- data/lib/sequel_core/pretty_table.rb +71 -0
- data/lib/sequel_core/schema/generator.rb +320 -0
- data/lib/sequel_core/schema/sql.rb +325 -0
- data/lib/sequel_core/schema.rb +2 -0
- data/lib/sequel_core/sql.rb +887 -0
- data/lib/sequel_core/version.rb +11 -0
- data/lib/sequel_core.rb +172 -0
- data/lib/sequel_model/association_reflection.rb +267 -0
- data/lib/sequel_model/associations.rb +499 -0
- data/lib/sequel_model/base.rb +523 -0
- data/lib/sequel_model/caching.rb +82 -0
- data/lib/sequel_model/dataset_methods.rb +26 -0
- data/lib/sequel_model/eager_loading.rb +370 -0
- data/lib/sequel_model/exceptions.rb +7 -0
- data/lib/sequel_model/hooks.rb +101 -0
- data/lib/sequel_model/inflector.rb +281 -0
- data/lib/sequel_model/plugins.rb +62 -0
- data/lib/sequel_model/record.rb +568 -0
- data/lib/sequel_model/schema.rb +49 -0
- data/lib/sequel_model/validations.rb +429 -0
- data/lib/sequel_model.rb +91 -0
- data/spec/adapters/ado_spec.rb +46 -0
- data/spec/adapters/firebird_spec.rb +376 -0
- data/spec/adapters/informix_spec.rb +96 -0
- data/spec/adapters/mysql_spec.rb +881 -0
- data/spec/adapters/oracle_spec.rb +244 -0
- data/spec/adapters/postgres_spec.rb +687 -0
- data/spec/adapters/spec_helper.rb +10 -0
- data/spec/adapters/sqlite_spec.rb +555 -0
- data/spec/integration/dataset_test.rb +134 -0
- data/spec/integration/eager_loader_test.rb +696 -0
- data/spec/integration/prepared_statement_test.rb +130 -0
- data/spec/integration/schema_test.rb +180 -0
- data/spec/integration/spec_helper.rb +58 -0
- data/spec/integration/type_test.rb +96 -0
- data/spec/rcov.opts +6 -0
- data/spec/sequel_core/connection_pool_spec.rb +526 -0
- data/spec/sequel_core/core_ext_spec.rb +156 -0
- data/spec/sequel_core/core_sql_spec.rb +522 -0
- data/spec/sequel_core/database_spec.rb +1188 -0
- data/spec/sequel_core/dataset_spec.rb +3481 -0
- data/spec/sequel_core/expression_filters_spec.rb +363 -0
- data/spec/sequel_core/migration_spec.rb +261 -0
- data/spec/sequel_core/object_graph_spec.rb +272 -0
- data/spec/sequel_core/pretty_table_spec.rb +58 -0
- data/spec/sequel_core/schema_generator_spec.rb +167 -0
- data/spec/sequel_core/schema_spec.rb +780 -0
- data/spec/sequel_core/spec_helper.rb +55 -0
- data/spec/sequel_core/version_spec.rb +7 -0
- data/spec/sequel_model/association_reflection_spec.rb +93 -0
- data/spec/sequel_model/associations_spec.rb +1767 -0
- data/spec/sequel_model/base_spec.rb +419 -0
- data/spec/sequel_model/caching_spec.rb +215 -0
- data/spec/sequel_model/dataset_methods_spec.rb +78 -0
- data/spec/sequel_model/eager_loading_spec.rb +1165 -0
- data/spec/sequel_model/hooks_spec.rb +485 -0
- data/spec/sequel_model/inflector_spec.rb +119 -0
- data/spec/sequel_model/model_spec.rb +588 -0
- data/spec/sequel_model/plugins_spec.rb +80 -0
- data/spec/sequel_model/record_spec.rb +1184 -0
- data/spec/sequel_model/schema_spec.rb +90 -0
- data/spec/sequel_model/spec_helper.rb +78 -0
- data/spec/sequel_model/validations_spec.rb +1067 -0
- data/spec/spec.opts +0 -0
- data/spec/spec_config.rb.example +10 -0
- metadata +177 -3
data/CHANGELOG
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
=== HEAD
|
2
2
|
|
3
|
-
*
|
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
|