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.
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.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.send(meth, *args)
ds = ds.limit(1) if limit1
ds
end
Sequel::Dataset::PlaceholderLiteralizer.loader(model, &block)
end
end
Sequel.synchronize{@finder_loaders[meth_name] = loader_proc}
mod = opts[:mod] || (class << self; self; end)
if prepare
def_prepare_method(mod, meth_name)
else
def_finder_method(mod, meth_name, type)
end
end