Skip to content

Commit

Permalink
Move caching, hook class methods, and STI into plugins, deprecate old…
Browse files Browse the repository at this point in the history
… methods of using them

This large commit was necessary because caching and STI both depended
on the hook class methods.

This commit refactors the plugin support to try loading plugins from
sequel/plugins/plugin_name before sequel_plugin_name.  It also makes
the plugins code use Model::ClassMethods.

This commit moves Sequel::Model constants and instance variables to
sequel_model.rb, so they will be loaded before other files.

This commit refactors Model. inherited because
subclass.superclass == self, and to remove the hook class method
and STI stuff.

This commit removes the PRIVATE_HOOKS, before_update_values and
before_delete.  They are not needed as with the recent commits, you
can just override update_values and delete in the InstanceMethods
plugin submodule, do the hook stuff and then call super.

Finally, this commit adds a spec_plugin rake task.  It's not
possible to run the specs for the plugins/extensions in the same
task as the model/core specs, because the plugin specs may modify
the workings of model/core, and those changes can't be a factor
when running the model/core specs.  All plugins and extensions are
loaded and tested simultaneously, in order to make sure they all
work together (currently fairly easy, as they all were available
by default).
  • Loading branch information
jeremyevans committed Mar 8, 2009
1 parent d8bb2d6 commit 6f31723
Show file tree
Hide file tree
Showing 23 changed files with 1,423 additions and 384 deletions.
8 changes: 8 additions & 0 deletions Rakefile
Expand Up @@ -98,6 +98,7 @@ lib_dir = File.join(File.dirname(__FILE__), 'lib')
fixRUBYLIB = Proc.new{ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)}
sequel_core_specs = "spec/sequel_core/*_spec.rb"
sequel_model_specs = "spec/sequel_model/*_spec.rb"
sequel_plugin_specs = "spec/extensions/*_spec.rb"
spec_opts = proc{File.read("spec/spec.opts").split("\n")}
rcov_opts = proc{File.read("spec/rcov.opts").split("\n")}

Expand Down Expand Up @@ -132,6 +133,13 @@ Spec::Rake::SpecTask.new("spec_model") do |t|
t.spec_opts = spec_opts.call
end

desc "Run extension/plugin specs"
Spec::Rake::SpecTask.new("spec_plugin") do |t|
fixRUBYLIB.call
t.spec_files = FileList[sequel_plugin_specs]
t.spec_opts = spec_opts.call
end

desc "Run integration tests"
Spec::Rake::SpecTask.new("integration") do |t|
fixRUBYLIB.call
Expand Down
121 changes: 121 additions & 0 deletions lib/sequel/plugins/caching.rb
@@ -0,0 +1,121 @@
module Sequel
module Plugins
# Sequel's built-in caching plugin supports caching to any object that
# implements the Ruby-Memcache API. You can add caching for any model
# or for all models via:
#
# Model.plugin :caching, store # Cache all models
# MyModel.plugin :caching, store # Just cache MyModel
#
# The cache store should implement the Ruby-Memcache API:
#
# cache_store.set(key, obj, time) # Associate the obj with the given key
# # in the cache for the time (specified
# # in seconds)
# cache_store.get(key) => obj # Returns object set with same key
# cache_store.get(key2) => nil # nil returned if there isn't an object
# # currently in the cache with that key
module Caching
# Set the cache_store and cache_ttl attributes for the given model.
# If the :ttl option is not given, 3600 seconds is the default.
def self.apply(model, store, opts={})
model.instance_eval do
@cache_store = store
@cache_ttl = opts[:ttl] || 3600
end
end

module ClassMethods
# The cache store object for the model, which should implement the
# Ruby-Memcache API
attr_reader :cache_store

# The time to live for the cache store, in seconds.
attr_reader :cache_ttl

# Set the time to live for the cache store, in seconds (default is 3600, # so 1 hour).
def set_cache_ttl(ttl)
@cache_ttl = ttl
end

