sequel 5.82.0 → 5.84.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequel +9 -17
  3. data/lib/sequel/adapters/jdbc/derby.rb +1 -1
  4. data/lib/sequel/adapters/shared/db2.rb +1 -1
  5. data/lib/sequel/adapters/shared/mssql.rb +14 -2
  6. data/lib/sequel/adapters/shared/postgres.rb +42 -4
  7. data/lib/sequel/adapters/shared/sqlite.rb +3 -1
  8. data/lib/sequel/database/connecting.rb +1 -4
  9. data/lib/sequel/database/misc.rb +27 -7
  10. data/lib/sequel/database/schema_methods.rb +17 -1
  11. data/lib/sequel/dataset/sql.rb +13 -0
  12. data/lib/sequel/extensions/pg_json_ops.rb +328 -1
  13. data/lib/sequel/extensions/stdio_logger.rb +48 -0
  14. data/lib/sequel/extensions/string_agg.rb +15 -2
  15. data/lib/sequel/plugins/defaults_setter.rb +16 -4
  16. data/lib/sequel/plugins/optimistic_locking.rb +2 -0
  17. data/lib/sequel/sql.rb +8 -5
  18. data/lib/sequel/version.rb +1 -1
  19. metadata +4 -235
  20. data/CHANGELOG +0 -1377
  21. data/README.rdoc +0 -936
  22. data/doc/advanced_associations.rdoc +0 -884
  23. data/doc/association_basics.rdoc +0 -1859
  24. data/doc/bin_sequel.rdoc +0 -146
  25. data/doc/cheat_sheet.rdoc +0 -255
  26. data/doc/code_order.rdoc +0 -104
  27. data/doc/core_extensions.rdoc +0 -405
  28. data/doc/dataset_basics.rdoc +0 -96
  29. data/doc/dataset_filtering.rdoc +0 -222
  30. data/doc/extensions.rdoc +0 -77
  31. data/doc/fork_safety.rdoc +0 -84
  32. data/doc/mass_assignment.rdoc +0 -98
  33. data/doc/migration.rdoc +0 -660
  34. data/doc/model_dataset_method_design.rdoc +0 -129
  35. data/doc/model_hooks.rdoc +0 -254
  36. data/doc/model_plugins.rdoc +0 -270
  37. data/doc/mssql_stored_procedures.rdoc +0 -43
  38. data/doc/object_model.rdoc +0 -563
  39. data/doc/opening_databases.rdoc +0 -439
  40. data/doc/postgresql.rdoc +0 -611
  41. data/doc/prepared_statements.rdoc +0 -144
  42. data/doc/querying.rdoc +0 -1070
  43. data/doc/reflection.rdoc +0 -120
  44. data/doc/release_notes/5.0.0.txt +0 -159
  45. data/doc/release_notes/5.1.0.txt +0 -31
  46. data/doc/release_notes/5.10.0.txt +0 -84
  47. data/doc/release_notes/5.11.0.txt +0 -83
  48. data/doc/release_notes/5.12.0.txt +0 -141
  49. data/doc/release_notes/5.13.0.txt +0 -27
  50. data/doc/release_notes/5.14.0.txt +0 -63
  51. data/doc/release_notes/5.15.0.txt +0 -39
  52. data/doc/release_notes/5.16.0.txt +0 -110
  53. data/doc/release_notes/5.17.0.txt +0 -31
  54. data/doc/release_notes/5.18.0.txt +0 -69
  55. data/doc/release_notes/5.19.0.txt +0 -28
  56. data/doc/release_notes/5.2.0.txt +0 -33
  57. data/doc/release_notes/5.20.0.txt +0 -89
  58. data/doc/release_notes/5.21.0.txt +0 -87
  59. data/doc/release_notes/5.22.0.txt +0 -48
  60. data/doc/release_notes/5.23.0.txt +0 -56
  61. data/doc/release_notes/5.24.0.txt +0 -56
  62. data/doc/release_notes/5.25.0.txt +0 -32
  63. data/doc/release_notes/5.26.0.txt +0 -35
  64. data/doc/release_notes/5.27.0.txt +0 -21
  65. data/doc/release_notes/5.28.0.txt +0 -16
  66. data/doc/release_notes/5.29.0.txt +0 -22
  67. data/doc/release_notes/5.3.0.txt +0 -121
  68. data/doc/release_notes/5.30.0.txt +0 -20
  69. data/doc/release_notes/5.31.0.txt +0 -148
  70. data/doc/release_notes/5.32.0.txt +0 -46
  71. data/doc/release_notes/5.33.0.txt +0 -24
  72. data/doc/release_notes/5.34.0.txt +0 -40
  73. data/doc/release_notes/5.35.0.txt +0 -56
  74. data/doc/release_notes/5.36.0.txt +0 -60
  75. data/doc/release_notes/5.37.0.txt +0 -30
  76. data/doc/release_notes/5.38.0.txt +0 -28
  77. data/doc/release_notes/5.39.0.txt +0 -19
  78. data/doc/release_notes/5.4.0.txt +0 -80
  79. data/doc/release_notes/5.40.0.txt +0 -40
  80. data/doc/release_notes/5.41.0.txt +0 -25
  81. data/doc/release_notes/5.42.0.txt +0 -136
  82. data/doc/release_notes/5.43.0.txt +0 -98
  83. data/doc/release_notes/5.44.0.txt +0 -32
  84. data/doc/release_notes/5.45.0.txt +0 -34
  85. data/doc/release_notes/5.46.0.txt +0 -87
  86. data/doc/release_notes/5.47.0.txt +0 -59
  87. data/doc/release_notes/5.48.0.txt +0 -14
  88. data/doc/release_notes/5.49.0.txt +0 -59
  89. data/doc/release_notes/5.5.0.txt +0 -61
  90. data/doc/release_notes/5.50.0.txt +0 -78
  91. data/doc/release_notes/5.51.0.txt +0 -47
  92. data/doc/release_notes/5.52.0.txt +0 -87
  93. data/doc/release_notes/5.53.0.txt +0 -23
  94. data/doc/release_notes/5.54.0.txt +0 -27
  95. data/doc/release_notes/5.55.0.txt +0 -21
  96. data/doc/release_notes/5.56.0.txt +0 -51
  97. data/doc/release_notes/5.57.0.txt +0 -23
  98. data/doc/release_notes/5.58.0.txt +0 -31
  99. data/doc/release_notes/5.59.0.txt +0 -73
  100. data/doc/release_notes/5.6.0.txt +0 -31
  101. data/doc/release_notes/5.60.0.txt +0 -22
  102. data/doc/release_notes/5.61.0.txt +0 -43
  103. data/doc/release_notes/5.62.0.txt +0 -132
  104. data/doc/release_notes/5.63.0.txt +0 -33
  105. data/doc/release_notes/5.64.0.txt +0 -50
  106. data/doc/release_notes/5.65.0.txt +0 -21
  107. data/doc/release_notes/5.66.0.txt +0 -24
  108. data/doc/release_notes/5.67.0.txt +0 -32
  109. data/doc/release_notes/5.68.0.txt +0 -61
  110. data/doc/release_notes/5.69.0.txt +0 -26
  111. data/doc/release_notes/5.7.0.txt +0 -108
  112. data/doc/release_notes/5.70.0.txt +0 -35
  113. data/doc/release_notes/5.71.0.txt +0 -21
  114. data/doc/release_notes/5.72.0.txt +0 -33
  115. data/doc/release_notes/5.73.0.txt +0 -66
  116. data/doc/release_notes/5.74.0.txt +0 -45
  117. data/doc/release_notes/5.75.0.txt +0 -35
  118. data/doc/release_notes/5.76.0.txt +0 -86
  119. data/doc/release_notes/5.77.0.txt +0 -63
  120. data/doc/release_notes/5.78.0.txt +0 -67
  121. data/doc/release_notes/5.79.0.txt +0 -28
  122. data/doc/release_notes/5.8.0.txt +0 -170
  123. data/doc/release_notes/5.80.0.txt +0 -40
  124. data/doc/release_notes/5.81.0.txt +0 -31
  125. data/doc/release_notes/5.82.0.txt +0 -61
  126. data/doc/release_notes/5.9.0.txt +0 -99
  127. data/doc/schema_modification.rdoc +0 -679
  128. data/doc/security.rdoc +0 -443
  129. data/doc/sharding.rdoc +0 -286
  130. data/doc/sql.rdoc +0 -648
  131. data/doc/testing.rdoc +0 -204
  132. data/doc/thread_safety.rdoc +0 -15
  133. data/doc/transactions.rdoc +0 -250
  134. data/doc/validations.rdoc +0 -558
  135. data/doc/virtual_rows.rdoc +0 -265
