sequel 5.80.0 → 5.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequel +9 -4
  3. data/lib/sequel/adapters/ado.rb +1 -1
  4. data/lib/sequel/adapters/ibmdb.rb +1 -0
  5. data/lib/sequel/adapters/jdbc/db2.rb +2 -2
  6. data/lib/sequel/adapters/jdbc/derby.rb +3 -3
  7. data/lib/sequel/adapters/jdbc/h2.rb +2 -2
  8. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -2
  9. data/lib/sequel/adapters/jdbc/jtds.rb +2 -2
  10. data/lib/sequel/adapters/jdbc/mysql.rb +1 -1
  11. data/lib/sequel/adapters/jdbc/oracle.rb +5 -5
  12. data/lib/sequel/adapters/jdbc/postgresql.rb +5 -5
  13. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +6 -6
  14. data/lib/sequel/adapters/jdbc/sqlite.rb +2 -2
  15. data/lib/sequel/adapters/jdbc/sqlserver.rb +2 -2
  16. data/lib/sequel/adapters/jdbc.rb +8 -8
  17. data/lib/sequel/adapters/mysql2.rb +8 -1
  18. data/lib/sequel/adapters/shared/access.rb +1 -0
  19. data/lib/sequel/adapters/shared/db2.rb +1 -1
  20. data/lib/sequel/adapters/shared/mssql.rb +18 -5
  21. data/lib/sequel/adapters/shared/mysql.rb +8 -4
  22. data/lib/sequel/adapters/shared/oracle.rb +1 -0
  23. data/lib/sequel/adapters/shared/postgres.rb +106 -13
  24. data/lib/sequel/adapters/shared/sqlite.rb +4 -2
  25. data/lib/sequel/adapters/sqlite.rb +4 -0
  26. data/lib/sequel/adapters/trilogy.rb +1 -2
  27. data/lib/sequel/connection_pool/sharded_threaded.rb +26 -10
  28. data/lib/sequel/connection_pool/threaded.rb +26 -10
  29. data/lib/sequel/connection_pool.rb +2 -2
  30. data/lib/sequel/core.rb +15 -0
  31. data/lib/sequel/database/connecting.rb +20 -26
  32. data/lib/sequel/database/dataset_defaults.rb +3 -3
  33. data/lib/sequel/database/misc.rb +46 -10
  34. data/lib/sequel/database/query.rb +11 -11
  35. data/lib/sequel/database/schema_generator.rb +8 -0
  36. data/lib/sequel/database/schema_methods.rb +17 -1
  37. data/lib/sequel/dataset/actions.rb +9 -1
  38. data/lib/sequel/dataset/deprecated_singleton_class_methods.rb +1 -1
  39. data/lib/sequel/dataset/prepared_statements.rb +2 -1
  40. data/lib/sequel/dataset/query.rb +9 -5
  41. data/lib/sequel/dataset/sql.rb +25 -5
  42. data/lib/sequel/extensions/caller_logging.rb +2 -0
  43. data/lib/sequel/extensions/connection_validator.rb +15 -10
  44. data/lib/sequel/extensions/dataset_run.rb +41 -0
  45. data/lib/sequel/extensions/migration.rb +23 -3
  46. data/lib/sequel/extensions/null_dataset.rb +2 -2
  47. data/lib/sequel/extensions/pg_auto_parameterize.rb +1 -1
  48. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +93 -10
  49. data/lib/sequel/extensions/pg_enum.rb +3 -3
  50. data/lib/sequel/extensions/pg_json_ops.rb +642 -9
  51. data/lib/sequel/extensions/pg_row.rb +3 -1
  52. data/lib/sequel/extensions/pg_schema_caching.rb +90 -0
  53. data/lib/sequel/extensions/provenance.rb +2 -0
  54. data/lib/sequel/extensions/query_blocker.rb +172 -0
  55. data/lib/sequel/extensions/schema_caching.rb +24 -9
  56. data/lib/sequel/extensions/schema_dumper.rb +16 -4
  57. data/lib/sequel/extensions/sqlite_json_ops.rb +1 -1
  58. data/lib/sequel/extensions/stdio_logger.rb +48 -0
  59. data/lib/sequel/extensions/string_agg.rb +17 -4
  60. data/lib/sequel/extensions/temporarily_release_connection.rb +178 -0
  61. data/lib/sequel/extensions/virtual_row_method_block.rb +1 -0
  62. data/lib/sequel/model/associations.rb +28 -3
  63. data/lib/sequel/model/base.rb +67 -18
  64. data/lib/sequel/plugins/association_pks.rb +1 -1
  65. data/lib/sequel/plugins/column_encryption.rb +1 -1
  66. data/lib/sequel/plugins/composition.rb +1 -1
  67. data/lib/sequel/plugins/defaults_setter.rb +16 -4
  68. data/lib/sequel/plugins/enum.rb +1 -1
  69. data/lib/sequel/plugins/forbid_lazy_load.rb +14 -1
  70. data/lib/sequel/plugins/input_transformer.rb +1 -1
  71. data/lib/sequel/plugins/inspect_pk.rb +44 -0
  72. data/lib/sequel/plugins/instance_filters.rb +4 -1
  73. data/lib/sequel/plugins/inverted_subsets.rb +1 -0
  74. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  75. data/lib/sequel/plugins/nested_attributes.rb +10 -5
  76. data/lib/sequel/plugins/optimistic_locking.rb +2 -0
  77. data/lib/sequel/plugins/paged_operations.rb +5 -2
  78. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +6 -1
  79. data/lib/sequel/plugins/pg_auto_validate_enums.rb +88 -0
  80. data/lib/sequel/plugins/pg_eager_any_typed_array.rb +95 -0
  81. data/lib/sequel/plugins/rcte_tree.rb +1 -1
  82. data/lib/sequel/plugins/serialization.rb +11 -5
  83. data/lib/sequel/plugins/sql_comments.rb +7 -2
  84. data/lib/sequel/plugins/static_cache_cache.rb +50 -13
  85. data/lib/sequel/plugins/subset_conditions.rb +85 -5
  86. data/lib/sequel/plugins/subset_static_cache.rb +263 -0
  87. data/lib/sequel/plugins/tactical_eager_loading.rb +6 -2
  88. data/lib/sequel/plugins/validate_associated.rb +1 -1
  89. data/lib/sequel/sql.rb +16 -6
  90. data/lib/sequel/version.rb +1 -1
  91. metadata +12 -234
  92. data/CHANGELOG +0 -1355
  93. data/README.rdoc +0 -936
  94. data/doc/advanced_associations.rdoc +0 -884
  95. data/doc/association_basics.rdoc +0 -1859
  96. data/doc/bin_sequel.rdoc +0 -146
  97. data/doc/cheat_sheet.rdoc +0 -255
  98. data/doc/code_order.rdoc +0 -102
  99. data/doc/core_extensions.rdoc +0 -405
  100. data/doc/dataset_basics.rdoc +0 -96
  101. data/doc/dataset_filtering.rdoc +0 -222
  102. data/doc/extensions.rdoc +0 -77
  103. data/doc/fork_safety.rdoc +0 -84
  104. data/doc/mass_assignment.rdoc +0 -98
  105. data/doc/migration.rdoc +0 -660
  106. data/doc/model_dataset_method_design.rdoc +0 -129
  107. data/doc/model_hooks.rdoc +0 -254
  108. data/doc/model_plugins.rdoc +0 -270
  109. data/doc/mssql_stored_procedures.rdoc +0 -43
  110. data/doc/object_model.rdoc +0 -563
  111. data/doc/opening_databases.rdoc +0 -436
  112. data/doc/postgresql.rdoc +0 -611
  113. data/doc/prepared_statements.rdoc +0 -144
  114. data/doc/querying.rdoc +0 -1070
  115. data/doc/reflection.rdoc +0 -120
  116. data/doc/release_notes/5.0.0.txt +0 -159
  117. data/doc/release_notes/5.1.0.txt +0 -31
  118. data/doc/release_notes/5.10.0.txt +0 -84
  119. data/doc/release_notes/5.11.0.txt +0 -83
  120. data/doc/release_notes/5.12.0.txt +0 -141
  121. data/doc/release_notes/5.13.0.txt +0 -27
  122. data/doc/release_notes/5.14.0.txt +0 -63
  123. data/doc/release_notes/5.15.0.txt +0 -39
  124. data/doc/release_notes/5.16.0.txt +0 -110
  125. data/doc/release_notes/5.17.0.txt +0 -31
  126. data/doc/release_notes/5.18.0.txt +0 -69
  127. data/doc/release_notes/5.19.0.txt +0 -28
  128. data/doc/release_notes/5.2.0.txt +0 -33
  129. data/doc/release_notes/5.20.0.txt +0 -89
  130. data/doc/release_notes/5.21.0.txt +0 -87
  131. data/doc/release_notes/5.22.0.txt +0 -48
  132. data/doc/release_notes/5.23.0.txt +0 -56
  133. data/doc/release_notes/5.24.0.txt +0 -56
  134. data/doc/release_notes/5.25.0.txt +0 -32
  135. data/doc/release_notes/5.26.0.txt +0 -35
  136. data/doc/release_notes/5.27.0.txt +0 -21
  137. data/doc/release_notes/5.28.0.txt +0 -16
  138. data/doc/release_notes/5.29.0.txt +0 -22
  139. data/doc/release_notes/5.3.0.txt +0 -121
  140. data/doc/release_notes/5.30.0.txt +0 -20
  141. data/doc/release_notes/5.31.0.txt +0 -148
  142. data/doc/release_notes/5.32.0.txt +0 -46
  143. data/doc/release_notes/5.33.0.txt +0 -24
  144. data/doc/release_notes/5.34.0.txt +0 -40
  145. data/doc/release_notes/5.35.0.txt +0 -56
  146. data/doc/release_notes/5.36.0.txt +0 -60
  147. data/doc/release_notes/5.37.0.txt +0 -30
  148. data/doc/release_notes/5.38.0.txt +0 -28
  149. data/doc/release_notes/5.39.0.txt +0 -19
  150. data/doc/release_notes/5.4.0.txt +0 -80
  151. data/doc/release_notes/5.40.0.txt +0 -40
  152. data/doc/release_notes/5.41.0.txt +0 -25
  153. data/doc/release_notes/5.42.0.txt +0 -136
  154. data/doc/release_notes/5.43.0.txt +0 -98
  155. data/doc/release_notes/5.44.0.txt +0 -32
  156. data/doc/release_notes/5.45.0.txt +0 -34
  157. data/doc/release_notes/5.46.0.txt +0 -87
  158. data/doc/release_notes/5.47.0.txt +0 -59
  159. data/doc/release_notes/5.48.0.txt +0 -14
  160. data/doc/release_notes/5.49.0.txt +0 -59
  161. data/doc/release_notes/5.5.0.txt +0 -61
  162. data/doc/release_notes/5.50.0.txt +0 -78
  163. data/doc/release_notes/5.51.0.txt +0 -47
  164. data/doc/release_notes/5.52.0.txt +0 -87
  165. data/doc/release_notes/5.53.0.txt +0 -23
  166. data/doc/release_notes/5.54.0.txt +0 -27
  167. data/doc/release_notes/5.55.0.txt +0 -21
  168. data/doc/release_notes/5.56.0.txt +0 -51
  169. data/doc/release_notes/5.57.0.txt +0 -23
  170. data/doc/release_notes/5.58.0.txt +0 -31
  171. data/doc/release_notes/5.59.0.txt +0 -73
  172. data/doc/release_notes/5.6.0.txt +0 -31
  173. data/doc/release_notes/5.60.0.txt +0 -22
  174. data/doc/release_notes/5.61.0.txt +0 -43
  175. data/doc/release_notes/5.62.0.txt +0 -132
  176. data/doc/release_notes/5.63.0.txt +0 -33
  177. data/doc/release_notes/5.64.0.txt +0 -50
  178. data/doc/release_notes/5.65.0.txt +0 -21
  179. data/doc/release_notes/5.66.0.txt +0 -24
  180. data/doc/release_notes/5.67.0.txt +0 -32
  181. data/doc/release_notes/5.68.0.txt +0 -61
  182. data/doc/release_notes/5.69.0.txt +0 -26
  183. data/doc/release_notes/5.7.0.txt +0 -108
  184. data/doc/release_notes/5.70.0.txt +0 -35
  185. data/doc/release_notes/5.71.0.txt +0 -21
  186. data/doc/release_notes/5.72.0.txt +0 -33
  187. data/doc/release_notes/5.73.0.txt +0 -66
  188. data/doc/release_notes/5.74.0.txt +0 -45
  189. data/doc/release_notes/5.75.0.txt +0 -35
  190. data/doc/release_notes/5.76.0.txt +0 -86
  191. data/doc/release_notes/5.77.0.txt +0 -63
  192. data/doc/release_notes/5.78.0.txt +0 -67
  193. data/doc/release_notes/5.79.0.txt +0 -28
  194. data/doc/release_notes/5.8.0.txt +0 -170
  195. data/doc/release_notes/5.80.0.txt +0 -40
  196. data/doc/release_notes/5.9.0.txt +0 -99
  197. data/doc/schema_modification.rdoc +0 -679
  198. data/doc/security.rdoc +0 -443
  199. data/doc/sharding.rdoc +0 -286
  200. data/doc/sql.rdoc +0 -648
  201. data/doc/testing.rdoc +0 -190
  202. data/doc/thread_safety.rdoc +0 -15
  203. data/doc/transactions.rdoc +0 -250
  204. data/doc/validations.rdoc +0 -558
  205. 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.