Refactoring design doc manipulation for a much simpler and more reliable approach

This commit is contained in:
Sam Lown 2011-04-17 02:46:33 +02:00
parent 2eed3581af
commit 5805f6e27b
19 changed files with 285 additions and 195 deletions

View file

@ -1,7 +1,13 @@
module CouchRest
module Model
# Warning! The Collection module is seriously depricated.
# Use the new Design Views instead, as this code copies many other parts
# of CouchRest Model.
#
# Expect this to be removed soon.
#
module Collection
def self.included(base)
base.extend(ClassMethods)
end
@ -131,6 +137,9 @@ module CouchRest
else
@view_name = "#{design_doc}/#{view_name}"
end
# Save the design doc, ready for use
@container_class.save_design_doc(@database)
end
# See Collection.paginate

View file

@ -10,10 +10,12 @@ module CouchRest
included do
add_config :model_type_key
add_config :mass_assign_any_attribute
add_config :auto_update_design_doc
configure do |config|
config.model_type_key = 'couchrest-type' # 'model'?
config.mass_assign_any_attribute = false
config.auto_update_design_doc = true
end
end

View file

@ -3,19 +3,13 @@ module CouchRest
module Model
module DesignDoc
extend ActiveSupport::Concern
module ClassMethods
def design_doc
@design_doc ||= ::CouchRest::Design.new(default_design_doc)
end
# Use when something has been changed, like a view, so that on the next request
# the design docs will be updated (if changed!)
def req_design_doc_refresh
@design_doc_fresh = { }
end
def design_doc_id
"_design/#{design_doc_slug}"
end
@ -24,6 +18,80 @@ module CouchRest
self.to_s
end
def design_doc_full_url(db = database)
"#{db.uri}/#{design_doc_id}"
end
# Retreive the latest version of the design document directly
# from the database. This is never cached and will return nil if
# the design is not present.
#
# Use this method if you'd like to compare revisions [_rev] which
# is not stored in the normal design doc.
def stored_design_doc(db = database)
db.get(design_doc_id)
rescue RestClient::ResourceNotFound
nil
end
# Save the design doc onto a target database in a thread-safe way,
# not modifying the model's design_doc
#
# See also save_design_doc! to always save the design doc even if there
# are no changes.
def save_design_doc(db = database, force = false)
update_design_doc(db, force)
end
# Force the update of the model's design_doc even if it hasn't changed.
def save_design_doc!(db = database)
save_design_doc(db, true)
end
private
def design_doc_cache
Thread.current[:couchrest_design_cache] ||= {}
end
def design_doc_cache_checksum(db)
design_doc_cache[design_doc_full_url(db)]
end
def set_design_doc_cache_checksum(db, checksum)
design_doc_cache[design_doc_full_url(db)] = checksum
end
# Writes out a design_doc to a given database if forced
# or the stored checksum is not the same as the current
# generated checksum.
#
# Returns the original design_doc provided, but does
# not update it with the revision.
def update_design_doc(db, force = false)
return design_doc unless force || auto_update_design_doc
# Grab the design doc's checksum
checksum = design_doc.checksum!
# If auto updates enabled, check checksum cache
return design_doc if auto_update_design_doc && design_doc_cache_checksum(db) == checksum
# Load up the stored doc (if present), update, and save
saved = stored_design_doc(db)
if saved
if force || saved['couchrest-hash'] != checksum
saved.merge!(design_doc)
db.save_doc(saved)
end
else
db.save_doc(design_doc)
design_doc.delete('_rev') # Prevent conflicts, never store rev as DB specific
end
# Ensure checksum cached for next attempt if using auto updates
set_design_doc_cache_checksum(db, checksum) if auto_update_design_doc
design_doc
end
def default_design_doc
{
"_id" => design_doc_id,
@ -40,68 +108,7 @@ module CouchRest
}
end
# DEPRECATED
# use stored_design_doc to retrieve the current design doc
def all_design_doc_versions(db = database)
db.documents :startkey => "_design/#{self.to_s}",
:endkey => "_design/#{self.to_s}-\u9999"
end
# Retreive the latest version of the design document directly
# from the database.
def stored_design_doc(db = database)
db.get(design_doc_id) rescue nil
end
alias :model_design_doc :stored_design_doc
def refresh_design_doc(db = database)
raise "Database missing for design document refresh" if db.nil?
unless design_doc_fresh(db)
save_design_doc(db)
design_doc_fresh(db, true)
end
end
# Save the design doc onto a target database in a thread-safe way,
# not modifying the model's design_doc
#
# See also save_design_doc! to always save the design doc even if there
# are no changes.
def save_design_doc(db = database, force = false)
update_design_doc(Design.new(design_doc), db, force)
end
# Force the update of the model's design_doc even if it hasn't changed.
def save_design_doc!(db = database)
save_design_doc(db, true)
end
protected
def design_doc_fresh(db, fresh = nil)
@design_doc_fresh ||= {}
if fresh.nil?
@design_doc_fresh[db.uri] || false
else
@design_doc_fresh[db.uri] = fresh
end
end
# Writes out a design_doc to a given database, returning the
# updated design doc
def update_design_doc(design_doc, db, force = false)
design_doc['couchrest-hash'] = design_doc.checksum
saved = stored_design_doc(db)
if saved
if force || saved['couchrest-hash'] != design_doc['couchrest-hash']
saved.merge!(design_doc)
db.save_doc(saved)
end
else
db.save_doc(design_doc)
end
design_doc
end
end # module ClassMethods