# Copy the cache_store and cache_ttl to the subclass.
def inherited(subclass)
super
store = @cache_store
ttl = @cache_ttl
subclass.instance_eval do
@cache_store = store
@cache_ttl = ttl
end
end

private

# Delete the entry with the matching key from the cache
def cache_delete(key)
@cache_store.delete(key)
nil
end

# Return a key string for the pk
def cache_key(pk)
"#{self}:#{Array(pk).join(',')}"
end

# Lookup the primary key in the cache.
# If found, return the matching object.
# Otherwise, get the matching object from the database and
# update the cache with it.
def cache_lookup(pk)
ck = cache_key(pk)
unless obj = @cache_store.get(ck)
obj = dataset[primary_key_hash(pk)]
@cache_store.set(ck, obj, @cache_ttl)
end
obj
end
end

module InstanceMethods
# Remove the object from the cache when updating
def before_update
return false if super == false
cache_delete
end

# Return a key unique to the underlying record for caching, based on the
# primary key value(s) for the object. If the model does not have a primary
# key, raise an Error.
def cache_key
raise(Error, "No primary key is associated with this model") unless key = primary_key
pk = case key
when Array
key.collect{|k| @values[k]}
else
@values[key] || (raise Error, 'no primary key for this record')
end
model.send(:cache_key, pk)
end

# Remove the object from the cache when deleting
def delete
cache_delete
super
end

# Remove the object from the cache when updating
def update_values(*args)
cache_delete
super
end

private

# Delete this object from the cache
def cache_delete
model.send(:cache_delete, cache_key)
end
end
end
end
end
17 changes: 17 additions & 0 deletions lib/sequel/plugins/hook_class_methods.rb
@@ -0,0 +1,17 @@
module Sequel
module Plugins
module HookClassMethods
module ClassMethods
Model::HOOKS.each{|h| class_eval("def #{h}(method = nil, &block); add_hook(:#{h}, method, &block) end", __FILE__, __LINE__)}

def add_hook_type(*hooks)
hooks.each do |hook|
@hooks[hook] = []
instance_eval("def #{hook}(method = nil, &block); add_hook(:#{hook}, method, &block) end", __FILE__, __LINE__)
class_eval("def #{hook}; run_hooks(:#{hook}); end", __FILE__, __LINE__)
end
end
end
end
end
end
62 changes: 62 additions & 0 deletions lib/sequel/plugins/single_table_inheritance.rb
@@ -0,0 +1,62 @@
module Sequel
module Plugins
# Sequel's built in Single Table Inheritance plugin makes subclasses
# of this model only load rows where the given key field matches the
# subclass's name. If the key given has a NULL value or there are
# any problems looking up the class, uses the current class.
#
# You should only use this in the parent class, not in the subclasses.
#
# You shouldn't call set_dataset in the model after applying this
# plugin, otherwise subclasses might use the wrong dataset.
#
# The filters and row_proc that sti_key sets up in subclasses may not work correctly if
# those subclasses have further subclasses. For those middle subclasses,
# you may need to call set_dataset manually with the correct filter and
# row_proc.
module SingleTableInheritance
# Set the sti_key and sti_dataset for the model, and change the
# dataset's row_proc so that the dataset yields objects of varying classes,
# where the class used has the same name as the key field.
def self.apply(model, key)
model.instance_eval do
@sti_key = key
@sti_dataset = dataset
dataset.row_proc = Proc.new{|r| (r[key].constantize rescue model).load(r)}
end
end

module ClassMethods
# The base dataset for STI, to which filters are added to get
# only the models for the specific STI subclass.
attr_reader :sti_dataset

# The column name holding the STI key for this model
attr_reader :sti_key

# Copy the sti_key and sti_dataset to the subclasses, and filter the
# subclass's dataset so it is restricted to rows where the key column
# matches the subclass's name.
def inherited(subclass)
super
sk = sti_key
sd = sti_dataset
subclass.set_dataset(sd.filter(sk=>subclass.name.to_s), :inherited=>true)
subclass.instance_eval do
@sti_key = sk
@sti_dataset = sd
@simple_table = nil
end
end
end

