Class: CouchRest::Model

Inherits:
Hash
  • Object
show all
Includes:
Extlib::Hook
Defined in:
lib/couchrest/core/model.rb

Overview

CouchRest::Model - ORM, the CouchDB way

CouchRest::Model provides an ORM-like interface for CouchDB documents. It avoids all usage of method_missing, and tries to strike a balance between usability and magic. See CouchRest::Model#view_by for documentation about the view-generation system.

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 < 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['couchrest-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
    self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
  end
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(keys = {}) ⇒ Model

instantiates the hash by converting all the keys to strings.



55
56
57
58
59
60
61
62
63
64
65
# File 'lib/couchrest/core/model.rb', line 55

def initialize keys = {}
  super()
  apply_defaults
  keys.each do |k,v|
    self[k.to_s] = v
  end
  cast_keys
  unless self['_id'] && self['_rev']
    self['couchrest-type'] = self.class.to_s
  end
end

Class Method Details

.all(opts = {}) ⇒ Object



95
96
97
98
99
100
101
102
103
104
# File 'lib/couchrest/core/model.rb', line 95

def all opts = {}
  self.generated_design_doc ||= default_design_doc
  unless design_doc_fresh
    refresh_design_doc
  end
  view_name = "#{design_doc_slug}/all"
  raw = opts.delete(:raw)
  view = fetch_view(view_name, opts)
  process_view_results view, raw
end

.cast(field, opts = {}) ⇒ Object

Cast a field as another class. The class must be happy to have the field’s primitive type as the argument to it’s constucture. Classes which inherit from CouchRest::Model are happy to act as sub-objects for any fields that are stored in JSON as object (and therefore are parsed from the JSON as Ruby Hashes).



111
112
113
114
# File 'lib/couchrest/core/model.rb', line 111

def cast field, opts = {}
  self.casts ||= {}
  self.casts[field.to_s] = opts
end

.databaseObject

returns the CouchRest::Database instance that this class uses



85
86
87
# File 'lib/couchrest/core/model.rb', line 85

def database
  self.class_database || CouchRest::Model.default_database
end

.defaultObject



145
146
147
# File 'lib/couchrest/core/model.rb', line 145

def default
  self.default_obj
end

.design_docObject

Fetch the generated design doc. Could raise an error if the generated views have not been queried yet.



298
299
300
# File 'lib/couchrest/core/model.rb', line 298

def design_doc
  database.get("_design/#{design_doc_slug}")
end

.get(id) ⇒ Object

load a document from the database



90
91
92
93
# File 'lib/couchrest/core/model.rb', line 90

def get id
  doc = database.get id
  new(doc)
end

.key_accessor(*keys) ⇒ Object

Defines methods for reading and writing from fields in the document. Uses key_writer and key_reader internally.



118
119
120
121
# File 'lib/couchrest/core/model.rb', line 118

def key_accessor *keys
  key_writer *keys
  key_reader *keys
end

.key_reader(*keys) ⇒ Object

For each argument key, define a method key that reads the corresponding field on the CouchDB document.



136
137
138
139
140
141
142
143
# File 'lib/couchrest/core/model.rb', line 136

def key_reader *keys
  keys.each do |method|
    key = method.to_s
    define_method method do
      self[key]
    end
  end
end

.key_writer(*keys) ⇒ Object

For each argument key, define a method key= that sets the corresponding field on the CouchDB document.



125
126
127
128
129
130
131
132
# File 'lib/couchrest/core/model.rb', line 125

def key_writer *keys
  keys.each do |method|
    key = method.to_s
    define_method "#{method}=" do |value|
      self[key] = value
    end
  end
end

.set_default(hash) ⇒ Object



149
150
151
# File 'lib/couchrest/core/model.rb', line 149

def set_default hash
  self.default_obj = hash
end

.timestamps!Object

Automatically set updated_at and created_at fields on the document whenever saving occurs. CouchRest uses a pretty decent time format by default. See Time#to_json



156
157
158
159
160
161
162
163
# File 'lib/couchrest/core/model.rb', line 156

def timestamps!
  before(:create) do
    self['updated_at'] = self['created_at'] = Time.now
  end                  
  before(:update) do   
    self['updated_at'] = Time.now
  end
end

.unique_id(method = nil, &block) ⇒ Object

Name a method that will be called before the document is first saved, which returns a string to be used for the document’s _id. 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.



172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/couchrest/core/model.rb', line 172

def unique_id method = nil, &block
  if method
    define_method :set_unique_id do
      self['_id'] ||= self.send(method)
    end
  elsif block
    define_method :set_unique_id do
      uniqid = block.call(self)
      raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
      self['_id'] ||= uniqid
    end
  end
end

.use_database(db) ⇒ Object

override the CouchRest::Model-wide default_database



80
81
82
# File 'lib/couchrest/core/model.rb', line 80

def use_database db
  self.class_database = db
end

.view_by(*keys) ⇒ Object

Define a CouchDB view. The name of the view will be the concatenation of by and the keys joined by and

Example views:

class Post
  # 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['couchrest-type'] == 'Post' && doc.tags) {                   
          doc.tags.forEach(function(tag){                       
            emit(doc.tag, 1);                                   
          });                                                   
        }                                                       
      }",                                                       
    :reduce =>                                                  
      "function(keys, values, rereduce) {                       
        return sum(values);                                     
      }"                                                        
