Class: CouchRest::Model
- 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
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. :key => "ruby", :reduce => true
Class Method Summary collapse
-
.all(opts = {}, &block) ⇒ Object
Load all documents that have the “couchrest-type” field equal to the name of the current class.
- .all_design_doc_versions ⇒ Object
-
.cast(field, opts = {}) ⇒ Object
Cast a field as another class.
-
.cleanup_design_docs! ⇒ Object
Deletes any non-current design docs that were created by this class.
-
.database ⇒ Object
returns the CouchRest::Database instance that this class uses.
- .default ⇒ Object
-
.first(opts = {}) ⇒ Object
Load the first document that have the “couchrest-type” field equal to the name of the current class.
-
.get(id) ⇒ Object
Load a document from the database by id.
-
.has_view?(view) ⇒ Boolean
returns stored defaults if the there is a view named this in the design doc.
-
.key_accessor(*keys) ⇒ Object
Defines methods for reading and writing from fields in the document.
-
.key_reader(*keys) ⇒ Object
For each argument key, define a method
key
that reads the corresponding field on the CouchDB document. -
.key_writer(*keys) ⇒ Object
For each argument key, define a method
key=
that sets the corresponding field on the CouchDB document. - .method_missing(m, *args) ⇒ Object
- .set_default(hash) ⇒ Object
-
.timestamps! ⇒ Object
Automatically set
updated_at
andcreated_at
fields on the document whenever saving occurs. -
.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
. -
.use_database(db) ⇒ Object
override the CouchRest::Model-wide default_database.
-
.view(name, query = {}, &block) ⇒ Object
Dispatches to any named view.
-
.view_by(*keys) ⇒ Object
Define a CouchDB view.
Instance Method Summary collapse
-
#attachment_url(attachment_name) ⇒ Object
returns URL to fetch the attachment from.
-
#create_attachment(args = {}) ⇒ Object
creates a file attachment to the current doc.
-
#database ⇒ Object
returns the database used by this model’s class.
-
#delete_attachment(attachment_name) ⇒ Object
deletes a file attachment from the current doc.
-
#destroy ⇒ Object
Deletes the document from the database.
-
#has_attachment?(attachment_name) ⇒ Boolean
returns true if attachment_name exists.
-
#initialize(keys = {}) ⇒ Model
constructor
instantiates the hash by converting all the keys to strings.
-
#read_attachment(attachment_name) ⇒ Object
reads the data from an attachment.
-
#save(bulk = false) ⇒ Object
Overridden to set the unique ID.
-
#save! ⇒ Object
Saves the document to the db using create or update.
-
#update_attachment(args = {}) ⇒ Object
modifies a file attachment on the current doc.
-
#update_attributes(hash) ⇒ Object
Takes a hash as argument, and applies the values by using writer methods for each key.
-
#update_attributes_without_saving(hash) ⇒ Object
Takes a hash as argument, and applies the values by using writer methods for each key.
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_versions ⇒ Object
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..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..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 |
.database ⇒ Object
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 |
.default ⇒ Object
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
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 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 () return unless () "#{database.root}/#{self.id}/#{}" 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 (args={}) raise ArgumentError unless args[:file] && args[:name] return if (args[:name]) self['_attachments'] ||= {} (args) rescue ArgumentError => e raise ArgumentError, 'You must specify :file and :name' end |
#database ⇒ Object
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 () return unless self['_attachments'] self['_attachments'].delete end |
#destroy ⇒ Object
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
545 546 547 |
# File 'lib/couchrest/core/model.rb', line 545 def () !!(self['_attachments'] && self['_attachments'][] && !self['_attachments'][].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 () Base64.decode64(database.(self.id, )) 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 (args={}) raise ArgumentError unless args[:file] && args[:name] return unless (args[:name]) (args[:name]) (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 |