File: tactical_eager_loading.rb

package info (click to toggle)
ruby-sequel 5.15.0-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 8,856 kB
  • sloc: ruby: 95,762; makefile: 3
file content (199 lines) | stat: -rw-r--r-- 7,997 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# frozen-string-literal: true

module Sequel
  module Plugins
    # The tactical_eager_loading plugin allows you to eagerly load
    # an association for all objects retrieved from the same dataset
    # without calling +eager+ on the dataset.  If you attempt to load
    # associated objects for a record and the association for that
    # object is currently not cached, it assumes you want to get
    # the associated objects for all objects retrieved with the dataset that
    # retrieved the current object.
    #
    # Tactical eager loading only takes affect if you retrieved the
    # current object with Dataset#all, it doesn't work if you
    # retrieved the current object with Dataset#each.
    #
    # Basically, this allows the following code to issue only two queries:
    #
    #   Album.where{id<100}.all do |a|
    #     a.artists
    #   end
    #   # SELECT * FROM albums WHERE (id < 100)
    #   # SELECT * FROM artists WHERE id IN (...)
    #
    # Note that if you are passing a callback to the association method via
    # a block or :callback option, or using the :reload option to reload
    # the association, eager loading will not be done.
    #
    # You can use the :eager_reload option to reload the association for all
    # objects that the current object was retrieved with:
    #
    #   # SELECT * FROM albums WHERE (id < 100)
    #   albums = Album.where{id<100}.all
    #
    #   # Eagerly load all artists for these albums
    #   # SELECT * FROM artists WHERE id IN (...)
    #   albums.first.artists
    #
    #   # Do something that may affect which artists are associated to the albums
    #
    #   # Eagerly reload all artists for these albums
    #   # SELECT * FROM artists WHERE id IN (...)
    #   albums.first.artists(eager_reload: true)
    # 
    # You can also use the :eager option to specify dependent associations
    # to eager load:
    #
    #  albums = Album.where{id<100}.all
    #
    #   # Eager load all artists for these albums, and all albums for those artists
    #   # SELECT * FROM artists WHERE id IN (...)
    #   # SELECT * FROM albums WHERE artist_id IN (...)
    #   albums.first.artists(eager: :albums)
    #
    # You can also use :eager to specify an eager callback. For example:
    #
    #   albums = Album.where{id<100}.all
    #
    #   # Eagerly load all artists whose name starts with A-M for these albums
    #   # SELECT * FROM artists WHERE name > 'N' AND id IN (...)
    #   albums.first.artists(eager: lambda{|ds| ds.where(Sequel[:name] > 'N')})
    #
    # Note that the :eager option only takes effect if the association
    # has not already been loaded for the model.
    #
    # The tactical_eager_loading plugin also allows transparent eager
    # loading when calling association methods on associated objects
    # eagerly loaded via Dataset#eager_graph.  This can reduce N queries
    # to a single query when iterating over all associated objects.
    # Consider the following code:
    #
    #   artists = Artist.eager_graph(:albums).all
    #   artists.each do |artist|
    #     artist.albums.each do |album|
    #       album.tracks
    #     end
    #   end
    #
    # By default this will issue a single query to load the artists and
    # albums, and then one query for each album to load the tracks for
    # the album:
    #
    #   # SELECT artists.id, ...
    #            albums.id, ...
    #   # FROM artists
    #   # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id);
    #   # SELECT * FROM tracks WHERE album_id = 1;
    #   # SELECT * FROM tracks WHERE album_id = 2;
    #   # SELECT * FROM tracks WHERE album_id = 10;
    #   # ...
    #
    # With the tactical_eager_loading plugin, this uses the same
    # query to load the artists and albums, but then issues a single query
    # to load the tracks for all albums.
    #
    #   # SELECT artists.id, ...
    #            albums.id, ...
    #   # FROM artists
    #   # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id);
    #   # SELECT * FROM tracks WHERE (tracks.album_id IN (1, 2, 10, ...));
    #
    # Note that transparent eager loading for associated objects
    # loaded by eager_graph will only take place if the associated classes
    # also use the tactical_eager_loading plugin.
    #
    # When using this plugin, calling association methods on separate
    # instances of the same result set is not thread-safe, because this
    # plugin attempts to modify all instances of the same result set
    # to eagerly set the associated objects, and having separate threads
    # modify the same model instance is not thread-safe.
    #
    # Usage:
    #
    #   # Make all model subclass instances use tactical eager loading (called before loading subclasses)
    #   Sequel::Model.plugin :tactical_eager_loading
    #
    #   # Make the Album class use tactical eager loading
    #   Album.plugin :tactical_eager_loading
    module TacticalEagerLoading
      module InstanceMethods
        # The dataset that retrieved this object, set if the object was
        # reteived via Dataset#all.
        attr_accessor :retrieved_by

        # All model objects retrieved with this object, set if the object was
        # reteived via Dataset#all.
        attr_accessor :retrieved_with

        # Remove retrieved_by and retrieved_with when marshalling.  retrieved_by
        # contains unmarshallable objects, and retrieved_with can be very large
        # and is not helpful without retrieved_by.
        def marshallable!
          @retrieved_by = nil
          @retrieved_with = nil
          super
        end

        private

        # If there the association is not in the associations cache and the object
        # was reteived via Dataset#all, eagerly load the association for all model
        # objects retrieved with the current object.
        def load_associated_objects(opts, dynamic_opts=OPTS, &block)
          dynamic_opts = load_association_objects_options(dynamic_opts, &block)
          name = opts[:name]
          if (!associations.include?(name) || dynamic_opts[:eager_reload]) && opts[:allow_eager] != false && retrieved_by && !frozen? && !dynamic_opts[:callback] && !dynamic_opts[:reload]
            begin
              retrieved_by.send(:eager_load, retrieved_with.reject(&:frozen?), name=>dynamic_opts[:eager] || OPTS)
            rescue Sequel::UndefinedAssociation
              # This can happen if class table inheritance is used and the association
              # is only defined in a subclass.  This particular instance can use the
              # association, but it can't be eagerly loaded as the parent class doesn't
              # have access to the association, and that's the class doing the eager loading.
              nil
            end
          end
          super
        end
      end

      module DatasetMethods
        private

        # Set the retrieved_with and retrieved_by attributes for each of the associated objects
        # created by the eager graph loader with the appropriate class dataset and array of objects.
        def _eager_graph_build_associations(_, egl)
          objects = super

          master = egl.master
          egl.records_map.each do |k, v|
            next if k == master || v.empty?

            by = opts[:graph][:table_aliases][k]
            values = v.values

            values.each do |o|
              next unless o.is_a?(TacticalEagerLoading::InstanceMethods) && !o.retrieved_by
              o.retrieved_by = by
              o.retrieved_with = values
            end
          end

          objects
        end

        # Set the retrieved_with and retrieved_by attributes for each object
        # with the current dataset and array of all objects.
        def post_load(objects)
          super
          objects.each do |o|
            next unless o.is_a?(Sequel::Model)
            o.retrieved_by = self
            o.retrieved_with = objects
          end
        end
      end
    end
  end
end