end

view_by :date will create a view defined by this Javascript function:

function(doc) {
  if (doc['couchrest-type'] == 'Post' && doc.date) {
    emit(doc.date, null);
  }
}

It can be queried by calling Post.by_date which accepts all valid options for CouchRest::Database#view. In addition, calling with the :raw => true option will return the view rows themselves. By default Post.by_date will return the documents included in the generated view.

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 :reduce => true to return reduce results. The default for custom views is to query with :reduce => false.

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.

To understand the capabilities of this view system more compeletly, it is recommended that you read the RSpec file at spec/core/model.rb.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/couchrest/core/model.rb', line 248

def view_by *keys
  opts = keys.pop if keys.last.is_a?(Hash)
  opts ||= {}
  type = self.to_s

  method_name = "by_#{keys.join('_and_')}"
  self.generated_design_doc ||= default_design_doc

  if opts[:map]
    view = {}
    view['map'] = opts.delete(:map)
    if opts[:reduce]
      view['reduce'] = opts.delete(:reduce)
      opts[:reduce] = false
    end
    generated_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['couchrest-type'] == '#{type}' && #{key_protection}) {
        emit(#{key_emit}, null);
      }
    }
    JAVASCRIPT
    generated_design_doc['views'][method_name] = {
      'map' => map_function
    }
  end

  self.design_doc_fresh = false

  self.meta_class.instance_eval do
    define_method method_name do |*args|
      query = opts.merge(args[0] || {})
      query[:raw] = true if query[:reduce]
      unless design_doc_fresh
        refresh_design_doc
      end
      raw = query.delete(:raw)
      view_name = "#{design_doc_slug}/#{method_name}"
      view = fetch_view(view_name, query)
      process_view_results view, raw
    end
  end
end

Instance Method Details

#databaseObject

returns the database used by this model’s class



372
373
374
# File 'lib/couchrest/core/model.rb', line 372

def database
  self.class.database
end

#destroyObject

Deletes the document from the database. Runs the :delete callbacks. Removes the _id and _rev fields, preparing the document to be saved to a new _id.



405
406
407
408
409
410
411
412
# File 'lib/couchrest/core/model.rb', line 405

def destroy
  result = database.delete self
  if result['ok']
    self['_rev'] = nil
    self['_id'] = nil
  end
  result['ok']
end

#idObject

alias for self



377
378
379
# File 'lib/couchrest/core/model.rb', line 377

def id
  self['_id']
end

#new_record?Boolean

returns true if the document has never been saved

Returns:

  • (Boolean)


387
388
389
# File 'lib/couchrest/core/model.rb', line 387

def new_record?
  !rev
end

#revObject

alias for self



382
383
384
# File 'lib/couchrest/core/model.rb', line 382

def rev
  self['_rev']
end

#saveObject

Saves the document to the db using create or update. Also runs the :save callbacks. Sets the _id and _rev fields based on CouchDB’s response.



394
395
396
397
398
399
400
# File 'lib/couchrest/core/model.rb', line 394

def save
  if new_record?
    create
  else
    update
  end
end