2008-09-30 19:20:15 +02:00
# = CouchRest::Model - ORM, the CouchDB way
2008-09-29 18:55:40 +02:00
module CouchRest
2008-09-30 19:20:15 +02:00
# = CouchRest::Model - ORM, the CouchDB way
#
2008-09-30 08:16:44 +02:00
# CouchRest::Model provides an ORM-like interface for CouchDB documents. It avoids all usage of <tt>method_missing</tt>, and tries to strike a balance between usability and magic. See CouchRest::Model::MagicViews#view_by for documentation about the view-generation system. For the other class methods, inspiried by DataMapper and ActiveRecord, see CouchRest::Model::ClassMethods. The InstanceMethods are pretty basic.
#
# ==== Example
#
# This is an example class using CouchRest::Model. It is taken from the spec/couchrest/core/model_spec.rb file, which may be even more up to date than this example.
#
# class Article
# include CouchRest::Model
# use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
# unique_id :slug
#
# view_by :date, :descending => true
# view_by :user_id, :date
#
# view_by :tags,
# :map =>
# "function(doc) {
# if (doc.type == 'Article' && doc.tags) {
# doc.tags.forEach(function(tag){
# emit(tag, 1);
# });
# }
# }",
# :reduce =>
# "function(keys, values, rereduce) {
# return sum(values);
# }"
#
# key_writer :date
# key_reader :slug, :created_at, :updated_at
# key_accessor :title, :tags
#
# timestamps!
#
# before(:create, :generate_slug_from_title)
# def generate_slug_from_title
# doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
# end
# end
2008-09-29 18:55:40 +02:00
module Model
class << self
2008-09-30 07:56:24 +02:00
# this is the CouchRest::Database that model classes will use unless they override it with <tt>use_database</tt>
2008-09-29 18:55:40 +02:00
attr_accessor :default_database
end
# instance methods on the model classes
module InstanceMethods
attr_accessor :doc
2008-09-30 01:28:57 +02:00
def initialize keys = { }
self . doc = { }
keys . each do | k , v |
doc [ k . to_s ] = v
end
2008-09-29 18:55:40 +02:00
unless doc [ '_id' ] && doc [ '_rev' ]
init_doc
end
end
2008-09-30 07:56:24 +02:00
# returns the database used by this model's class
2008-09-29 18:55:40 +02:00
def database
self . class . database
end
2008-09-30 07:56:24 +02:00
# alias for doc['_id']
2008-09-29 18:55:40 +02:00
def id
doc [ '_id' ]
end
2008-09-30 07:56:24 +02:00
# alias for doc['_rev']
2008-09-29 18:55:40 +02:00
def rev
doc [ '_rev' ]
end
2008-09-30 07:56:24 +02:00
# returns true if the doc has never been saved
2008-09-30 01:28:57 +02:00
def new_record?
! doc [ '_rev' ]
end
2008-09-30 07:56:24 +02:00
# save the doc to the db using create or update
2008-09-29 18:55:40 +02:00
def save
2008-09-30 01:28:57 +02:00
if new_record?
create
else
update
end
end
protected
def create
2008-09-30 03:10:07 +02:00
set_unique_id if respond_to? ( :set_unique_id ) # hack
2008-09-30 01:28:57 +02:00
save_doc
end
def update
save_doc
end
private
def save_doc
2008-09-29 18:55:40 +02:00
result = database . save doc
if result [ 'ok' ]
doc [ '_id' ] = result [ 'id' ]
doc [ '_rev' ] = result [ 'rev' ]
end
result [ 'ok' ]
end
def init_doc
doc [ 'type' ] = self . class . to_s
end
end # module InstanceMethods
2008-09-30 08:39:57 +02:00
# Class methods for models that include CouchRest::Model
2008-09-29 18:55:40 +02:00
module ClassMethods
2008-09-30 07:56:24 +02:00
# override the CouchRest::Model-wide default_database
2008-09-29 18:55:40 +02:00
def use_database db
@database = db
end
2008-09-30 07:56:24 +02:00
# returns the CouchRest::Database instance that this class uses
2008-09-29 18:55:40 +02:00
def database
@database || CouchRest :: Model . default_database
end
2008-09-30 07:56:24 +02:00
# load a document from the database
2008-09-30 01:28:57 +02:00
def get id
doc = database . get id
new ( doc )
end
2008-09-30 07:56:24 +02:00
# Defines methods for reading and writing from fields in the document. Uses key_writer and key_reader internally.
2008-09-30 01:28:57 +02:00
def key_accessor * keys
key_writer * keys
key_reader * keys
end
2008-09-30 07:56:24 +02:00
# For each argument key, define a method <tt>key=</tt> that sets the corresponding field on the CouchDB document.
2008-09-30 01:28:57 +02:00
def key_writer * keys
keys . each do | method |
key = method . to_s
define_method " #{ method } = " do | value |
doc [ key ] = value
end
end
end
2008-09-30 07:56:24 +02:00
# For each argument key, define a method <tt>key</tt> that reads the corresponding field on the CouchDB document.
2008-09-30 01:28:57 +02:00
def key_reader * keys
keys . each do | method |
key = method . to_s
define_method method do
doc [ key ]
end
end
end
2008-09-30 07:56:24 +02:00
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields on the document whenever saving occurs. CouchRest uses a pretty decent time format by default. See Time#to_json
2008-09-30 01:28:57 +02:00
def timestamps!
before ( :create ) do
doc [ 'updated_at' ] = doc [ 'created_at' ] = Time . now
end
before ( :update ) do
doc [ 'updated_at' ] = Time . now
end
end
2008-09-30 07:56:24 +02:00
# Name a method that will be called before the document is first saved, which returns a string to be used for the document's <tt>_id</tt>. Because CouchDB enforces a constraint that each id must be unique, this can be used to enforce eg: uniq usernames. Note that this id must be globally unique across all document types which share a database, so if you'd like to scope uniqueness to this class, you should use the class name as part of the unique id.
2008-09-30 03:10:07 +02:00
def unique_id method
define_method :set_unique_id do
2008-09-30 01:28:57 +02:00
doc [ '_id' ] || = self . send ( method )
2008-09-29 18:55:40 +02:00
end
end
2008-09-30 01:28:57 +02:00
2008-09-29 18:55:40 +02:00
end # module ClassMethods
2008-09-30 01:28:57 +02:00
module MagicViews
2008-09-30 07:56:24 +02:00
# Define a CouchDB view. The name of the view will be the concatenation of <tt>by</tt> and the keys joined by <tt>_and_</tt>
#
2008-09-30 08:16:44 +02:00
# ==== Example views:
#
2008-09-30 07:56:24 +02:00
# class Post
2008-09-30 08:16:44 +02:00
# # view with default options
# # query with Post.by_date
# view_by :date, :descending => true
#
# # view with compound sort-keys
# # query with Post.by_user_id_and_date
# view_by :user_id, :date
#
# # view with custom map/reduce functions
# # query with Post.by_tags :reduce => true
# view_by :tags,
# :map =>
# "function(doc) {
# if (doc.type == 'Post' && doc.tags) {
# doc.tags.forEach(function(tag){
# emit(doc.tag, 1);
# });
# }
# }",
# :reduce =>
# "function(keys, values, rereduce) {
# return sum(values);
# }"
2008-09-30 07:56:24 +02:00
# end
#
2008-09-30 08:16:44 +02:00
# <tt>view_by :date</tt> will create a view defined by this Javascript function:
2008-09-30 07:56:24 +02:00
#
# function(doc) {
# if (doc.type == 'Post' && doc.date) {
# emit(doc.date, null);
# }
# }
#
2008-09-30 07:57:52 +02:00
# It can be queried by calling <tt>Post.by_date</tt> which accepts all valid options for CouchRest::Database#view. In addition, calling with the <tt>:raw => true</tt> option will return the view rows themselves. By default <tt>Post.by_date</tt> will return the documents included in the generated view.
2008-09-30 08:16:44 +02:00
#
# CouchRest::Database#view options can be applied at view definition time as defaults, and they will be curried and used at view query time. Or they can be overridden at query time.
#
# Custom views can be queried with <tt>:reduce => true</tt> to return reduce results. The default for custom views is to query with <tt>:reduce => false</tt>.
#
2008-09-30 19:20:15 +02:00
# Views are generated (on a per-model basis) lazily on first-access. This means that if you are deploying changes to a view, the views for that model won't be available until generation is complete. This can take some time with large databases. Strategies are in the works.
#
2008-09-30 08:16:44 +02:00
# To understand the capabilities of this view system more compeletly, it is recommended that you read the RSpec file at <tt>spec/core/model.rb</tt>.
2008-09-30 01:28:57 +02:00
def view_by * keys
2008-09-30 03:10:07 +02:00
opts = keys . pop if keys . last . is_a? ( Hash )
2008-09-30 05:18:18 +02:00
opts || = { }
2008-09-30 01:28:57 +02:00
type = self . to_s
method_name = " by_ #{ keys . join ( '_and_' ) } "
@@design_doc || = default_design_doc
2008-09-30 03:10:07 +02:00
2008-09-30 05:18:18 +02:00
if opts [ :map ]
2008-09-30 03:10:07 +02:00
view = { }
2008-09-30 05:18:18 +02:00
view [ 'map' ] = opts . delete ( :map )
if opts [ :reduce ]
view [ 'reduce' ] = opts . delete ( :reduce )
opts [ :reduce ] = false
end
2008-09-30 03:10:07 +02:00
@@design_doc [ 'views' ] [ method_name ] = view
else
doc_keys = keys . collect { | k | " doc[' #{ k } '] " }
key_protection = doc_keys . join ( ' && ' )
key_emit = doc_keys . length == 1 ? " #{ doc_keys . first } " : " [ #{ doc_keys . join ( ', ' ) } ] "
map_function = <<-JAVASCRIPT
function ( doc ) {
if ( doc . type == '#{type}' && #{key_protection}) {
emit ( #{key_emit}, null);
}
}
JAVASCRIPT
@@design_doc [ 'views' ] [ method_name ] = {
'map' = > map_function
}
end
2008-09-30 01:28:57 +02:00
@@design_doc_fresh = false
2008-09-30 02:27:41 +02:00
2008-09-30 01:28:57 +02:00
self . meta_class . instance_eval do
2008-09-30 02:46:33 +02:00
define_method method_name do | * args |
2008-09-30 05:18:18 +02:00
query = opts . merge ( args [ 0 ] || { } )
query [ :raw ] = true if query [ :reduce ]
2008-09-30 01:28:57 +02:00
unless @@design_doc_fresh
refresh_design_doc
end
2008-09-30 05:18:18 +02:00
raw = query . delete ( :raw )
2008-09-30 02:27:41 +02:00
view_name = " #{ type } / #{ method_name } "
2008-09-30 05:18:18 +02:00
view = fetch_view ( view_name , query )
2008-09-30 02:27:41 +02:00
if raw
2008-09-30 05:18:18 +02:00
view
2008-09-30 02:27:41 +02:00
else
# TODO this can be optimized once the include-docs patch is applied
view [ 'rows' ] . collect { | r | new ( database . get ( r [ 'id' ] ) ) }
end
2008-09-30 01:28:57 +02:00
end
end
end
private
2008-09-30 02:46:33 +02:00
def fetch_view view_name , opts
2008-09-30 02:27:41 +02:00
retryable = true
begin
2008-09-30 02:46:33 +02:00
database . view ( view_name , opts )
2008-09-30 02:27:41 +02:00
# the design doc could have been deleted by a rouge process
rescue RestClient :: ResourceNotFound = > e
if retryable
refresh_design_doc
retryable = false
retry
else
raise e
end
end
end
2008-09-30 01:28:57 +02:00
def design_doc_id
" _design/ #{ self . to_s } "
end
def default_design_doc
{
" _id " = > design_doc_id ,
" language " = > " javascript " ,
" views " = > { }
}
end
def refresh_design_doc
saved = database . get ( design_doc_id ) rescue nil
if saved
2008-09-30 02:27:41 +02:00
@@design_doc [ 'views' ] . each do | name , view |
saved [ 'views' ] [ name ] = view
end
database . save ( saved )
2008-09-30 01:28:57 +02:00
else
database . save ( @@design_doc )
end
@@design_doc_fresh = true
end
end # module MagicViews
module Callbacks
def self . included ( model )
model . class_eval <<-EOS, __FILE__, __LINE__
include Extlib :: Hook
2008-09-30 02:27:41 +02:00
register_instance_hooks :save , :create , :update #, :destroy
2008-09-30 01:28:57 +02:00
EOS
end
end # module Callbacks
2008-09-29 18:55:40 +02:00
# bookkeeping section
# load the code into the model class
2008-09-30 01:28:57 +02:00
def self . included ( model )
model . send ( :include , InstanceMethods )
model . extend ClassMethods
model . extend MagicViews
model . send ( :include , Callbacks )
2008-09-29 18:55:40 +02:00
end
end # module Model
end # module CouchRest