@@ -1,884 +0,0 @@
1
- = Advanced Associations
2
-
3
- Sequel::Model's association support is powerful and flexible, but it can be difficult for
4
- new users to understand what the support enables. This guide shows off some of the more
5
- advanced Sequel::Model association features.
6
-
7
- You should probably review the {Model Associations Basics and Options guide}[rdoc-ref:doc/association_basics.rdoc]
8
- before reviewing this guide.
9
-
10
- == Sequel::Model Eager Loading
11
-
12
- Sequel::Model offers two different ways to perform eager loading, +eager+ and
13
- +eager_graph+. +eager+ uses an SQL query per association, +eager_graph+ uses a single
14
- SQL query containing JOINs.
15
-
16
- Assuming the following associations:
17
-
18
- Artist.one_to_many :albums
19
- Album.one_to_many :tracks
20
- Tracks.many_to_one :lyric
21
-
22
- Let's say you wanted to load all artists and eagerly load the related albums, tracks, and lyrics.
23
-
24
- Artist.eager(albums: {tracks: :lyric})
25
- # 4 Queries:
26
- # SELECT * FROM artists;
27
- # SELECT * FROM albums WHERE (artist_id IN (...));
28
- # SELECT * FROM tracks WHERE (album_id IN (...));
29
- # SELECT * FROM lyrics WHERE (id IN (...));
30
-
31
- Artist.eager_graph(albums: {tracks: :lyric})
32
- # 1 Query:
33
- # SELECT artists.id, artists.name, ...
34
- # albums.id AS albums_id, albums.name AS albums_name, ...
35
- # tracks.id AS tracks_id, tracks.name AS tracks_name, ...
36
- # lyric.id AS lyric_id, ...
37
- # FROM artists
38
- # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id)
39
- # LEFT OUTER JOIN tracks ON (tracks.album_id = albums.id)
40
- # LEFT OUTER JOIN lyrics AS lyric ON (lyric.id = tracks.lyric_id);
41
-
42
- In general, the recommendation is to use +eager+ unless you have a reason to use +eager_graph+.
43
- +eager_graph+ is needed when you want to reference columns in an associated table. For example,
44
- if you want to order the loading of returned artists based on the names of the albums, you cannot
45
- do:
46
-
47
- Artist.eager(albums: {tracks: :lyric}).order{albums[:name]}
48
-
49
- because the initial query Sequel will use would be:
50
-
51
- # SELECT * FROM artists ORDER BY albums.name;
52
-
53
- and +albums+ is not a valid qualifier in such a query. In this situation, you must use +eager_graph+:
54
-
55
- Artist.eager_graph(albums: {tracks: :lyric}).order{albums[:name]}
56
-
57
- Whether +eager+ or +eager_graph+ performs better is association and database dependent. If
58
- you are concerned about performance, you should try benchmarking both cases with appropriate
59
- data to see which performs better.
60
-
61
- === Mixing eager and eager_graph
62
-
63
- Sequel offers the ability to mix +eager+ and +eager_graph+ when loading results. This can
64
- be done at the main level by calling both +eager+ and +eager_graph+ on the same dataset:
65
-
66
- Album.eager(:artist).eager_graph(:tracks)
67
- # 2 Queries:
68
- # SELECT albums.id, albums.name, ...
69
- # artist.id AS artist_id, artist.name AS artist_name, ...
70
- # FROM albums
71
- # LEFT OUTER JOIN artists AS artist ON (artist.id = albums.artist_id);
72
- # SELECT * FROM artists WHERE (id IN (...));
73
-
74
- You can also use +eager+ to load initial associations, and +eager_graph+ to load
75
- remaining associations, by using +eager_graph+ in an eager load callback:
76
-
77
- Artist.eager(albums: {tracks: proc{|ds| ds.eager_graph(:lyric)}})
78
- # 3 Queries:
79
- # SELECT * FROM artists;
80
- # SELECT * FROM albums WHERE (artist_id IN (...));
81
- # SELECT tracks.id, tracks.name, ...
82
- # lyric.id AS lyric_id, ...
83
- # FROM tracks
84
- # LEFT OUTER JOIN lyrics AS lyric ON (lyric.id = tracks.lyric_id)
85
- # WHERE (tracks.album_id IN (...));
86
-
87
- Using the +eager_graph_eager+ plugin, you can use +eager_graph+ to load the
88
- initial associations, and +eager+ to load the remaining associations. When
89
- you call +eager_graph_eager+, you must specify the dependency chain at
90
- which to start the eager loading via +eager+:
91
-
92
- Artist.plugin :eager_graph_eager
93
- Artist.eager_graph(albums: :tracks).eager_graph_eager([:albums, :tracks], :lyric)
94
- # 2 Queries:
95
- # SELECT artists.id, artists.name, ...
96
- # albums.id AS albums_id, albums.name AS albums_name, ...
97
- # tracks.id AS tracks_id, tracks.name AS tracks_name, ...
98
- # FROM artists
99
- # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id)
100
- # LEFT OUTER JOIN tracks ON (tracks.album_id= albums.id);
101
- # SELECT * FROM lyrics WHERE (id IN (...));
102
-
103
- These two approaches can also be nested, with +eager+ -> +eager_graph+ -> +eager+:
104
-
105
- Album.plugin :eager_graph_eager
106
- Artist.eager(albums: proc{|ds| ds.eager_graph(:tracks).eager_graph_eager([:tracks], :lyric)})
107
- # 3 Queries:
108
- # SELECT * FROM artists;
109
- # SELECT albums.id, albums.name, ...
110
- # tracks.id AS tracks_id, tracks.name AS tracks_name, ...
111
- # FROM albums
112
- # LEFT OUTER JOIN tracks ON (tracks.album_id = albums.id)
113
- # WHERE (albums.artist_id IN (...));
114
- # SELECT * FROM lyrics WHERE (id IN (...));
115
-
116
- Or with 2 separate +eager_graph+ queries:
117
-
118
- Artist.eager_graph(:albums).eager_graph_eager([:albums], tracks: proc{|ds| ds.eager_graph(:lyric)})
119
- # 2 Queries:
120
- # SELECT artists.id, artists.name, ...
121
- # albums.id AS albums_id, albums.name AS albums_name, ...
122
- # FROM artists
123
- # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id);
124
- # SELECT tracks.id, tracks.name, ...
125
- # lyric.id AS lyric_id, ...
126
- # FROM tracks
127
- # LEFT OUTER JOIN lyrics AS lyric ON (lyric.id = tracks.lyric_id)
128
- # WHERE (tracks.album_id IN (...));
129
-
130
- == Sequel::Model Association Loading Options
131
-
132
- There are a bunch of advanced association options that are available to
133
- handle more complex cases. First we'll go over some of the simpler ones:
134
-
135
- All associations take a block that can be used to further filter/modify the
136
- default dataset:
137
-
138
- Artist.one_to_many :gold_albums, class: :Album do |ds|
139
- ds.where{copies_sold > 500000}
140
- end
141
-
142
- There's also an :eager_block option if you want to use a different block when
143
- eager loading via <tt>Dataset#eager</tt>.
144
-
145
- There are many options for changing how the association is eagerly
146
- loaded via <tt>Dataset#eager_graph</tt>:
147
-
148
- :graph_join_type :: The type of join to do (<tt>:inner</tt>, <tt>:left</tt>, <tt>:right</tt>)
149
- :graph_conditions :: Additional conditions to put on join (needs to be a
150
- hash or array of all two pairs). Automatically assumes unqualified symbols
151
- or first element of the pair to be columns of the associated model, and
152
- unqualified symbols of the second element of the pair to be columns of the
153
- current model.
154
- :graph_block :: A block passed to +join_table+, allowing you to specify
155
- conditions other than equality, or to use OR, or set up any arbitrary
156
- condition. The block is passed the associated table alias, current table
157
- alias, and an array of previous joins clause objects.
158
- :graph_only_conditions :: Use these conditions instead of the standard
159
- association conditions. This is necessary when you don't want to have an
160
- equal condition between the foreign key and primary key of the tables.
161
- You can also use this to have a JOIN USING (array of symbols), or a NATURAL
162
- or CROSS JOIN (nil, with the appropriate <tt>:graph_join_type</tt>).
163
-
164
- These can be used like this:
165
-
166
- # Makes Artist.eager_graph(:required_albums).all not return artists that
167
- # don't have any albums
168
- Artist.one_to_many :required_albums, class: :Album, graph_join_type: :inner
169
-
170
- # Makes sure all returned albums have the active flag set
171
- Artist.one_to_many :active_albums, class: :Album, graph_conditions: {active: true}
172
-
173
- # Only returns albums that have sold more than 500,000 copies
174
- Artist.one_to_many :gold_albums, class: :Album,
175
- graph_block: proc{|j,lj,js| Sequel[j][:copies_sold] > 500000}
176
-
177
- # Handles the case where the tables are associated by a case insensitive name string
178
- Artist.one_to_many :albums, key: :artist_name,
179
- graph_only_conditions: nil,
180
- graph_block: proc{|j,lj,js| {Sequel.function(:lower, Sequel[j][:artist_name])=>Sequel.function(:lower, Sequel[lj][:name])}}
181
-
182
- # Handles the case where both key columns have the name artist_name, and you want to use
183
- # a JOIN USING
184
- Artist.one_to_many :albums, key: :artist_name, graph_only_conditions: [:artist_name]
185
-
186
- One advantage of using +eager_graph+ is that you can easily filter/order
187
- on columns in an associated table on a per-query basis, using regular
188
- Sequel dataset methods. For example, if you only want to retrieve artists
189
- who have albums that start with A, and eager load just those albums,
190
- ordered by the albums name, you can do:
191
-
192
- albums = Artist.
193
- eager_graph(:albums).
194
- where{Sequel.like(albums[:name], 'A%')}.
195
- order{albums[:name]}.
196
- all
197
-
198
- For lazy loading (e.g. Model[1].association), the <tt>:dataset</tt> option can be used
199
- to specify an arbitrary dataset (one that uses different keys, multiple keys,
200
- joins to other tables, etc.).
201
-
202
- == Custom Eager Loaders
203
-
204
- For eager loading via +eager+, the <tt>:eager_loader</tt> option can be used to specify
205
- how to eagerly load a complex association. This is an extremely powerful
206
- option. Though it can often be verbose (compared to other things in Sequel),
207
- it allows you complete control over how to eagerly load associations for a
208
- group of objects.
209
-
210
- :eager_loader should be a proc that takes a single hash argument, which will
211
- have at least the following keys:
212
-
213
- :id_map :: A mapping of key values to arrays of current model instances,
214
- usage described below
215
- :rows :: An array of model objects
216
- :associations :: A hash of dependent associations to eagerly load
217
- :self :: The dataset that is doing the eager loading
218
- :eager_block :: A dynamic callback for this eager load.
219
-
220
- Since you are given all of the records, you can do things like filter on
221
- associations that are specified by multiple keys, or do multiple
222
- queries depending on the content of the records (which would be
223
- necessary for polymorphic associations). Inside the <tt>:eager_loader</tt>
224
- proc, you should get the related objects and populate the
225
- associations cache for all objects in the array of records. The hash
226
- of dependent associations is available for you to cascade the eager
227
- loading down multiple levels, but it is up to you to use it.
228
-
229
- The id_map is a performance enhancement that is used by the default
230
- association loaders and is also available to you. It is a hash with keys
231
- foreign/primary key values, and values being arrays of current model
232
- objects having the foreign/primary key value associated with the key.
233
- This may be hard to visualize, so I'll give an example. Let's say you
234
- have the following associations
235
-
236
- Album.many_to_one :artist
237
- Album.one_to_many :tracks
238
-
239
- and the following three albums in the database:
240
-
241
- album1 = Album.create(artist_id: 3) # id: 1
242
- album2 = Album.create(artist_id: 3) # id: 2
243
- album3 = Album.create(artist_id: 2) # id: 3
244
-
245
- If you try to eager load this dataset:
246
-
247
- Album.eager(:artist, :tracks).all
248
-
249
- Then the id_map provided to the artist :eager_loader proc would be:
250
-
251
- {3=>[album1, album2], 2=>[album3]}
252
-
253
- The artist id_map contains a mapping of artist_id values to arrays of
254
- album objects. Since both album1 and album2 have the same artist_id,
255
- the are both in the array related to that key. album3 has a different
256
- artist_id, so it is in a different array. Eager loading of artists is
257
- done by looking for any artist having one of the keys in the hash:
258
-
259
- artists = Artist.where(id: id_map.keys).all
260
-
261
- When the artists are retrieved, you can iterate over them, find entries
262
- with matching keys, and manually associate them to the albums:
263
-
264
- artists.each do |artist|
265
- # Find related albums using the artist_id_map
266
- if albums = id_map[artist.id]
267
- # Iterate over the albums
268
- albums.each do |album|
269
- # Manually set the artist association for each album
270
- album.associations[:artist] = artist
271
- end
272
- end
273
- end
274
-
275
- The id_map provided to the tracks :eager_loader proc would be:
276
-
277
- {1=>[album1], 2=>[album2], 3=>[album3]}
278
-
279
- Now the id_map contains a mapping of id values to arrays of album objects (in this
280
- case each array only has a single object, because id is the primary key). So when
281
- looking for tracks to eagerly load, you only need to look for ones that have an
282
- album_id with one of the keys in the hash:
283
-
284
- tracks = Track.where(album_id: id_map.keys).all
285
-
286
- When the tracks are retrieved, you can iterate over them, find entries with matching
287
- keys, and manually associate them to the albums:
288
-
289
- tracks.each do |track|
290
- if albums = id_map[track.album_id]
291
- albums.each do |album|
292
- album.associations[:tracks] << track
293
- end
294
- end
295
- end
296
-
297
- === Two basic example eager loaders
298
-
299
- Putting the code in the above examples together, you almost have enough for a basic
300
- working eager loader. The main important thing that is missing is you need to set
301
- initial values for the eagerly loaded associations. For the artist association, you
302
- need to initial the values to nil:
303
-
304
- # rows here is the :rows entry in the hash passed to the eager loader
305
- rows.each{|album| album.associations[:artist] = nil}
306
-
307
- For the tracks association, you set the initial value to an empty array:
308
-
309
- rows.each{|album| album.associations[:track] = []}
310
-
311
- These are done so that if an album currently being loaded doesn't have an associated
312
- artist or any associated tracks, the lack of them will be cached, so calling the
313
- artist or tracks method on the album will not do another database lookup.
314
-
315
- So putting everything together, the artist eager loader looks like:
316
-
317
- Album.many_to_one :artist, eager_loader: (proc do |eo_opts|
318
- eo_opts[:rows].each{|album| album.associations[:artist] = nil}
319
- id_map = eo_opts[:id_map]
320
- Artist.where(id: id_map.keys).all do |artist|
321
- if albums = id_map[artist.id]
322
- albums.each do |album|
323
- album.associations[:artist] = artist
324
- end
325
- end
326
- end
327
- end)
328
-
329
- and the tracks eager loader looks like:
330
-
331
- Album.one_to_many :tracks, eager_loader: (proc do |eo_opts|
332
- eo_opts[:rows].each{|album| album.associations[:tracks] = []}
333
- id_map = eo_opts[:id_map]
334
- Track.where(album_id: id_map.keys).all do |track|
335
- if albums = id_map[track.album_id]
336
- albums.each do |album|
337
- album.associations[:tracks] << track
338
- end
339
- end
340
- end
341
- end)
342
-
343
- Now, these are both overly simplistic eager loaders that don't respect cascaded
344
- associations or any of the association options. But hopefully they both
345
- provide simple examples that you can more easily build and learn from, as
346
- the custom eager loaders described later in this page are more complex.
347
-
348
- Basically, the eager loading steps can be broken down into:
349
-
350
- 1. Set default association values (nil/[]) for each of the current objects
351
- 2. Return just related associated objects by filtering the associated class
352
- to include only rows with keys present in the id_map.
353
- 3. Iterating over the returned associated objects, indexing into the id_map
354
- using the foreign/primary key value in the associated object to get
355
- current values associated to that specific object.
356
- 4. For each of those current values, updating the cached association value to
357
- include that specific object.
358
-
359
- Using the :eager_loader proc, you should be able to eagerly load all associations
360
- that can be eagerly loaded, even if Sequel doesn't natively support such eager
361
- loading.
362
-
363
- == Limited Associations
364
-
365
- Sequel supports specifying limits and/or offsets for associations:
366
-
367
- Artist.one_to_many :first_10_albums, class: :Album, order: :release_date, limit: 10
368
-
369
- For retrieving the associated objects for a single object, this just uses
370
- a LIMIT:
371
-
372
- artist.first_10_albums
373
- # SELECT * FROM albums WHERE (artist_id = 1) LIMIT 10
374
-
375
- === Eager Loading via eager
376
-
377
- However, if you want to eagerly load an association, you must use a different
378
- approach. Sequel has 4 separate strategies for dealing with such cases.
379
-
380
- The default strategy used on all databases is a UNION-based approach, which
381
- will submit multiple subqueries in a UNION query:
382
-
383
- Artist.where(id: [1,2]).eager(:first_10_albums).all
384
- # SELECT * FROM (SELECT * FROM albums WHERE (artist_id = 1) LIMIT 10) UNION ALL
385
- # SELECT * FROM (SELECT * FROM albums WHERE (artist_id = 2) LIMIT 10)
386
-
387
- This is the fastest way to load the associated objects on most databases, as long as
388
- there is an index on albums.artist_id. Without an index it is probably the slowest
389
- approach, so make sure you have an index on the key columns. If you cannot add an
390
- index, you'll want to manually specify the :eager_limit_strategy option as shown below.
391
-
392
- On PostgreSQL, for *_one associations that don't use an offset, you can
393
- choose to use a the distinct on strategy:
394
-
395
- Artist.one_to_one :first_album, class: :Album, order: :release_date,
396
- eager_limit_strategy: :distinct_on
397
- Artist.where(id: [1,2]).eager(:first_album).all
398
- # SELECT DISTINCT ON (albums.artist_id) *
399
- # FROM albums
400
- # WHERE (albums.artist_id IN (1, 2))
401
- # ORDER BY albums.artist_id, release_date
402
-
403
- Otherwise, if the database supports window functions, you can choose to use
404
- the window function strategy:
405
-
406
- Artist.one_to_many :first_10_albums, class: :Album, order: :release_date, limit: 10,
407
- eager_limit_strategy: :window_function
408
- Artist.where(id: [1,2]).eager(:first_10_albums).all
409
- # SELECT * FROM (
410
- # SELECT *, row_number() OVER (PARTITION BY albums.artist_id ORDER BY release_date) AS x_sequel_row_number_x
411
- # FROM albums
412
- # WHERE (albums.artist_id IN (1, 2))
413
- # ) AS t1
414
- # WHERE (x_sequel_row_number_x <= 10)
415
-
416
- Alternatively, you can use the :ruby strategy, which will fall back to
417
- retrieving all records, and then will slice the resulting array to get
418
- the first 10 after retrieval.
419
-
420
- === Dynamic Eager Loading Limits
421
-
422
- If you need to eager load variable numbers of records (with limits that aren't
423
- known at the time of the association definition), Sequel supports an
424
- :eager_limit dataset option that can be defined in an eager loading callback:
425
-
426
- Artist.one_to_many :albums
427
- Artist.where(id: [1, 2]).eager(albums: lambda{|ds| ds.order(:release_date).clone(eager_limit: 3)}).all
428
- # SELECT * FROM (
429
- # SELECT *, row_number() OVER (PARTITION BY albums.artist_id ORDER BY release_date) AS x_sequel_row_number_x
430
- # FROM albums
431
- # WHERE (albums.artist_id IN (1, 2))
432
- # ) AS t1
433
- # WHERE (x_sequel_row_number_x <= 3)
434
-
435
- You can also customize the :eager_limit_strategy on a case-by-case basis by passing in that option in the same way:
436
-
437
- Artist.where(id: [1, 2]).eager(albums: lambda{|ds| ds.order(:release_date).clone(eager_limit: 3, eager_limit_strategy: :ruby)}).all
438
- # SELECT * FROM albums WHERE (albums.artist_id IN (1, 2)) ORDER BY release_date
439
-
440
- The :eager_limit and :eager_limit_strategy options currently only work when
441
- eager loading via #eager, not with #eager_graph.
442
-
443
- === Eager Loading via eager_graph_with_options
444
-
445
- When eager loading an association via eager_graph (which uses JOINs), the
446
- situation is similar. While the UNION-based strategy cannot be used as
447
- you don't know the records being eagerly loaded in advance, Sequel can use
448
- a variant of the other 3 strategies. By default it retrieves all records
449
- and then does the array slice in ruby. As eager_graph does not support
450
- options, to use an eager_graph limit strategy you have to use the
451
- eager_graph_with_options method with the :limit_strategy option.
452
-
453
- The :distinct_on strategy uses DISTINCT ON in a subquery and JOINs that
454
- subquery:
455
-
456
- Artist.eager_graph_with_options(:first_album, limit_strategy: :distinct_on).all
457
- # SELECT artists.id, artists.name, first_album.id AS first_album_id,
458
- # first_album.name AS first_album_name, first_album.artist_id,
459
- # first_album.release_date
460
- # FROM artists
461
- # LEFT OUTER JOIN (
462
- # SELECT DISTINCT ON (albums.artist_id) *
463
- # FROM albums
464
- # ORDER BY albums.artist_id, release_date
465
- # ) AS first_album ON (first_album.artist_id = artists.id)
466
-
467
- The :window_function approach JOINs to a nested subquery using a window
468
- function:
469
-
470
- Artist.eager_graph_with_options(:first_10_albums, limit_strategy: :window_function).all
471
- # SELECT artists.id, artists.name, first_10_albums.id AS first_10_albums_id,
472
- # first_10_albums.name AS first_10_albums_name, first_10_albums.artist_id,
473
- # first_10_albums.release_date
474
- # FROM artists
475
- # LEFT OUTER JOIN (
476
- # SELECT id, name, artist_id, release_date
477
- # FROM (
478
- # SELECT *, row_number() OVER (PARTITION BY tracks.album_id ORDER BY release_date) AS x_sequel_row_number_x
479
- # FROM albums
480
- # ) AS t1 WHERE (x_sequel_row_number_x <= 10)
481
- # ) AS first_10_albums ON (first_10_albums.artist_id = artists.id)
482
-
483
- The :correlated_subquery approach JOINs to a nested subquery using a correlated
484
- subquery:
485
-
486
- Artist.eager_graph_with_options(:first_10_albums, limit_strategy: :correlated_subquery).all
487
- # SELECT artists.id, artists.name, first_10_albums.id AS first_10_albums_id,
488
- # first_10_albums.name AS first_10_albums_name, first_10_albums.artist_id,
489
- # first_10_albums.release_date
490
- # FROM artists
491
- # LEFT OUTER JOIN (
492
- # SELECT *
493
- # FROM albums
494
- # WHERE albums.id IN (
495
- # SELECT t1.id
496
- # FROM tracks AS t1
497
- # WHERE (t1.album_id = tracks.album_id)
498
- # ORDER BY release_date
499
- # LIMIT 10
500
- # )
501
- # ) AS first_10_albums ON (first_10_albums.artist_id = artists.id)
502
-
503
- The reason that Sequel does not automatically use the :distinct_on, :window function
504
- or :correlated_subquery strategy for eager_graph is that it can perform much worse than the
505
- default of just doing the array slicing in ruby. If you are only using eager_graph to
506
- return a few records, it may be cheaper to get all of their associated records and filter
507
- them in ruby as opposed to computing the set of limited associated records for all rows.
508
-
509
- It's recommended to only use an eager_graph limit strategy if you have benchmarked
510
- it against the default behavior and found it is faster for your use case.
511
-
512
- === Filtering By Associations
513
-
514
- In order to return correct results, Sequel automatically uses a limit strategy when
515
- using filtering by associations with limited associations, if the database supports
516
- it. As in the eager_graph case, the UNION-based strategy doesn't work. Unlike
517
- in the eager and eager_graph cases, the array slicing in ruby approach does not work,
518
- you must use an SQL-based strategy. Sequel will select an appropriate default
519
- strategy based on the database you are using, and you can override it using the
520
- :filter_limit_strategy option.
521
-
522
- The :distinct_on strategy:
523
-
524
- Artist.where(first_album: Album[1]).all
525
- # SELECT *
526
- # FROM artists
527
- # WHERE (artists.id IN (
528
- # SELECT albums.artist_id
529
- # FROM albums
530
- # WHERE ((albums.artist_id IS NOT NULL) AND (albums.id IN (
531
- # SELECT DISTINCT ON (albums.artist_id) albums.id
532
- # FROM albums
533
- # ORDER BY albums.artist_id, release_date
534
- # )) AND (albums.id = 1))))
535
-
536
- The :window_function strategy:
537
-
538
- Artist.where(first_10_albums: Album[1]).all
539
- # SELECT *
540
- # FROM artists
541
- # WHERE (artists.id IN (
542
- # SELECT albums.artist_id
543
- # FROM albums
544
- # WHERE ((albums.artist_id IS NOT NULL) AND (albums.id IN (
545
- # SELECT id FROM (
546
- # SELECT albums.id, row_number() OVER (PARTITION BY albums.artist_id ORDER BY release_date) AS x_sequel_row_number_x
547
- # FROM albums
548
- # ) AS t1
549
- # WHERE (x_sequel_row_number_x <= 10)
550
- # )) AND (albums.id = 1))))
551
-
552
- The :correlated_subquery strategy:
553
-
554
- Artist.where(first_10_albums: Album[1]).all
555
- # SELECT *
556
- # FROM artists
557
- # WHERE (artists.id IN (
558
- # SELECT albums.artist_id
559
- # FROM albums
560
- # WHERE ((albums.artist_id IS NOT NULL) AND (albums.id IN (
561
- # SELECT t1.id
562
- # FROM albums AS t1
563
- # WHERE (t1.artist_id = albums.artist_id)
564
- # ORDER BY release_date
565
- # LIMIT 1
566
- # )) AND (albums.id = 1))))
567
-
568
- Note that filtering by limited associations does not work on MySQL, as MySQL does not support
569
- any of the strategies. It's also not supported when using composite keys on databases
570
- that don't support window functions and don't support multiple columns in IN.
571
-
572
- === Additional Association Types
573
-
574
- While the above examples for limited associations showed one_to_many and one_to_one associations,
575
- it's just because those are the simplest examples. Sequel supports all of the same features for
576
- many_to_many and one_through_one associations that are enabled by default, as well as the
577
- many_through_many and one_through_many associations that are added by the many_through_many
578
- plugin.
579
-
580
- == More advanced association examples
581
-
582
- === Association extensions
583
-
584
- All associations come with an <tt><i>association</i>_dataset</tt> method that can be further filtered or
585
- otherwise modified:
586
-
587
- class Author < Sequel::Model
588
- one_to_many :authorships
589
- end
590
- Author.first.authorships_dataset.where{number < 10}.first
591
-
592
- You can extend a dataset with a module using the <tt>:extend</tt> association option. You can reference
593
- the model object that created the association dataset via the dataset's
594
- +model_object+ method, and the related association reflection via the dataset's
595
- +association_reflection+ method:
596
-
597
- module FindOrCreate
598
- def find_or_create(vals)
599
- first(vals) || model.create(vals.merge(association_reflection[:key]=>model_object.id))
600
- end
601
- end
602
- class Author < Sequel::Model
603
- one_to_many :authorships, extend: FindOrCreate
604
- end
605
- Author.first.authorships_dataset.find_or_create(name: 'Blah', number: 10)
606
-
607
- === many_to_many associations through model tables
608
-
609
- The many_to_many association can be used even when the join table is a table used for a
610
- model. The only requirement is the join table has foreign keys to both the current
611
- model and the associated model. Anytime there is a one_to_many association from model A to
612
- model B, and model B has a many_to_one association to model C, you can use a many_to_many
613
- association from model A to model C.
614
-
615
- class Author < Sequel::Model
616
- one_to_many :authorships
617
- many_to_many :books, join_table: :authorships
618
- end
619
-
620
- class Authorship < Sequel::Model
621
- many_to_one :author
622
- many_to_one :book
623
- end
624
-
625
- @author = Author.first
626
- @author.books
627
-
628
- === many_to_many for three-level associations
629
-
630
- You can even use a many_to_many association between model A and model C if model A has a
631
- one_to_many association to model B, and model B has a one_to_many association to model C.
632
- You just need to use the appropriate :right_key and :right_primary_key options. And in
633
- the reverse direction from model C to model A, you can use a one_through_one association
634
- using the :left_key and :left_primary_key options.
635
-
636
- class Firm < Sequel::Model
637
- one_to_many :clients
638
- many_to_many :invoices, join_table: :clients, right_key: :id, right_primary_key: :client_id
639
- end
640
-
641
- class Client < Sequel::Model
642
- many_to_one :firm
643
- one_to_many :invoices
644
- end
645
-
646
- class Invoice < Sequel::Model
647
- many_to_one :client
648
- one_through_one :firm, join_table: :clients, left_key: :id, left_primary_key: :client_id
649
- end
650
-
651
- Firm.first.invoices
652
- Invoice.first.firm
653
-
654
- To handle cases where there are multiple join tables, you can use the many_through_many
655
- plugin that ships with Sequel.
656
-
657
- === Polymorphic Associations
658
-
659
- Sequel discourages the use of polymorphic associations, which is the reason they
660
- are not supported by default. All polymorphic associations can be made non-polymorphic
661
- by using additional tables and/or columns instead of having a column
662
- containing the associated class name as a string.
663
-
664
- Polymorphic associations break referential integrity and are significantly more
665
- complex than non-polymorphic associations, so their use is not recommended unless
666
- you are stuck with an existing design that uses them.
667
-
668
- If you must use them, look for the sequel_polymorphic external plugin, as it makes using
669
- polymorphic associations in Sequel about as easy as it is in ActiveRecord. However,
670
- here's how they can be done using Sequel's custom associations (the sequel_polymorphic
671
- external plugin is just a generic version of this code):
672
-
673
- Sequel.extension :inflector # for attachable_type.constantize
674
-
675
- class Asset < Sequel::Model
676
- many_to_one :attachable, reciprocal: :assets, reciprocal_type: :one_to_many,
677
- setter: (lambda do |attachable|
678
- self[:attachable_id] = (attachable.pk if attachable)
679
- self[:attachable_type] = (attachable.class.name if attachable)
680
- end),
681
- dataset: (proc do
682
- klass = attachable_type.constantize
683
- klass.where(klass.primary_key=>attachable_id)
684
- end),
685
- eager_loader: (lambda do |eo|
686
- id_map = {}
687
- eo[:rows].each do |asset|
688
- asset.associations[:attachable] = nil
689
- ((id_map[asset.attachable_type] ||= {})[asset.attachable_id] ||= []) << asset
690
- end
691
- id_map.each do |klass_name, id_map|
692
- klass = klass_name.constantize
693
- klass.where(klass.primary_key=>id_map.keys).all do |attach|
694
- id_map[attach.pk].each do |asset|
695
- asset.associations[:attachable] = attach
696
- end
697
- end
698
- end
699
- end)
700
- end
701
-
702
- class Post < Sequel::Model
703
- one_to_many :assets, key: :attachable_id, reciprocal: :attachable, conditions: {attachable_type: 'Post'},
704
- adder: lambda{|asset| asset.update(attachable_id: pk, attachable_type: 'Post')},
705
- remover: lambda{|asset| asset.update(attachable_id: nil, attachable_type: nil)},
706
- clearer: lambda{assets_dataset.update(attachable_id: nil, attachable_type: nil)}
707
- end
708
-
709
- class Note < Sequel::Model
710
- one_to_many :assets, key: :attachable_id, reciprocal: :attachable, conditions: {attachable_type: 'Note'},
711
- adder: lambda{|asset| asset.update(attachable_id: pk, attachable_type: 'Note')},
712
- remover: lambda{|asset| asset.update(attachable_id: nil, attachable_type: nil)},
713
- clearer: lambda{assets_dataset.update(attachable_id: nil, attachable_type: nil)}
714
- end
715
-
716
- @asset.attachable = @post
717
- @asset.attachable = @note
718
-
719
- === Joining on multiple keys
720
-
721
- Let's say you have two tables that are associated with each other with multiple
722
- keys. This can be handled using Sequel's built in composite key support for
723
- associations:
724
-
725
- # Both of these models have an album_id, number, and disc_number fields.
726
- # All FavoriteTracks have an associated track, but not all tracks have an
727
- # associated favorite track
728
-
729
- class Track < Sequel::Model
730
- many_to_one :favorite_track, key: [:disc_number, :number, :album_id], primary_key: [:disc_number, :number, :album_id]
731
- end
732
- class FavoriteTrack < Sequel::Model
733
- one_to_one :tracks, key: [:disc_number, :number, :album_id], primary_key: [:disc_number, :number, :album_id]
734
- end
735
-
736
- === Tree - All Ancestors and Descendants
737
-
738
- Let's say you want to store a tree relationship in your database, it's pretty
739
- simple:
740
-
741
- class Node < Sequel::Model
742
- many_to_one :parent, class: self
743
- one_to_many :children, key: :parent_id, class: self
744
- end
745
-
746
- You can easily get a node's parent with node.parent, and a node's children with
747
- node.children. You can even eager load the relationship up to a certain depth:
748
-
749
- # Eager load three generations of generations of children for a given node
750
- Node.where(id: 1).eager(children: {children: :children}).all.first
751
- # Load parents and grandparents for a group of nodes
752
- Node.where{id < 10}.eager(parent: :parent).all
753
-
754
- What if you want to get all ancestors up to the root node, or all descendants,
755
- without knowing the depth of the tree?
756
-
757
- class Node < Sequel::Model
758
- many_to_one :ancestors, class: self,
759
- eager_loader: (lambda do |eo|
760
- # Handle cases where the root node has the same parent_id as primary_key
761
- # and also when it is NULL
762
- non_root_nodes = eo[:rows].reject do |n|
763
- if [nil, n.pk].include?(n.parent_id)
764
- # Make sure root nodes have their parent association set to nil
765
- n.associations[:parent] = nil
766
- true
767
- else
768
- false
769
- end
770
- end
771
- unless non_root_nodes.empty?
772
- id_map = {}
773
- # Create an map of parent_ids to nodes that have that parent id
774
- non_root_nodes.each{|n| (id_map[n.parent_id] ||= []) << n}
775
- # Doesn't cause an infinite loop, because when only the root node
776
- # is left, this is not called.
777
- Node.where(id: id_map.keys).eager(:ancestors).all do |node|
778
- # Populate the parent association for each node
779
- id_map[node.pk].each{|n| n.associations[:parent] = node}
780
- end
781
- end
782
- end)
783
- many_to_one :descendants, eager_loader: (lambda do |eo|
784
- id_map = {}
785
- eo[:rows].each do |n|
786
- # Initialize an empty array of child associations for each parent node
787
- n.associations[:children] = []
788
- # Populate identity map of nodes
789
- id_map[n.pk] = n
790
- end
791
- # Doesn't cause an infinite loop, because the :eager_loader is not called
792
- # if no records are returned. Exclude id = parent_id to avoid infinite loop
793
- # if the root note is one of the returned records and it has parent_id = id
794
- # instead of parent_id = NULL.
795
- Node.where(parent_id: id_map.keys).exclude(id: :parent_id).eager(:descendants).all do |node|
796
- # Get the parent from the identity map
797
- parent = id_map[node.parent_id]
798
- # Set the child's parent association to the parent
799
- node.associations[:parent] = parent
800
- # Add the child association to the array of children in the parent
801
- parent.associations[:children] << node
802
- end
803
- end)
804
- end
805
-
806
- Note that Sequel ships with an rcte_tree plugin that does all of the above and more:
807
-
808
- class Node < Sequel::Model
809
- plugin :rcte_tree
810
- end
811
-
812
- === Joining multiple keys to a single key, through a third table
813
-
814
- Let's say you have a database of songs, lyrics, and artists. Each song
815
- may or may not have a lyric (most songs are instrumental). The lyric can be
816
- associated to an artist in each of four ways: composer, arranger, vocalist,
817
- or lyricist. These may all be the same, or they could all be different, and
818
- none of them are required. The songs table has a lyric_id field to associate
819
- it to the lyric, and the lyric table has four fields to associate it to the
820
- artist (composer_id, arranger_id, vocalist_id, and lyricist_id).
821
-
822
- What you want to do is get all songs for a given artist, ordered by the song's
823
- name, with no duplicates?
824
-
825
- class Artist < Sequel::Model
826
- one_to_many :songs, order: Sequel[:songs][:name],
827
- dataset: proc{Song.select_all(:songs).join(:lyrics, id: :lyric_id, id=>[:composer_id, :arranger_id, :vocalist_id, :lyricist_id])},
828
- eager_loader: (lambda do |eo|
829
- h = eo[:id_map]
830
- ids = h.keys
831
- eo[:rows].each{|r| r.associations[:songs] = []}
832
- Song.select_all(:songs).
833
- select_append{[lyrics[:composer_id], lyrics[:arranger_id], lyrics[:vocalist_id], lyrics[:lyricist_id]]}.
834
- join(:lyrics, id: :lyric_id){Sequel.or(composer_id: ids, arranger_id: ids, vocalist_id: ids, lyricist_id: ids)}.
835
- order{songs[:name]}.all do |song|
836
- [:composer_id, :arranger_id, :vocalist_id, :lyricist_id].each do |x|
837
- recs = h[song.values.delete(x)]
838
- recs.each{|r| r.associations[:songs] << song} if recs
839
- end
840
- end
841
- eo[:rows].each{|r| r.associations[:songs].uniq!}
842
- end)
843
- end
844
-
845
- === Statistics Associations (Sum of Associated Table Column)
846
-
847
- In addition to getting associated records, you can use Sequel's association support
848
- to get aggregate information for columns in associated tables (sums, averages, etc.).
849
-
850
- Let's say you have a database with projects and tickets. A project can have many
851
- tickets, and each ticket has a number of hours associated with it. You can use the
852
- association support to create a Project association that gives the sum of hours for all
853
- associated tickets.
854
-
855
- class Project < Sequel::Model
856
- one_to_many :tickets
857
- many_to_one :ticket_hours, read_only: true, key: :id,
858
- dataset: proc{Ticket.where(project_id: id).select{sum(hours).as(hours)}},
859
- eager_loader: (lambda do |eo|
860
- eo[:rows].each{|p| p.associations[:ticket_hours] = nil}
861
- Ticket.where(project_id: eo[:id_map].keys).
862
- select_group(:project_id).
863
- select_append{sum(hours).as(hours)}.
864
- all do |t|
865
- p = eo[:id_map][t.values.delete(:project_id)].first
866
- p.associations[:ticket_hours] = t
867
- end
868
- end)
869
- # The association method returns a Ticket object with a single aggregate
870
- # sum-of-hours value, but you want it to return an Integer/Float of just the
871
- # sum of hours, so you call super and return just the sum-of-hours value.
872
- # This works for both lazy loading and eager loading.
873
- def ticket_hours
874
- if s = super
875
- s[:hours]
876
- end
877
- end
878
- end
879
- class Ticket < Sequel::Model
880
- many_to_one :project
881
- end
882
-
883
- Note that it is often better to use a sum cache instead of this approach. You can implement
884
- a sum cache using +after_create+, +after_update+, and +after_delete+ hooks, or preferably using a database trigger.