View file

@ -28,8 +28,6 @@ module CouchRest
mapper.create_view_method(:all)
mapper.instance_eval(&block) if block_given?
req_design_doc_refresh
end
# Override the default page pagination value:

View file

@ -48,7 +48,6 @@ module CouchRest
end
# Base
def new(*args)
proxy_update(model.new(*args))
end
@ -56,7 +55,7 @@ module CouchRest
def build_from_database(doc = {})
proxy_update(model.build_from_database(doc))
end
def method_missing(m, *args, &block)
if has_view?(m)
if model.respond_to?(m)
@ -73,32 +72,32 @@ module CouchRest
end
super
end
# DocumentQueries
def all(opts = {}, &block)
proxy_update_all(@model.all({:database => @database}.merge(opts), &block))
end
def count(opts = {})
@model.count({:database => @database}.merge(opts))
end
def first(opts = {})
proxy_update(@model.first({:database => @database}.merge(opts)))
end
def last(opts = {})
proxy_update(@model.last({:database => @database}.merge(opts)))
end
def get(id)
proxy_update(@model.get(id, @database))
end
alias :find :get
# Views
def has_view?(view)
@model.has_view?(view)
end
@ -106,27 +105,22 @@ module CouchRest
def view_by(*args)
@model.view_by(*args)
end
def view(name, query={}, &block)
proxy_update_all(@model.view(name, {:database => @database}.merge(query), &block))
end
def first_from_view(name, *args)
# add to first hash available, or add to end
(args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
proxy_update(@model.first_from_view(name, *args))
end
# DesignDoc
def design_doc
@model.design_doc
end
def refresh_design_doc(db = nil)
@model.refresh_design_doc(db || @database)
end
def save_design_doc(db = nil)
@model.save_design_doc(db || @database)
end

View file

@ -1,15 +0,0 @@
CouchRest::Database.class_eval do
alias :delete_orig! :delete!
def delete!
clear_model_fresh_cache
delete_orig!
end
# If the database is deleted, ensure that the design docs will be refreshed.
def clear_model_fresh_cache
::CouchRest::Model::Base.subclasses.each{|klass| klass.req_design_doc_refresh if klass.respond_to?(:req_design_doc_refresh)}
end
end

View file

@ -1,18 +1,19 @@
CouchRest::Design.class_eval do
# Calculate a checksum of the Design document. Used for ensuring the latest
# version has been sent to the database.
# Calculate and update the checksum of the Design document.
# Used for ensuring the latest version has been sent to the database.
#
# This will generate an flatterned, ordered array of all the elements of the
# design document, convert to string then generate an MD5 Hash. This should
# result in a consisitent Hash accross all platforms.
#
def checksum
def checksum!
# create a copy of basic elements
base = self.dup
base.delete('_id')
base.delete('_rev')
base.delete('couchrest-hash')
result = nil
flatten =
lambda {|r|
@ -26,7 +27,7 @@ CouchRest::Design.class_eval do
end
}).call(r)
}
Digest::MD5.hexdigest(flatten.call(base).sort.join(''))
self['couchrest-hash'] = Digest::MD5.hexdigest(flatten.call(base).sort.join(''))
end
end

View file

@ -81,7 +81,6 @@ module CouchRest
end
keys.push opts
design_doc.view_by(*keys)
req_design_doc_refresh
end
# returns stored defaults if there is a view named this in the design doc
@ -99,9 +98,9 @@ module CouchRest
def view(name, query={}, &block)
query = query.dup # Modifications made on copy!
db = query.delete(:database) || database
refresh_design_doc(db)
query[:raw] = true if query[:reduce]
raw = query.delete(:raw)
save_design_doc(db)
fetch_view_with_docs(db, name, query, raw, &block)
end
@ -140,23 +139,10 @@ module CouchRest
def fetch_view(db, view_name, opts, &block)
raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
retryable = true
begin
design_doc.view_on(db, view_name, opts, &block)
# the design doc may not have been saved yet on this database
rescue RestClient::ResourceNotFound => e
if retryable
save_design_doc(db)
retryable = false
retry
else
raise e
end
end
design_doc.view_on(db, view_name, opts, &block)
end
end # module ClassMethods
end
end
end