File: eager_graph_eager.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (139 lines) | stat: -rw-r--r-- 5,307 bytes parent folder | download | duplicates (4)
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
# frozen-string-literal: true

module Sequel
  module Plugins
    # The eager_graph_eager plugin allows for chaining eager loads after eager_graph
    # loads.  Given the following model associations:
    #
    #   Band.one_to_many :albums
    #   Album.one_to_many :tracks
    #
    # Let's say you wanted to return bands ordered by album name, and eagerly load
    # those albums, you can do that using:
    #
    #   Band.eager_graph(:albums).order{albums[:name]}
    #
    # Let's say you also wanted to eagerly load the tracks for each album. You could
    # just add them to the eager_graph call:
    #  
    #   Band.eager_graph(albums: :tracks).order{albums[:name]}
    #
    # However, the bloats the result set, and you aren't ordering by the track
    # information, so a join is not required.  The eager_graph_eager plugin allows
    # you to specify that the tracks be eagerly loaded in a separate query after
    # the eager_graph load of albums:
    # 
    #   Band.eager_graph(:albums).eager_graph_eager([:albums], :tracks).order{albums[:name]}
    #
    # <tt>Dataset#eager_graph_eager</tt>'s first argument is a dependency chain, specified
    # as an array of symbols.  This specifies the point at which to perform the eager load.
    # The remaining arguments are arguments that could be passed to Dataset#eager to specify
    # what dependent associations should be loaded at that point.
    #
    # If you also have the following model association:
    #
    #   Track.one_to_many :lyrics
    #
    # Here's some different ways of performing eager loading:
    #
    #   # 4 Queries: bands, albums, tracks, lyrics
    #   Band.eager(albums: {tracks: :lyrics})
    #
    #   # 1 Query: bands+albums+tracks+lyrics
    #   Band.eager_graph(albums: {tracks: :lyrics})
    #
    #   # 3 Queries: bands+albums, tracks, lyrics
    #   Band.eager_graph(:albums).eager_graph_eager([:albums], tracks: :lyrics)
    #
    #   # 2 Queries: bands+albums+tracks, lyrics
    #   Band.eager_graph(albums: :tracks).eager_graph_eager([:albums, :tracks], :lyrics)
    #
    #   # 2 Queries: bands+albums, tracks+lyrics
    #   Band.eager_graph(:albums).eager_graph_eager([:albums], tracks: proc{|ds| ds.eager_graph(:lyrics)})
    #
    # Usage:
    #
    #   # Support eager_graph_eager in all subclass datasets (called before loading subclasses)
    #   Sequel::Model.plugin :eager_graph_eager
    #
    #   # Support eager_graph_eager in Album class datasets
    #   Album.plugin :eager_graph_eager
    module EagerGraphEager
      module DatasetMethods
        # Specify for the given dependency chain, after loading objects for the
        # current dataset via eager_graph, eagerly load the given associations at that point in the
        # dependency chain.
        #
        # dependency_chain :: Array of association symbols, with the first association symbol
        #                     specifying an association in the dataset's model, the next
        #                     association specifying an association in the previous association's
        #                     associated model, and so on.
        # assocs :: Symbols or hashes specifying associations to eagerly load at the point
        #           specified by the dependency chain.
        def eager_graph_eager(dependency_chain, *assocs)
          unless dependency_chain.is_a?(Array) && dependency_chain.all?{|s| s.is_a?(Symbol)} && !dependency_chain.empty?
            raise Error, "eager_graph_eager first argument must be array of symbols"
          end

          current = model
          deps = dependency_chain.map do |dep|
            unless ref = current.association_reflection(dep)
              raise Error, "invalid association #{dep.inspect} for #{current.inspect}"
            end
            current = ref.associated_class
            [dep, ref.returns_array?]
          end
          assocs = current.dataset.send(:eager_options_for_associations, assocs)

          deps.each(&:freeze)
          deps.unshift(current)
          deps.freeze

          assocs.freeze

          if h = @opts[:eager_graph_eager]
            h = Hash[h]
            h[deps] = assocs
          else
            h = {deps => assocs}
          end

          clone(:eager_graph_eager=>h.freeze)
        end

        protected

        # After building objects from the rows, if eager_graph_eager has been
        # called on the datasets, for each dependency chain specified, eagerly
        # load the appropriate associations.
        def eager_graph_build_associations(rows)
          objects = super

          if eager_data = @opts[:eager_graph_eager]
            eager_data.each do |deps, assocs|
              current = objects

              last_class, *deps = deps
              deps.each do |dep, is_multiple|
                current_assocs = current.map(&:associations)

                if is_multiple
                  current = current_assocs.flat_map{|a| a[dep]}
                else
                  current = current_assocs.map{|a| a[dep]}
                  current.compact!
                end

                current.uniq!(&:object_id)
              end

              last_class.dataset.send(:eager_load, current, assocs)
            end
          end

          objects
        end
      end
    end
  end
end