Class: CouchRest::Model

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

Overview

CouchRest::Model - Document modeling, 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://127.0.0.1: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

Examples of finding articles with these views:

  • All the articles by Barney published in the last 24 hours. Note that we use {} as a special value that sorts after all strings, numbers, and arrays.

    Article.by_user_id_and_date :startkey => ["barney", Time.now - 24 * 3600], :endkey => ["barney", {}]
    
  • The most recent 20 articles. Remember that the view_by :date has the default option :descending => true.

    Article.by_date :limit => 20
    
  • The raw CouchDB view reduce result for the custom :tags view. In this case we’ll get a count of the number of articles tagged “ruby”.

    Article.by_tags :key => "ruby", :reduce => true
    

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Document

#copy, #fetch_attachment, #id, #move, #put_attachment, #rev, #uri

Methods inherited from Response

#[], #[]=

Constructor Details

#initialize(keys = {}) ⇒ Model

instantiates the hash by converting all the keys to strings.



81
82
83
84
85
86
87
88
# File 'lib/couchrest/core/model.rb', line 81

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

Class Method Details

.all(opts = {}, &block) ⇒ Object

Load all documents that have the “couchrest-type” field equal to the name of the current class. Take the standard set of CouchRest::Database#view options.



121
122
123
124
125
126
127
# File 'lib/couchrest/core/model.rb', line 121

def all opts = {}, &block
  self.design_doc ||= Design.new(default_design_doc)
  unless design_doc_fresh
    refresh_design_doc
  end
  view :all, opts, &block
end

.all_design_doc_versionsObject



354
355
356
357
# File 'lib/couchrest/core/model.rb', line 354

def all_design_doc_versions
  database.documents :startkey => "_design/#{self.to_s}-", 
    :endkey => "_design/#{self.to_s}-\u9999"
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 constuctur. 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).

Example:

class Post < CouchRest::Model

  key_accessor :title, :body, :author

  cast :author, :as => 'Author'

end

post.author.class #=> Author

Using the same example, if a Post should have many Comments, we would declare it like this:

class Post < CouchRest::Model

  key_accessor :title, :body, :author, comments

  cast :author, :as => 'Author'
  cast :comments, :as => ['Comment']

end

post.author.class #=> Author
post.comments.class #=> Array
post.comments.first #=> Comment


179
180
181
182
# File 'lib/couchrest/core/model.rb', line 179

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

.cleanup_design_docs!Object

Deletes any non-current design docs that were created by this class. Running this when you’re deployed version of your application is steadily and consistently using the latest code, is the way to clear out old design docs. Running it to early could mean that live code has to regenerate potentially large indexes.



364
365
366
367
368
369
370
371
372
373
374
# File 'lib/couchrest/core/model.rb', line 364

def cleanup_design_docs!
  ddocs = all_design_doc_versions
  ddocs["rows"].each do |row|
    if (row['id'] != design_doc_id)
      database.delete_doc({
        "_id" => row['id'],
        "_rev" => row['value']['rev']
      })
    end
  end
end

.databaseObject

returns the CouchRest::Database instance that this class uses



108
109
110
# File 'lib/couchrest/core/model.rb', line 108

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

.defaultObject



213
214
215
# File 'lib/couchrest/core/model.rb', line 213

def default
  self.default_obj
end

.first(opts = {}) ⇒ Object

Load the first document that have the “couchrest-type” field equal to the name of the current class.

Returns

Object

The first object instance available

or

Nil

if no instances available

Parameters

opts<Hash>

View options, see CouchRest::Database#view options for more info.



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

def first opts = {}
  first_instance = self.all(opts.merge!(:limit => 1))
  first_instance.empty? ? nil : first_instance.first
end

.get(id) ⇒ Object

Load a document from the database by id



113
114
115
116
# File 'lib/couchrest/core/model.rb', line 113

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

.has_view?(view) ⇒ Boolean

returns stored defaults if the there is a view named this in the design doc

Returns:

  • (Boolean)


339
340
341
342
# File 'lib/couchrest/core/model.rb', line 339

def has_view?(view)
  view = view.to_s
  design_doc && design_doc['views'] && design_doc['views'][view]
end

.key_accessor(*keys) ⇒ Object

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



186
187
188
189
# File 'lib/couchrest/core/model.rb', line 186

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.



204
205
206
207
208
209
210
211
# File 'lib/couchrest/core/model.rb', line 204

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.



193
194
195
196
197
198
199
200
# File 'lib/couchrest/core/model.rb', line 193

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

.method_missing(m, *args) ⇒ Object



329
330
331
332
333
334
335
336
# File 'lib/couchrest/core/model.rb', line 329

def method_missing m, *args
  if has_view?(m)
    query = args.shift || {}
    view(m, query, *args)
  else
    super
  end
end

.set_default(hash) ⇒ Object



217
218
219
# File 'lib/couchrest/core/model.rb', line 217

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



224
225
226
227
228
229
# File 'lib/couchrest/core/model.rb', line 224

