Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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
Jeremy Evans authored
4 CHANGELOG
View
@@ -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)
5 lib/sequel/model.rb
View
@@ -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
76 lib/sequel/model/base.rb
View
@@ -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
30 lib/sequel/model/dataset_module.rb
View
@@ -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
11 spec/model/base_spec.rb
View
@@ -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.