Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Refactor how dataset methods are added to model datasets

Historically, the way dataset methods were added to datasets was
on a method-by-method basis.  The methods were stored in the
dataset_methods hash, keyed by method name, and each was
individually defined on the dataset's singleton class.

A while back, the dataset_module feature was added, so you
could just extend the dataset with a module, and it would
automatically have all of the module's methods, without having
to define the methods individually.

This commit removes the method-by-method implementation.  Now,
def_dataset_method calls dataset_module internally to define the
method inside the dataset module.

I've also decided to make things a little more consistent by
using a Module subclass, and allowing subset to be called inside
of a dataset_module block, for easier grouping of dataset methods
in one place.  Now Model.subset, just calls subset in the dataset
module.

Finally, adding public methods to a dataset module outside of the
block now adds the appropriate class methods, using a method_added
hook.

The only backwards incompatible change in this commit is the
removal of the Module#dataset_method attr_reader.
  • Loading branch information...
commit a897a828809cac95b11ec4903e946854a40aa6a9 1 parent e860e8f
@jeremyevans authored
View
4 CHANGELOG
@@ -1,5 +1,9 @@
=== HEAD
+* Remove Model.dataset_methods (jeremyevans)
+
+* Allow subset to be called inside a dataset_module block (jeremyevans)
+
* Make Dataset#avg, #interval, #min, #max, #range, and #sum accept virtual row blocks (jeremyevans)
* Make Dataset#count use a subselect when the dataset has an offset without a limit (jeremyevans) (#587)
View
5 lib/sequel/model.rb
@@ -106,7 +106,7 @@ class Model
# Class instance variables that are inherited in subclasses. If the value is <tt>:dup</tt>, dup is called
# on the superclass's instance variable when creating the instance variable in the subclass.
# If the value is +nil+, the superclass's instance variable is used directly in the subclass.
- INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup, :@dataset_methods=>:dup,
+ INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup,
:@dataset_method_modules=>:dup, :@primary_key=>nil, :@use_transactions=>nil,
:@raise_on_save_failure=>nil, :@require_modification=>nil,
:@restricted_columns=>:dup, :@restrict_primary_key=>nil,
@@ -129,7 +129,6 @@ class Model
@db = nil
@db_schema = nil
@dataset_method_modules = []
- @dataset_methods = {}
@overridable_methods_module = nil
@plugins = []
@primary_key = :id
@@ -147,7 +146,7 @@ class Model
@use_after_commit_rollback = true
@use_transactions = true
- Sequel.require %w"default_inflections inflections plugins base exceptions errors", "model"
+ Sequel.require %w"default_inflections inflections plugins dataset_module base exceptions errors", "model"
if !defined?(::SEQUEL_NO_ASSOCIATIONS) && !ENV.has_key?('SEQUEL_NO_ASSOCIATIONS')
Sequel.require 'associations', 'model'
plugin Model::Associations
View
76 lib/sequel/model/base.rb
@@ -24,11 +24,6 @@ module ClassMethods
# with all of these modules.
attr_reader :dataset_method_modules
- # Hash of dataset methods with method name keys and proc values that are
- # stored so when the dataset changes, methods defined with def_dataset_method
- # will be applied to the new dataset.
- attr_reader :dataset_methods
-
# SQL string fragment used for faster DELETE statement creation when deleting/destroying
# model instances, or nil if the optimization should not be used. For internal use only.
attr_reader :fast_instance_delete_sql
@@ -179,13 +174,19 @@ def dataset=(ds)
end
# Extend the dataset with a module, similar to adding
- # a plugin with the methods defined in DatasetMethods. If a block
- # is given, an anonymous module is created and the module_evaled, otherwise
- # the argument should be a module. Returns the module given or the anonymous
- # module created.
+ # a plugin with the methods defined in DatasetMethods.
+ # This is the recommended way to add methods to model datasets.
+ #
+ # If an argument, it should be a module, and is used to extend
+ # the underlying dataset. Otherwise an anonymous module is created, and
+ # if a block is given, it is module_evaled, allowing you do define
+ # dataset methods directly using the standard ruby def syntax.
+ # Returns the module given or the anonymous module created.
#
+ # # Usage with existing module
# Artist.dataset_module Sequel::ColumnsIntrospection
#
+ # # Usage with anonymous module
# Artist.dataset_module do
# def foo
# :bar
@@ -195,13 +196,24 @@ def dataset=(ds)
# # => :bar
# Artist.foo
# # => :bar
+ #
+ # Any anonymous modules created are actually instances of Sequel::Model::DatasetModule
+ # (a Module subclass), which allows you to call the subset method on them:
+ #
+ # Artist.dataset_module do
+ # subset :released, Sequel.identifier(release_date) > Sequel::CURRENT_DATE
+ # end
+ #
+ # Any public methods in the dataset module will have class methods created that
+ # call the method on the dataset, assuming that the class method is not already
+ # defined.
def dataset_module(mod = nil)
if mod
raise Error, "can't provide both argument and block to Model.dataset_module" if block_given?
dataset_extend(mod)
mod
else
- @dataset_module ||= Module.new
+ @dataset_module ||= DatasetModule.new(self)
@dataset_module.module_eval(&Proc.new) if block_given?
dataset_extend(@dataset_module)
@dataset_module
@@ -270,6 +282,10 @@ def def_column_alias(meth, column)
# If a block is not given, just define a class method on the model for each argument
# that calls the dataset method of the same argument name.
#
+ # It is recommended that you define methods inside a block passed to #dataset_module
+ # instead of using this method, as #dataset_module allows you to use normal
+ # ruby def syntax.
+ #
# # Add new dataset method and class method that calls it
# Artist.def_dataset_method(:by_name){order(:name)}
# Artist.filter(:name.like('A%')).by_name
@@ -280,18 +296,12 @@ def def_column_alias(meth, column)
# Artist.server!(:server1)
def def_dataset_method(*args, &block)
raise(Error, "No arguments given") if args.empty?
+
if block
raise(Error, "Defining a dataset method using a block requires only one argument") if args.length > 1
- meth = args.first
- @dataset_methods[meth] = block
- dataset.meta_def(meth, &block) if @dataset
- end
- args.each do |arg|
- if arg.to_s =~ NORMAL_METHOD_NAME_REGEXP
- instance_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__) unless respond_to?(arg, true)
- else
- def_model_dataset_method_block(arg)
- end
+ dataset_module{define_method(args.first, &block)}
+ else
+ args.each{|arg| def_model_dataset_method(arg)}
end
end
@@ -483,8 +493,7 @@ def set_allowed_columns(*cols)
# Returns self.
#
# This changes the row_proc of the dataset to return
- # model objects, extends the dataset with the dataset_method_modules,
- # and defines methods on the dataset using the dataset_methods.
+ # model objects and extends the dataset with the dataset_method_modules.
# It also attempts to determine the database schema for the model,
# based on the given dataset.
#
@@ -514,7 +523,6 @@ def set_dataset(ds, opts={})
@columns = @dataset.columns rescue nil
else
@dataset_method_modules.each{|m| @dataset.extend(m)} if @dataset_method_modules
- @dataset_methods.each{|meth, block| @dataset.meta_def(meth, &block)} if @dataset_methods
end
@dataset.model = self if @dataset.respond_to?(:model=)
check_non_connection_error{@db_schema = (inherited ? superclass.db_schema : get_db_schema)}
@@ -577,8 +585,8 @@ def setter_methods
end
end
- # Shortcut for +def_dataset_method+ that is restricted to modifying the
- # dataset's filter. Sometimes thought of as a scope, and like most dataset methods,
+ # Sets up a dataset method that returns a filtered dataset.
+ # Sometimes thought of as a scope, and like most dataset methods,
# they can be chained.
# For example:
#
@@ -597,9 +605,10 @@ def setter_methods
# Both the args given and the block are passed to <tt>Dataset#filter</tt>.
#
# This method creates dataset methods that do not accept arguments. To create
- # dataset methods that accept arguments, you have to use def_dataset_method.
+ # dataset methods that accept arguments, you should use define a
+ # method directly inside a #dataset_module block.
def subset(name, *args, &block)
- def_dataset_method(name){filter(*args, &block)}
+ dataset_module.subset(name, *args, &block)
end
# Returns name of primary table for the dataset. If the table for the dataset
@@ -642,8 +651,7 @@ def check_non_connection_error
def dataset_extend(mod)
dataset.extend(mod) if @dataset
dataset_method_modules << mod
- meths = mod.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s}
- def_dataset_method(*meths) unless meths.empty?
+ mod.public_instance_methods.each{|meth| def_model_dataset_method(meth)}
end
# Create a column accessor for a column with a method name that is hard to use in ruby code.
@@ -671,8 +679,14 @@ def def_column_accessor(*columns)
# Define a model method that calls the dataset method with the same name,
# only used for methods with names that can't be presented directly in
# ruby code.
- def def_model_dataset_method_block(arg)
- meta_def(arg){|*args, &block| dataset.send(arg, *args, &block)}
+ def def_model_dataset_method(meth)
+ return if respond_to?(meth, true)
+
+ if meth.to_s =~ NORMAL_METHOD_NAME_REGEXP
+ instance_eval("def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end", __FILE__, __LINE__)
+ else
+ meta_def(meth){|*args, &block| dataset.send(meth, *args, &block)}
+ end
end
# Get the schema from the database, fall back on checking the columns
View
30 lib/sequel/model/dataset_module.rb
@@ -0,0 +1,30 @@
+module Sequel
+ class Model
+ # This Module subclass is used by Model.dataset_module
+ # to add dataset methods to classes. It adds a couple
+ # of features standard Modules, allowing you to use
+ # the same subset method you can call on Model, as well
+ # as making sure that public methods added to the module
+ # automatically have class methods created for them.
+ class DatasetModule < ::Module
+ # Store the model related to this dataset module.
+ def initialize(model)
+ @model = model
+ end
+
+ # Define a named filter for this dataset, see
+ # Model.subset for details.
+ def subset(name, *args, &block)
+ define_method(name){filter(*args, &block)}
+ end
+
+ private
+
+ # Add a class method to the related model that
+ # calls the dataset method of the same name.
+ def method_added(meth)
+ @model.send(:def_model_dataset_method, meth)
+ end
+ end
+ end
+end
View
11 spec/model/base_spec.rb
@@ -187,6 +187,11 @@ def return_4; 4; end
@c.return_3.should == 3
end
+ it "should add methods defined in the module outside the block to the class" do
+ @c.dataset_module.module_eval{def return_3() 3 end}
+ @c.return_3.should == 3
+ end
+
it "should cache calls and readd methods if set_dataset is used" do
@c.dataset_module{def return_3() 3 end}
@c.set_dataset :items
@@ -251,6 +256,12 @@ def return_4; 4; end
Object.new.extend(@c.dataset_module Module.new{def return_3() 3 end}).return_3.should == 3
end
+ it "should have dataset_module support a subset method" do
+ @c.dataset_module{subset :released, :released}
+ @c.released.sql.should == 'SELECT * FROM items WHERE released'
+ @c.where(:foo).released.sql.should == 'SELECT * FROM items WHERE (foo AND released)'
+ end
+
it "should raise error if called with both an argument and ablock" do
proc{@c.dataset_module(Module.new{def return_3() 3 end}){}}.should raise_error(Sequel::Error)
end
Please sign in to comment.
Something went wrong with that request. Please try again.