sequel 5.82.0 → 5.84.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sequel +9 -17
- data/lib/sequel/adapters/jdbc/derby.rb +1 -1
- data/lib/sequel/adapters/shared/db2.rb +1 -1
- data/lib/sequel/adapters/shared/mssql.rb +14 -2
- data/lib/sequel/adapters/shared/postgres.rb +42 -4
- data/lib/sequel/adapters/shared/sqlite.rb +3 -1
- data/lib/sequel/database/connecting.rb +1 -4
- data/lib/sequel/database/misc.rb +27 -7
- data/lib/sequel/database/schema_methods.rb +17 -1
- data/lib/sequel/dataset/sql.rb +13 -0
- data/lib/sequel/extensions/pg_json_ops.rb +328 -1
- data/lib/sequel/extensions/stdio_logger.rb +48 -0
- data/lib/sequel/extensions/string_agg.rb +15 -2
- data/lib/sequel/plugins/defaults_setter.rb +16 -4
- data/lib/sequel/plugins/optimistic_locking.rb +2 -0
- data/lib/sequel/sql.rb +8 -5
- data/lib/sequel/version.rb +1 -1
- metadata +4 -235
- data/CHANGELOG +0 -1377
- data/README.rdoc +0 -936
- data/doc/advanced_associations.rdoc +0 -884
- data/doc/association_basics.rdoc +0 -1859
- data/doc/bin_sequel.rdoc +0 -146
- data/doc/cheat_sheet.rdoc +0 -255
- data/doc/code_order.rdoc +0 -104
- data/doc/core_extensions.rdoc +0 -405
- data/doc/dataset_basics.rdoc +0 -96
- data/doc/dataset_filtering.rdoc +0 -222
- data/doc/extensions.rdoc +0 -77
- data/doc/fork_safety.rdoc +0 -84
- data/doc/mass_assignment.rdoc +0 -98
- data/doc/migration.rdoc +0 -660
- data/doc/model_dataset_method_design.rdoc +0 -129
- data/doc/model_hooks.rdoc +0 -254
- data/doc/model_plugins.rdoc +0 -270
- data/doc/mssql_stored_procedures.rdoc +0 -43
- data/doc/object_model.rdoc +0 -563
- data/doc/opening_databases.rdoc +0 -439
- data/doc/postgresql.rdoc +0 -611
- data/doc/prepared_statements.rdoc +0 -144
- data/doc/querying.rdoc +0 -1070
- data/doc/reflection.rdoc +0 -120
- data/doc/release_notes/5.0.0.txt +0 -159
- data/doc/release_notes/5.1.0.txt +0 -31
- data/doc/release_notes/5.10.0.txt +0 -84
- data/doc/release_notes/5.11.0.txt +0 -83
- data/doc/release_notes/5.12.0.txt +0 -141
- data/doc/release_notes/5.13.0.txt +0 -27
- data/doc/release_notes/5.14.0.txt +0 -63
- data/doc/release_notes/5.15.0.txt +0 -39
- data/doc/release_notes/5.16.0.txt +0 -110
- data/doc/release_notes/5.17.0.txt +0 -31
- data/doc/release_notes/5.18.0.txt +0 -69
- data/doc/release_notes/5.19.0.txt +0 -28
- data/doc/release_notes/5.2.0.txt +0 -33
- data/doc/release_notes/5.20.0.txt +0 -89
- data/doc/release_notes/5.21.0.txt +0 -87
- data/doc/release_notes/5.22.0.txt +0 -48
- data/doc/release_notes/5.23.0.txt +0 -56
- data/doc/release_notes/5.24.0.txt +0 -56
- data/doc/release_notes/5.25.0.txt +0 -32
- data/doc/release_notes/5.26.0.txt +0 -35
- data/doc/release_notes/5.27.0.txt +0 -21
- data/doc/release_notes/5.28.0.txt +0 -16
- data/doc/release_notes/5.29.0.txt +0 -22
- data/doc/release_notes/5.3.0.txt +0 -121
- data/doc/release_notes/5.30.0.txt +0 -20
- data/doc/release_notes/5.31.0.txt +0 -148
- data/doc/release_notes/5.32.0.txt +0 -46
- data/doc/release_notes/5.33.0.txt +0 -24
- data/doc/release_notes/5.34.0.txt +0 -40
- data/doc/release_notes/5.35.0.txt +0 -56
- data/doc/release_notes/5.36.0.txt +0 -60
- data/doc/release_notes/5.37.0.txt +0 -30
- data/doc/release_notes/5.38.0.txt +0 -28
- data/doc/release_notes/5.39.0.txt +0 -19
- data/doc/release_notes/5.4.0.txt +0 -80
- data/doc/release_notes/5.40.0.txt +0 -40
- data/doc/release_notes/5.41.0.txt +0 -25
- data/doc/release_notes/5.42.0.txt +0 -136
- data/doc/release_notes/5.43.0.txt +0 -98
- data/doc/release_notes/5.44.0.txt +0 -32
- data/doc/release_notes/5.45.0.txt +0 -34
- data/doc/release_notes/5.46.0.txt +0 -87
- data/doc/release_notes/5.47.0.txt +0 -59
- data/doc/release_notes/5.48.0.txt +0 -14
- data/doc/release_notes/5.49.0.txt +0 -59
- data/doc/release_notes/5.5.0.txt +0 -61
- data/doc/release_notes/5.50.0.txt +0 -78
- data/doc/release_notes/5.51.0.txt +0 -47
- data/doc/release_notes/5.52.0.txt +0 -87
- data/doc/release_notes/5.53.0.txt +0 -23
- data/doc/release_notes/5.54.0.txt +0 -27
- data/doc/release_notes/5.55.0.txt +0 -21
- data/doc/release_notes/5.56.0.txt +0 -51
- data/doc/release_notes/5.57.0.txt +0 -23
- data/doc/release_notes/5.58.0.txt +0 -31
- data/doc/release_notes/5.59.0.txt +0 -73
- data/doc/release_notes/5.6.0.txt +0 -31
- data/doc/release_notes/5.60.0.txt +0 -22
- data/doc/release_notes/5.61.0.txt +0 -43
- data/doc/release_notes/5.62.0.txt +0 -132
- data/doc/release_notes/5.63.0.txt +0 -33
- data/doc/release_notes/5.64.0.txt +0 -50
- data/doc/release_notes/5.65.0.txt +0 -21
- data/doc/release_notes/5.66.0.txt +0 -24
- data/doc/release_notes/5.67.0.txt +0 -32
- data/doc/release_notes/5.68.0.txt +0 -61
- data/doc/release_notes/5.69.0.txt +0 -26
- data/doc/release_notes/5.7.0.txt +0 -108
- data/doc/release_notes/5.70.0.txt +0 -35
- data/doc/release_notes/5.71.0.txt +0 -21
- data/doc/release_notes/5.72.0.txt +0 -33
- data/doc/release_notes/5.73.0.txt +0 -66
- data/doc/release_notes/5.74.0.txt +0 -45
- data/doc/release_notes/5.75.0.txt +0 -35
- data/doc/release_notes/5.76.0.txt +0 -86
- data/doc/release_notes/5.77.0.txt +0 -63
- data/doc/release_notes/5.78.0.txt +0 -67
- data/doc/release_notes/5.79.0.txt +0 -28
- data/doc/release_notes/5.8.0.txt +0 -170
- data/doc/release_notes/5.80.0.txt +0 -40
- data/doc/release_notes/5.81.0.txt +0 -31
- data/doc/release_notes/5.82.0.txt +0 -61
- data/doc/release_notes/5.9.0.txt +0 -99
- data/doc/schema_modification.rdoc +0 -679
- data/doc/security.rdoc +0 -443
- data/doc/sharding.rdoc +0 -286
- data/doc/sql.rdoc +0 -648
- data/doc/testing.rdoc +0 -204
- data/doc/thread_safety.rdoc +0 -15
- data/doc/transactions.rdoc +0 -250
- data/doc/validations.rdoc +0 -558
- 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.
|