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
|
# frozen-string-literal: true
module Sequel
module Plugins
# The finder plugin adds Model.finder for defining optimized finder methods.
# There are two ways to use this. The recommended way is to pass a symbol
# that represents a model class method that returns a dataset:
#
# def Artist.by_name(name)
# where(name: name)
# end
#
# Artist.finder :by_name
#
# This creates an optimized first_by_name method, which you can call normally:
#
# Artist.first_by_name("Joe")
#
# The alternative way to use this to pass your own block:
#
# Artist.finder(name: :first_by_name){|pl, ds| ds.where(name: pl.arg).limit(1)}
#
# Additionally, there is a Model.prepared_finder method. This works similarly
# to Model.finder, but uses a prepared statement. This limits the types of
# arguments that will be accepted, but can perform better in the database.
#
# Usage:
#
# # Make all model subclasses support Model.finder
# # (called before loading subclasses)
# Sequel::Model.plugin :finder
#
# # Make the Album class support Model.finder
# Album.plugin :finder
module Finder
FINDER_TYPES = [:first, :all, :each, :get].freeze
def self.apply(model)
model.instance_exec do
@finders ||= {}
@finder_loaders ||= {}
end
end
module ClassMethods
# Create an optimized finder method using a dataset placeholder literalizer.
# This pre-computes the SQL to use for the query, except for given arguments.
#
# There are two ways to use this. The recommended way is to pass a symbol
# that represents a model class method that returns a dataset:
#
# def Artist.by_name(name)
# where(name: name)
# end
#
# Artist.finder :by_name
#
# This creates an optimized first_by_name method, which you can call normally:
#
# Artist.first_by_name("Joe")
#
# The alternative way to use this to pass your own block:
#
# Artist.finder(name: :first_by_name){|pl, ds| ds.where(name: pl.arg).limit(1)}
#
# Note that if you pass your own block, you are responsible for manually setting
# limits if necessary (as shown above).
#
# Options:
# :arity :: When using a symbol method name, this specifies the arity of the method.
# This should be used if if the method accepts an arbitrary number of arguments,
# or the method has default argument values. Note that if the method is defined
# as a dataset method, the class method Sequel creates accepts an arbitrary number
# of arguments, so you should use this option in that case. If you want to handle
# multiple possible arities, you need to call the finder method multiple times with
# unique :arity and :name methods each time.
# :name :: The name of the method to create. This must be given if you pass a block.
# If you use a symbol, this defaults to the symbol prefixed by the type.
# :mod :: The module in which to create the finder method. Defaults to the singleton
# class of the model.
# :type :: The type of query to run. Can be :first, :each, :all, or :get, defaults to
# :first.
#
# Caveats:
#
# This doesn't handle all possible cases. For example, if you have a method such as:
#
# def Artist.by_name(name)
# name ? where(name: name) : exclude(name: nil)
# end
#
# Then calling a finder without an argument will not work as you expect.
#
# Artist.finder :by_name
# Artist.by_name(nil).first
# # WHERE (name IS NOT NULL)
# Artist.first_by_name(nil)
# # WHERE (name IS NULL)
#
# See Dataset::PlaceholderLiteralizer for additional caveats. Note that if the model's
# dataset does not support placeholder literalizers, you will not be able to use this
# method.
def finder(meth=OPTS, opts=OPTS, &block)
if block
raise Error, "cannot pass both a method name argument and a block of Model.finder" unless meth.is_a?(Hash)
raise Error, "cannot pass two option hashes to Model.finder" unless opts.equal?(OPTS)
opts = meth
raise Error, "must provide method name via :name option when passing block to Model.finder" unless meth_name = opts[:name]
end
type = opts.fetch(:type, :first)
unless prepare = opts[:prepare]
raise Error, ":type option to Model.finder must be :first, :all, :each, or :get" unless FINDER_TYPES.include?(type)
end
limit1 = type == :first || type == :get
meth_name ||= opts[:name] || :"#{type}_#{meth}"
argn = lambda do |model|
if arity = opts[:arity]
arity
else
method = block || model.method(meth)
(method.arity < 0 ? method.arity.abs - 1 : method.arity)
end
end
loader_proc = if prepare
proc do |model|
args = prepare_method_args('$a', argn.call(model))
ds = if block
model.instance_exec(*args, &block)
else
model.public_send(meth, *args)
end
ds = ds.limit(1) if limit1
model_name = model.name
if model_name.to_s.empty?
model_name = model.object_id
else
model_name = model_name.gsub(/\W/, '_')
end
ds.prepare(type, :"#{model_name}_#{meth_name}")
end
else
proc do |model|
n = argn.call(model)
block ||= lambda do |pl, model2|
args = (0...n).map{pl.arg}
ds = model2.public_send(meth, *args)
ds = ds.limit(1) if limit1
ds
end
Sequel::Dataset::PlaceholderLiteralizer.loader(model, &block)
end
end
@finder_loaders[meth_name] = loader_proc
mod = opts[:mod] || singleton_class
if prepare
def_prepare_method(mod, meth_name)
else
def_finder_method(mod, meth_name, type)
end
end
def freeze
@finder_loaders.freeze
@finder_loaders.each_key{|k| finder_for(k)} if @dataset
@finders.freeze
super
end
# Similar to finder, but uses a prepared statement instead of a placeholder
# literalizer. This makes the SQL used static (cannot vary per call), but
# allows binding argument values instead of literalizing them into the SQL
# query string.
#
# If a block is used with this method, it is instance_execed by the model,
# and should accept the desired number of placeholder arguments.
#
# The options are the same as the options for finder, with the following
# exception:
# :type :: Specifies the type of prepared statement to create
def prepared_finder(meth=OPTS, opts=OPTS, &block)
if block
raise Error, "cannot pass both a method name argument and a block of Model.finder" unless meth.is_a?(Hash)
meth = meth.merge(:prepare=>true)
else
opts = opts.merge(:prepare=>true)
end
finder(meth, opts, &block)
end
Plugins.inherited_instance_variables(self, :@finders=>:dup, :@finder_loaders=>:dup)
private
# Define a finder method in the given module with the given method name that
# load rows using the finder with the given name.
def def_finder_method(mod, meth, type)
mod.send(:define_method, meth){|*args, &block| finder_for(meth).public_send(type, *args, &block)}
end
# Define a prepared_finder method in the given module that will call the associated prepared
# statement.
def def_prepare_method(mod, meth)
mod.send(:define_method, meth){|*args, &block| finder_for(meth).call(prepare_method_arg_hash(args), &block)}
end
# Find the finder to use for the give method. If a finder has not been loaded
# for the method, load the finder and set correctly in the finders hash, then
# return the finder.
def finder_for(meth)
unless finder = (frozen? ? @finders[meth] : Sequel.synchronize{@finders[meth]})
finder_loader = @finder_loaders.fetch(meth)
finder = finder_loader.call(self)
Sequel.synchronize{@finders[meth] = finder}
end
finder
end
# An hash of prepared argument values for the given arguments, with keys
# starting at a. Used by the methods created by prepared_finder.
def prepare_method_arg_hash(args)
h = {}
prepare_method_args('a', args.length).zip(args).each{|k, v| h[k] = v}
h
end
# An array of prepared statement argument names, of length n and starting with base.
def prepare_method_args(base, n)
(0...n).map do
s = base.to_sym
base = base.next
s
end
end
# Clear any finders when reseting the instance dataset
def reset_instance_dataset
Sequel.synchronize{@finders.clear} if @finders && !@finders.frozen?
super
end
end
end
end
end
|