module InstanceMethods
# Set the sti_key column to the name of the model.
def before_create
return false if super == false
send("#{model.sti_key}=", model.name.to_s)
end
end
end
end
end
58 changes: 53 additions & 5 deletions lib/sequel_model.rb
Expand Up @@ -43,9 +43,8 @@ def self.Model(source)
# * The following instance_methods all call the class method of the same
# name: columns, dataset, db, primary_key, db_schema.
# * The following class level attr_readers are created: allowed_columns,
# cache_store, cache_ttl, dataset_methods, primary_key, restricted_columns,
# sti_dataset, and sti_key. You should not usually need to
# access these directly.
# dataset_methods, primary_key, and restricted_columns.
# You should not usually need to access these directly.
# * All validation methods also accept the options specified in #validates_each,
# in addition to the options specified in the RDoc for that method.
# * The following class level attr_accessors are created: raise_on_typecast_failure,
Expand Down Expand Up @@ -81,11 +80,60 @@ def self.Model(source)
# * Model.dataset= => set_dataset
# * Model.is_a => is
class Model
# Dataset methods to proxy via metaprogramming
DATASET_METHODS = %w'<< all avg count delete distinct eager eager_graph each each_page
empty? except exclude filter first from from_self full_outer_join get graph
group group_and_count group_by having inner_join insert
insert_multiple intersect interval join join_table last
left_outer_join limit map multi_insert naked order order_by order_more
paginate print query range reverse_order right_outer_join select
select_all select_more server set set_graph_aliases single_value to_csv to_hash
transform union unfiltered unordered update where with_sql'.map{|x| x.to_sym}

# Empty instance variables, for -w compliance
EMPTY_INSTANCE_VARIABLES = [:@overridable_methods_module, :@transform, :@db, :@skip_superclass_validations]

# Hooks that are safe for public use
HOOKS = [:after_initialize, :before_create, :after_create, :before_update,
:after_update, :before_save, :after_save, :before_destroy, :after_destroy,
:before_validation, :after_validation]

# Instance variables that are inherited in subclasses
INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup, :@dataset_methods=>:dup, :@primary_key=>nil,
:@raise_on_save_failure=>nil, :@restricted_columns=>:dup, :@restrict_primary_key=>nil,
:@simple_pk=>nil, :@simple_table=>nil, :@strict_param_setting=>nil,
:@typecast_empty_string_to_nil=>nil, :@typecast_on_assignment=>nil,
:@raise_on_typecast_failure=>nil, :@association_reflections=>:dup}

# The setter methods (methods ending with =) that are never allowed
# to be called automatically via set.
RESTRICTED_SETTER_METHODS = %w"== === []= taguri= typecast_empty_string_to_nil= typecast_on_assignment= strict_param_setting= raise_on_save_failure= raise_on_typecast_failure="

@allowed_columns = nil
@association_reflections = {}
@cache_store = nil
@cache_ttl = nil
@db = nil
@db_schema = nil
@dataset_methods = {}
@overridable_methods_module = nil
@primary_key = :id
@raise_on_save_failure = true
@raise_on_typecast_failure = true
@restrict_primary_key = true
@restricted_columns = nil
@simple_pk = nil
@simple_table = nil
@skip_superclass_validations = nil
@strict_param_setting = true
@transform = nil
@typecast_empty_string_to_nil = true
@typecast_on_assignment = true
end
end

%w"inflector record association_reflection associations base hooks schema dataset_methods
caching plugins validations eager_loading exceptions deprecated".each do |f|
%w"inflector plugins record association_reflection associations base hooks schema dataset_methods
caching validations eager_loading exceptions deprecated".each do |f|
require "sequel_model/#{f}"
end

0 comments on commit 6f31723

Please sign in to comment.