File: subset_static_cache.rb

package info (click to toggle)
ruby-sequel 5.97.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 11,188 kB
  • sloc: ruby: 123,115; makefile: 3
file content (263 lines) | stat: -rw-r--r-- 9,725 bytes parent folder | download
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# frozen-string-literal: true

module Sequel
  module Plugins
    # The subset_static_cache plugin is designed for model subsets that are not modified at all
    # in production use cases, or at least where modifications to them would usually
    # coincide with an application restart.  When caching a model subset, it 
    # retrieves all rows in the database and statically caches a ruby array and hash
    # keyed on primary key containing all of the model instances.  All of these cached
    # instances are frozen so they won't be modified unexpectedly.
    #
    # With the following code:
    #
    #   class StatusType < Sequel::Model
    #     dataset_module do
    #       where :available, hidden: false
    #     end
    #     cache_subset :available
    #   end
    #   
    # The following methods will use the cache and not issue a database query:
    #
    # * StatusType.available.with_pk
    # * StatusType.available.all
    # * StatusType.available.each
    # * StatusType.available.first (without block, only supporting no arguments or single integer argument)
    # * StatusType.available.count (without an argument or block)
    # * StatusType.available.map
    # * StatusType.available.as_hash
    # * StatusType.available.to_hash
    # * StatusType.available.to_hash_groups
    #
    # The cache is not used if you chain methods before or after calling the cached
    # method, as doing so would not be safe:
    #
    #   StatusType.where{number > 1}.available.all 
    #   StatusType.available.where{number > 1}.all
    #
    # The cache is also not used if you change the class's dataset after caching
    # the subset, or in subclasses of the model.
    #
    # You should not modify any row that is statically cached when using this plugin,
    # as otherwise you will get different results for cached and uncached method
    # calls.
    module SubsetStaticCache
      def self.configure(model)
        model.class_exec do
          @subset_static_caches ||= ({}.compare_by_identity)
        end
      end

      module ClassMethods
        # Cache the given subset statically, so that calling the subset method on
        # the model will return a dataset that will return cached results instead
        # of issuing database queries (assuming the cache has the necessary
        # information).
        #
        # The model must already respond to the given method before cache_subset
        # is called.
        def cache_subset(meth)
          ds = send(meth).with_extend(CachedDatasetMethods)
          cache = ds.instance_variable_get(:@cache)

          rows, hash = subset_static_cache_rows(ds, meth)
          cache[:subset_static_cache_all] = rows
          cache[:subset_static_cache_map] = hash

          caches = @subset_static_caches
          caches[meth] = ds
          model = self
          subset_static_cache_module.send(:define_method, meth) do
            if (model == self) && (cached_dataset = caches[meth])
              cached_dataset
            else
              super()
            end
          end
          nil
        end

        Plugins.after_set_dataset(self, :clear_subset_static_caches)
        Plugins.inherited_instance_variables(self, :@subset_static_caches=>proc{{}.compare_by_identity})

        private

        # Clear the subset_static_caches.  This is used if the model dataset
        # changes, to prevent cached values from being used.
        def clear_subset_static_caches
          @subset_static_caches.clear
        end

        # A module for the subset static cache methods, so that you can define
        # a singleton method in the class with the same name, and call super
        # to get default behavior.
        def subset_static_cache_module
          return @subset_static_cache_module if @subset_static_cache_module

          # Ensure dataset_methods module is defined and class is extended with
          # it before calling creating this module.
          dataset_methods_module

          mod_name = "#{name}::@subset_static_cache_module"
          Sequel.synchronize{@subset_static_cache_module ||= Sequel.set_temp_name(Module.new){mod_name}}
          extend(@subset_static_cache_module)
          @subset_static_cache_module
        end
         
        # Return the frozen array and hash used for caching the subset
        # of the given dataset.
        def subset_static_cache_rows(ds, meth)
          all = load_subset_static_cache_rows(ds, meth)
          h = {}
          all.each do |o|
            o.errors.freeze
            h[o.pk.freeze] = o.freeze
          end
          [all, h.freeze]
        end

        # Return a frozen array for all rows in the dataset.
        def load_subset_static_cache_rows(ds, meth)
          ret = super if defined?(super)
          ret || ds.all.freeze
        end
      end

      module CachedDatasetMethods
        # An array of all of the dataset's instances, without issuing a database
        # query. If a block is given, yields each instance to the block.
        def all(&block)
          return super unless all = @cache[:subset_static_cache_all]

          array = all.dup
          array.each(&block) if block
          array
        end

        # Get the number of records in the cache, without issuing a database query,
        # if no arguments or block are provided.
        def count(*a, &block)
          if a.empty? && !block && (all = @cache[:subset_static_cache_all])
            all.size
          else
            super
          end
        end

        # If a block is given, multiple arguments are given, or a single
        # non-Integer argument is given, performs the default behavior of
        # issuing a database query.  Otherwise, uses the cached values
        # to return either the first cached instance (no arguments) or an
        # array containing the number of instances specified (single integer
        # argument).
        def first(*args)
          if !defined?(yield) && args.length <= 1 && (args.length == 0 || args[0].is_a?(Integer)) && (all = @cache[:subset_static_cache_all])
            all.first(*args)
          else
            super
          end
        end

        # Return the frozen object with the given pk, or nil if no such object exists
        # in the cache, without issuing a database query.
        def with_pk(pk)
          if cache = @cache[:subset_static_cache_map]
            cache[pk]
          else
            super
          end
        end

        # Yield each of the dataset's frozen instances to the block, without issuing a database
        # query.
        def each(&block)
          return super unless all = @cache[:subset_static_cache_all]
          all.each(&block)
        end

        # Use the cache instead of a query to get the results.
        def map(column=nil, &block)
          return super unless all = @cache[:subset_static_cache_all]
          if column
            raise(Error, "Cannot provide both column and block to map") if block
            if column.is_a?(Array)
              all.map{|r| r.values.values_at(*column)}
            else
              all.map{|r| r[column]}
            end
          else
            all.map(&block)
          end
        end

        # Use the cache instead of a query to get the results if possible
        def as_hash(key_column = nil, value_column = nil, opts = OPTS)
          return super unless all = @cache[:subset_static_cache_all]

          if key_column.nil? && value_column.nil?
            if opts[:hash]
              key_column = model.primary_key
            else
              return Hash[@cache[:subset_static_cache_map]]
            end
          end

          h = opts[:hash] || {}
          if value_column
            if value_column.is_a?(Array)
              if key_column.is_a?(Array)
                all.each{|r| h[r.values.values_at(*key_column)] = r.values.values_at(*value_column)}
              else
                all.each{|r| h[r[key_column]] = r.values.values_at(*value_column)}
              end
            else
              if key_column.is_a?(Array)
                all.each{|r| h[r.values.values_at(*key_column)] = r[value_column]}
              else
                all.each{|r| h[r[key_column]] = r[value_column]}
              end
            end
          elsif key_column.is_a?(Array)
            all.each{|r| h[r.values.values_at(*key_column)] = r}
          else
            all.each{|r| h[r[key_column]] = r}
          end
          h
        end

        # Alias of as_hash for backwards compatibility.
        def to_hash(*a)
          as_hash(*a)
        end

        # Use the cache instead of a query to get the results
        def to_hash_groups(key_column, value_column = nil, opts = OPTS)
          return super unless all = @cache[:subset_static_cache_all]

          h = opts[:hash] || {}
          if value_column
            if value_column.is_a?(Array)
              if key_column.is_a?(Array)
                all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r.values.values_at(*value_column)}
              else
                all.each{|r| (h[r[key_column]] ||= []) << r.values.values_at(*value_column)}
              end
            else
              if key_column.is_a?(Array)
                all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r[value_column]}
              else
                all.each{|r| (h[r[key_column]] ||= []) << r[value_column]}
              end
            end
          elsif key_column.is_a?(Array)
            all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r}
          else
            all.each{|r| (h[r[key_column]] ||= []) << r}
          end
          h
        end
      end
    end
  end
end