|
| 1 | +# frozen-string-literal: true |
| 2 | + |
| 3 | +module Sequel |
| 4 | + module Plugins |
| 5 | + # The subset_static_cache plugin is designed for model subsets that are not modified at all |
| 6 | + # in production use cases, or at least where modifications to them would usually |
| 7 | + # coincide with an application restart. When caching a model subset, it |
| 8 | + # retrieves all rows in the database and statically caches a ruby array and hash |
| 9 | + # keyed on primary key containing all of the model instances. All of these cached |
| 10 | + # instances are frozen so they won't be modified unexpectedly. |
| 11 | + # |
| 12 | + # With the following code: |
| 13 | + # |
| 14 | + # class StatusType < Sequel::Model |
| 15 | + # dataset_module do |
| 16 | + # where :available, hidden: false |
| 17 | + # end |
| 18 | + # cache_subset :available |
| 19 | + # end |
| 20 | + # |
| 21 | + # The following methods will use the cache and not issue a database query: |
| 22 | + # |
| 23 | + # * StatusType.available.with_pk |
| 24 | + # * StatusType.available.all |
| 25 | + # * StatusType.available.each |
| 26 | + # * StatusType.available.first (without block, only supporting no arguments or single integer argument) |
| 27 | + # * StatusType.available.count (without an argument or block) |
| 28 | + # * StatusType.available.map |
| 29 | + # * StatusType.available.as_hash |
| 30 | + # * StatusType.available.to_hash |
| 31 | + # * StatusType.available.to_hash_groups |
| 32 | + # |
| 33 | + # The cache is not used if you chain methods before or after calling the cached |
| 34 | + # method, as doing so would not be safe: |
| 35 | + # |
| 36 | + # StatusType.where{number > 1}.available.all |
| 37 | + # StatusType.available.where{number > 1}.all |
| 38 | + # |
| 39 | + # The cache is also not used if you change the class's dataset after caching |
| 40 | + # the subset, or in subclasses of the model. |
| 41 | + # |
| 42 | + # You should not modify any row that is statically cached when using this plugin, |
| 43 | + # as otherwise you will get different results for cached and uncached method |
| 44 | + # calls. |
| 45 | + module SubsetStaticCache |
| 46 | + def self.configure(model) |
| 47 | + model.class_exec do |
| 48 | + @subset_static_caches ||= ({}.compare_by_identity) |
| 49 | + end |
| 50 | + end |
| 51 | + |
| 52 | + module ClassMethods |
| 53 | + # Cache the given subset statically, so that calling the subset method on |
| 54 | + # the model will return a dataset that will return cached results instead |
| 55 | + # of issuing database queries (assuming the cache has the necessary |
| 56 | + # information). |
| 57 | + # |
| 58 | + # The model must already respond to the given method before cache_subset |
| 59 | + # is called. |
| 60 | + def cache_subset(meth) |
| 61 | + ds = send(meth).with_extend(CachedDatasetMethods) |
| 62 | + cache = ds.instance_variable_get(:@cache) |
| 63 | + |
| 64 | + rows, hash = subset_static_cache_rows(ds, meth) |
| 65 | + cache[:subset_static_cache_all] = rows |
| 66 | + cache[:subset_static_cache_map] = hash |
| 67 | + |
| 68 | + caches = @subset_static_caches |
| 69 | + caches[meth] = ds |
| 70 | + model = self |
| 71 | + subset_static_cache_module.send(:define_method, meth) do |
| 72 | + if (model == self) && (cached_dataset = caches[meth]) |
| 73 | + cached_dataset |
| 74 | + else |
| 75 | + super() |
| 76 | + end |
| 77 | + end |
| 78 | + nil |
| 79 | + end |
| 80 | + |
| 81 | + Plugins.after_set_dataset(self, :clear_subset_static_caches) |
| 82 | + Plugins.inherited_instance_variables(self, :@subset_static_caches=>proc{{}.compare_by_identity}) |
| 83 | + |
| 84 | + private |
| 85 | + |
| 86 | + # Clear the subset_static_caches. This is used if the model dataset |
| 87 | + # changes, to prevent cached values from being used. |
| 88 | + def clear_subset_static_caches |
| 89 | + @subset_static_caches.clear |
| 90 | + end |
| 91 | + |
| 92 | + # A module for the subset static cache methods, so that you can define |
| 93 | + # a singleton method in the class with the same name, and call super |
| 94 | + # to get default behavior. |
| 95 | + def subset_static_cache_module |
| 96 | + return @subset_static_cache_module if @subset_static_cache_module |
| 97 | + |
| 98 | + # Ensure dataset_methods module is defined and class is extended with |
| 99 | + # it before calling creating this module. |
| 100 | + dataset_methods_module |
| 101 | + |
| 102 | + Sequel.synchronize{@subset_static_cache_module ||= Module.new} |
| 103 | + extend(@subset_static_cache_module) |
| 104 | + @subset_static_cache_module |
| 105 | + end |
| 106 | + |
| 107 | + # Return the frozen array and hash used for caching the subset |
| 108 | + # of the given dataset. |
| 109 | + def subset_static_cache_rows(ds, meth) |
| 110 | + all = load_subset_static_cache_rows(ds, meth) |
| 111 | + h = {} |
| 112 | + all.each do |o| |
| 113 | + o.errors.freeze |
| 114 | + h[o.pk.freeze] = o.freeze |
| 115 | + end |
| 116 | + [all, h.freeze] |
| 117 | + end |
| 118 | + |
| 119 | + # Return a frozen array for all rows in the dataset. |
| 120 | + def load_subset_static_cache_rows(ds, meth) |
| 121 | + ret = super if defined?(super) |
| 122 | + ret || ds.all.freeze |
| 123 | + end |
| 124 | + end |
| 125 | + |
| 126 | + module CachedDatasetMethods |
| 127 | + # An array of all of the dataset's instances, without issuing a database |
| 128 | + # query. If a block is given, yields each instance to the block. |
| 129 | + def all(&block) |
| 130 | + return super unless all = @cache[:subset_static_cache_all] |
| 131 | + |
| 132 | + array = all.dup |
| 133 | + array.each(&block) if block |
| 134 | + array |
| 135 | + end |
| 136 | + |
| 137 | + # Get the number of records in the cache, without issuing a database query, |
| 138 | + # if no arguments or block are provided. |
| 139 | + def count(*a, &block) |
| 140 | + if a.empty? && !block && (all = @cache[:subset_static_cache_all]) |
| 141 | + all.size |
| 142 | + else |
| 143 | + super |
| 144 | + end |
| 145 | + end |
| 146 | + |
| 147 | + # If a block is given, multiple arguments are given, or a single |
| 148 | + # non-Integer argument is given, performs the default behavior of |
| 149 | + # issuing a database query. Otherwise, uses the cached values |
| 150 | + # to return either the first cached instance (no arguments) or an |
| 151 | + # array containing the number of instances specified (single integer |
| 152 | + # argument). |
| 153 | + def first(*args) |
| 154 | + if !defined?(yield) && args.length <= 1 && (args.length == 0 || args[0].is_a?(Integer)) && (all = @cache[:subset_static_cache_all]) |
| 155 | + all.first(*args) |
| 156 | + else |
| 157 | + super |
| 158 | + end |
| 159 | + end |
| 160 | + |
| 161 | + # Return the frozen object with the given pk, or nil if no such object exists |
| 162 | + # in the cache, without issuing a database query. |
| 163 | + def with_pk(pk) |
| 164 | + if cache = @cache[:subset_static_cache_map] |
| 165 | + cache[pk] |
| 166 | + else |
| 167 | + super |
| 168 | + end |
| 169 | + end |
| 170 | + |
| 171 | + # Yield each of the dataset's frozen instances to the block, without issuing a database |
| 172 | + # query. |
| 173 | + def each(&block) |
| 174 | + return super unless all = @cache[:subset_static_cache_all] |
| 175 | + all.each(&block) |
| 176 | + end |
| 177 | + |
| 178 | + # Use the cache instead of a query to get the results. |
| 179 | + def map(column=nil, &block) |
| 180 | + return super unless all = @cache[:subset_static_cache_all] |
| 181 | + if column |
| 182 | + raise(Error, "Cannot provide both column and block to map") if block |
| 183 | + if column.is_a?(Array) |
| 184 | + all.map{|r| r.values.values_at(*column)} |
| 185 | + else |
| 186 | + all.map{|r| r[column]} |
| 187 | + end |
| 188 | + else |
| 189 | + all.map(&block) |
| 190 | + end |
| 191 | + end |
| 192 | + |
| 193 | + # Use the cache instead of a query to get the results if possible |
| 194 | + def as_hash(key_column = nil, value_column = nil, opts = OPTS) |
| 195 | + return super unless all = @cache[:subset_static_cache_all] |
| 196 | + |
| 197 | + if key_column.nil? && value_column.nil? |
| 198 | + if opts[:hash] |
| 199 | + key_column = model.primary_key |
| 200 | + else |
| 201 | + return Hash[@cache[:subset_static_cache_map]] |
| 202 | + end |
| 203 | + end |
| 204 | + |
| 205 | + h = opts[:hash] || {} |
| 206 | + if value_column |
| 207 | + if value_column.is_a?(Array) |
| 208 | + if key_column.is_a?(Array) |
| 209 | + all.each{|r| h[r.values.values_at(*key_column)] = r.values.values_at(*value_column)} |
| 210 | + else |
| 211 | + all.each{|r| h[r[key_column]] = r.values.values_at(*value_column)} |
| 212 | + end |
| 213 | + else |
| 214 | + if key_column.is_a?(Array) |
| 215 | + all.each{|r| h[r.values.values_at(*key_column)] = r[value_column]} |
| 216 | + else |
| 217 | + all.each{|r| h[r[key_column]] = r[value_column]} |
| 218 | + end |
| 219 | + end |
| 220 | + elsif key_column.is_a?(Array) |
| 221 | + all.each{|r| h[r.values.values_at(*key_column)] = r} |
| 222 | + else |
| 223 | + all.each{|r| h[r[key_column]] = r} |
| 224 | + end |
| 225 | + h |
| 226 | + end |
| 227 | + |
| 228 | + # Alias of as_hash for backwards compatibility. |
| 229 | + def to_hash(*a) |
| 230 | + as_hash(*a) |
| 231 | + end |
| 232 | + |
| 233 | + # Use the cache instead of a query to get the results |
| 234 | + def to_hash_groups(key_column, value_column = nil, opts = OPTS) |
| 235 | + return super unless all = @cache[:subset_static_cache_all] |
| 236 | + |
| 237 | + h = opts[:hash] || {} |
| 238 | + if value_column |
| 239 | + if value_column.is_a?(Array) |
| 240 | + if key_column.is_a?(Array) |
| 241 | + all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r.values.values_at(*value_column)} |
| 242 | + else |
| 243 | + all.each{|r| (h[r[key_column]] ||= []) << r.values.values_at(*value_column)} |
| 244 | + end |
| 245 | + else |
| 246 | + if key_column.is_a?(Array) |
| 247 | + all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r[value_column]} |
| 248 | + else |
| 249 | + all.each{|r| (h[r[key_column]] ||= []) << r[value_column]} |
| 250 | + end |
| 251 | + end |
| 252 | + elsif key_column.is_a?(Array) |
| 253 | + all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r} |
| 254 | + else |
| 255 | + all.each{|r| (h[r[key_column]] ||= []) << r} |
| 256 | + end |
| 257 | + h |
| 258 | + end |
| 259 | + end |
| 260 | + end |
| 261 | + end |
| 262 | +end |
0 commit comments