def timestamps!
  before(:save) do
    self['updated_at'] = Time.now
    self['created_at'] = self['updated_at'] if new_document?
  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.



238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/couchrest/core/model.rb', line 238

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



103
104
105
# File 'lib/couchrest/core/model.rb', line 103

def use_database db
  self.class_database = db
end

.view(name, query = {}, &block) ⇒ Object

Dispatches to any named view.



345
346
347
348
349
350
351
352
# File 'lib/couchrest/core/model.rb', line 345

def view name, query={}, &block
  unless design_doc_fresh
    refresh_design_doc
  end
  query[:raw] = true if query[:reduce]        
  raw = query.delete(:raw)
  fetch_view_with_docs(name, query, raw, &block)
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_spec.rb.



315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/couchrest/core/model.rb', line 315

def view_by *keys
  self.design_doc ||= Design.new(default_design_doc)
  opts = keys.pop if keys.last.is_a?(Hash)
  opts ||= {}
  ducktype = opts.delete(:ducktype)
  unless ducktype || opts[:map]
    opts[:guards] ||= []
    opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
  end
  keys.push opts
  self.design_doc.view_by(*keys)
  self.design_doc_fresh = false
end

Instance Method Details

#attachment_url(attachment_name) ⇒ Object

returns URL to fetch the attachment from



550
551
552
553
# File 'lib/couchrest/core/model.rb', line 550

def attachment_url(attachment_name)
  return unless has_attachment?(attachment_name)
  "#{database.root}/#{self.id}/#{attachment_name}"
end

#create_attachment(args = {}) ⇒ Object

creates a file attachment to the current doc



514
515
516
517
518
519
520
521
# File 'lib/couchrest/core/model.rb', line 514

def create_attachment(args={})
  raise ArgumentError unless args[:file] && args[:name]
  return if has_attachment?(args[:name])
  self['_attachments'] ||= {}
  set_attachment_attr(args)
rescue ArgumentError => e
  raise ArgumentError, 'You must specify :file and :name'
end

#databaseObject

returns the database used by this model’s class



460
461
462
# File 'lib/couchrest/core/model.rb', line 460

def database
  self.class.database
end

#delete_attachment(attachment_name) ⇒ Object

deletes a file attachment from the current doc



539
540
541
542
# File 'lib/couchrest/core/model.rb', line 539

def delete_attachment(attachment_name)
  return unless self['_attachments']
  self['_attachments'].delete attachment_name
end

#destroyObject

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



504
505
506
507
508
509
510
511
# File 'lib/couchrest/core/model.rb', line 504

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

#has_attachment?(attachment_name) ⇒ Boolean

returns true if attachment_name exists

Returns:

  • (Boolean)


545
546
547
# File 'lib/couchrest/core/model.rb', line 545

def has_attachment?(attachment_name)
  !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
end

#read_attachment(attachment_name) ⇒ Object

reads the data from an attachment



524
525
526
# File 'lib/couchrest/core/model.rb', line 524

def read_attachment(attachment_name)
  Base64.decode64(database.fetch_attachment(self.id, attachment_name))
end

#save(bulk = false) ⇒ Object

Overridden to set the unique ID. Returns a boolean value



489
490
491
492
493
# File 'lib/couchrest/core/model.rb', line 489

def save bulk = false
  set_unique_id if new_document? && self.respond_to?(:set_unique_id)
  result = database.save_doc(self, bulk)
  result["ok"] == true
end

#save!Object

Saves the document to the db using create or update. Raises an exception if the document is not saved properly.



497
498
499
# File 'lib/couchrest/core/model.rb', line 497

def save!
  raise "#{self.inspect} failed to save" unless self.save
end

#update_attachment(args = {}) ⇒ Object

modifies a file attachment on the current doc



529
530
531
532
533
534
535
536
# File 'lib/couchrest/core/model.rb', line 529

def update_attachment(args={})
  raise ArgumentError unless args[:file] && args[:name]
  return unless has_attachment?(args[:name])
  delete_attachment(args[:name])
  set_attachment_attr(args)
rescue ArgumentError => e
  raise ArgumentError, 'You must specify :file and :name'
end

#update_attributes(hash) ⇒ Object

Takes a hash as argument, and applies the values by using writer methods for each key. Raises a NoMethodError if the corresponding methods are missing. In case of error, no attributes are changed.



479
480
481
482
# File 'lib/couchrest/core/model.rb', line 479

def update_attributes hash
  update_attributes_without_saving hash
  save
end

#update_attributes_without_saving(hash) ⇒ Object

Takes a hash as argument, and applies the values by using writer methods for each key. It doesn’t save the document at the end. Raises a NoMethodError if the corresponding methods are missing. In case of error, no attributes are changed.



467
468
469
470
471
472
473
474
# File 'lib/couchrest/core/model.rb', line 467

def update_attributes_without_saving hash
  hash.each do |k, v|
    raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=")
  end      
  hash.each do |k, v|
    self.send("#{k}=",v)
  end
end