Class: Couchbase::Model

Inherits:
Object show all
Extended by:
ActiveModel::Callbacks, ActiveModel::Naming
Includes:
ActiveModel::Conversion, ActiveModel::Validations
Defined in:
lib/couchbase/model.rb,
lib/couchbase/model/uuid.rb,
lib/couchbase/model/version.rb,
lib/couchbase/model/configuration.rb

Overview

Declarative layer for Couchbase gem

You can also let the library generate the unique identifier for you:

p = Post.create(:title => 'How to generate ID',
                :body => 'Open up the editor...')
p.id        #=> "74f43c3116e788d09853226603000809"

There are several algorithms available. By default it use `:sequential` algorithm, but you can change it to more suitable one for you:

class Post < Couchbase::Model
  attribute :title
  attribute :body
  attribute :draft

  uuid_algorithm :random
end

You can define connection options on per model basis:

class Post < Couchbase::Model
  attribute :title
  attribute :body
  attribute :draft

  connect :port => 80, :bucket => 'blog'
end

Since:

Defined Under Namespace

Modules: Configuration Classes: UUID

Constant Summary

VERSION =
'0.5.3'

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Model) initialize(attrs = {})

Constructor for all subclasses of Couchbase::Model

Optionally takes a Hash of attribute value pairs.

Since:

  • 0.0.1



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/couchbase/model.rb', line 472

def initialize(attrs = {})
  @errors = ::ActiveModel::Errors.new(self) if defined?(::ActiveModel)
  @_attributes = ::Hash.new do |h, k|
    default = self.class.attributes[k]
    h[k] = if default.respond_to?(:call)
             default.call
           else
             default
           end
  end
  case attrs
  when Hash
    if defined?(HashWithIndifferentAccess) && !attrs.is_a?(HashWithIndifferentAccess)
      attrs = attrs.with_indifferent_access
    end
    @id = attrs.delete(:id)
    @key = attrs.delete(:key)
    @value = attrs.delete(:value)
    @doc = attrs.delete(:doc)
    @meta = attrs.delete(:meta)
    @raw = attrs.delete(:raw)
    update_attributes(@doc || attrs)
  else
    @raw = attrs
  end
end

Instance Attribute Details

- (Object) doc (readonly)

Since:

  • 0.2.0



115
116
117
# File 'lib/couchbase/model.rb', line 115

def doc
  @doc
end

- (Object) errors (readonly)

Since:

  • 0.4.5



121
122
123
# File 'lib/couchbase/model.rb', line 121

def errors
  @errors
end

- (Object) id

Each model must have identifier

Since:

  • 0.0.1



106
107
108
# File 'lib/couchbase/model.rb', line 106

def id
  @id
end

- (Object) key (readonly)

Since:

  • 0.2.0



109
110
111
# File 'lib/couchbase/model.rb', line 109

def key
  @key
end

- (Object) meta (readonly)

Since:

  • 0.2.0



118
119
120
# File 'lib/couchbase/model.rb', line 118

def meta
  @meta
end

- (Object) raw (readonly)

Since:

  • 0.4.5



124
125
126
# File 'lib/couchbase/model.rb', line 124

def raw
  @raw
end

- (Object) value (readonly)

Since:

  • 0.2.0



112
113
114
# File 'lib/couchbase/model.rb', line 112

def value
  @value
end

Class Method Details

+ (Object) attribute(*names)

Defines an attribute for the model

Examples:

Define some attributes for a model

class Post < Couchbase::Model
  attribute :title
  attribute :body
  attribute :published_at
end

post = Post.new(:title => 'Hello world',
                :body => 'This is the first example...',
                :published_at => Time.now)

Since:

  • 0.0.1



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/couchbase/model.rb', line 312

def self.attribute(*names)
  options = {}
  if names.last.is_a?(Hash)
    options = names.pop
  end
  names.each do |name|
    name = name.to_sym
    attributes[name] = options[:default]
    next if self.instance_methods.include?(name)
    define_method(name) do
      read_attribute(name)
    end
    define_method(:#{name}=") do |value|
      write_attribute(name, value)
    end
  end
end

+ (Hash) attributes

All defined attributes within a class.

See Also:

Since:

  • 0.0.1



660
661
662
663
664
665
666
# File 'lib/couchbase/model.rb', line 660

def self.attributes
  @attributes ||= if self == Model
                    @@attributes.dup
                  else
                    couchbase_ancestor.attributes.dup
                  end
end

+ (Object) belongs_to(name, options = {})

Defines a belongs_to association for the model

Examples:

Define some association for a model

class Brewery < Couchbase::Model
  attribute :name
end

class Beer < Couchbase::Model
  attribute :name, :brewery_id
  belongs_to :brewery
end

Beer.find("heineken").brewery.name

Options Hash (options):

  • :class_name (String, Symbol)

    the name of the association class

Since:

  • 0.3.0



383
384
385
386
387
388
389
390
# File 'lib/couchbase/model.rb', line 383

def self.belongs_to(name, options = {})
  ref = "#{name}_id"
  attribute(ref)
  assoc = name.to_s.camelize.constantize
  define_method(name) do
    assoc.find(self.send(ref))
  end
end

+ (Couchbase::Bucket) connect(*options)

Use custom connection options

Examples:

Choose specific bucket

class Post < Couchbase::Model
  connect :bucket => 'posts'
  ...
end

See Also:

  • Bucket#initialize

Since:

  • 0.0.1



147
148
149
# File 'lib/couchbase/model.rb', line 147

def self.connect(*options)
  self.bucket = Couchbase.connect(*options)
end

+ (Object) couchbase_ancestor

Returns the first ancestor that is also a Couchbase::Model ancestor.

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


686
687
688
689
690
# File 'lib/couchbase/model.rb', line 686

def self.couchbase_ancestor
  ancestors[1..-1].each do |ancestor|
    return ancestor if ancestor.ancestors.include?(Couchbase::Model)
  end
end

+ (Couchbase::Model, false) create(*args)

Create the model with given attributes

Since:

  • 0.0.1



452
453
454
# File 'lib/couchbase/model.rb', line 452

def self.create(*args)
  new(*args).create
end

+ (Object) create!(*args)

Creates an object just like {Model{Model.create but raises an exception if the record is invalid.

Raises:

  • (Couchbase::Error::RecordInvalid)

    if the instance is invalid

Since:

  • 0.5.1



461
462
463
# File 'lib/couchbase/model.rb', line 461

def self.create!(*args)
  new(*args).create!
end

+ (Object) defaults(options = nil)

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


185
186
187
188
189
190
191
# File 'lib/couchbase/model.rb', line 185

def self.defaults(options = nil)
  if options
    @_defaults = options
  else
    @_defaults || {}
  end
end

+ (String) design_document(name = nil)

Associate custom design document with the model

Design document is the special document which contains views, the chunks of code for building map/reduce indexes. When this method called without argument, it just returns the effective design document name.

Examples:

Choose specific design document name

class Post < Couchbase::Model
  design_document :my_posts
  ...
end

See Also:

Since:

  • 0.1.0



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

def self.design_document(name = nil)
  if name
    @_design_doc = name.to_s
  else
    @_design_doc ||= begin
                       name = self.name.dup
                       name.gsub!(/::/, '_')
                       name.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
                       name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
                       name.downcase!
                     end
  end
end

+ (Object) ensure_design_document!

Ensure that design document is up to date.

This method also cares about organizing view in separate javascript files. The general structure is the following (+[root]+ is the directory, one of the Model::Configuration.design_documents_paths):

[root]
|
`- link
|  |
|  `- by_created_at
|  |  |
|  |  `- map.js
|  |
|  `- by_session_id
|  |  |
|  |  `- map.js
|  |
|  `- total_views
|  |  |
|  |  `- map.js
|  |  |
|  |  `- reduce.js

The directory structure above demonstrate layout for design document with id _design/link and three views: by_create_at, +by_session_id` and `total_views`.

Since:

  • 0.1.0



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/couchbase/model.rb', line 222

def self.ensure_design_document!
  unless Configuration.design_documents_paths
    raise 'Configuration.design_documents_path must be directory'
  end

  doc = {'_id' => "_design/#{design_document}", 'views' => {}}
  digest = Digest::MD5.new
  mtime = 0
  views.each do |name, _|
    doc['views'][name] = {}
    doc['spatial'] = {}
    ['map', 'reduce', 'spatial'].each do |type|
      Configuration.design_documents_paths.each do |path|
        ff = File.join(path, design_document.to_s, name.to_s, "#{type}.js")
        if File.file?(ff)
          contents = File.read(ff).gsub(/^\s*\/\/.*$\n\r?/, '').strip
          next if contents.empty?
          mtime = [mtime, File.mtime(ff).to_i].max
          digest << contents
          case type
          when 'map', 'reduce'
            doc['views'][name][type] = contents
          when 'spatial'
            doc['spatial'][name] = contents
          end
          break # pick first matching file
        end
      end
    end
  end

  doc['views'].delete_if {|_, v| v.empty? }
  doc.delete('spatial') if doc['spatial'] && doc['spatial'].empty?
  doc['signature'] = digest.to_s
  doc['timestamp'] = mtime
  if doc['signature'] != thread_storage[:signature] && doc['timestamp'] > thread_storage[:timestamp].to_i
    current_doc = bucket.design_docs[design_document.to_s]
    if current_doc.nil? || (current_doc['signature'] != doc['signature'] && doc['timestamp'] > current_doc[:timestamp].to_i)
      bucket.save_design_doc(doc)
      current_doc = doc
    end
    thread_storage[:signature] = current_doc['signature']
    thread_storage[:timestamp] = current_doc['timestamp'].to_i
  end
end

+ (true, false) exists?(id)

Check if the key exists in the bucket

Since:

  • 0.0.1



639
640
641
# File 'lib/couchbase/model.rb', line 639

def self.exists?(id)
  !!bucket.get(id, :quiet => true)
end

+ (Couchbase::Model, Array) find(*id)

Find the model using id attribute

Examples:

Find model using id

post = Post.find('the-id')

Find multiple models using id

post = Post.find('one', 'two')

Raises:

  • (Couchbase::Error::NotFound)

    when given key isn't exist

Since:

  • 0.0.1



421
422
423
# File 'lib/couchbase/model.rb', line 421

def self.find(*id)
  _find(false, *id)
end

+ (Couchbase::Model, ...) find_by_id(*id)

Find the model using id attribute

Unlike find, this method won't raise Error::NotFound error when key doesn't exist in the bucket

Examples:

Find model using id

post = Post.find_by_id('the-id')

Find multiple models using id

posts = Post.find_by_id(['the-id', 'the-id2'])

Since:

  • 0.1.0



442
443
444
# File 'lib/couchbase/model.rb', line 442

def self.find_by_id(*id)
  _find(true, *id)
end

+ (Object) inspect

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


811
812
813
814
815
816
817
# File 'lib/couchbase/model.rb', line 811

def self.inspect
  buf = "#{name}"
  if self != Couchbase::Model
    buf << "(#{['id', attributes.map(&:first)].flatten.join(', ')})"
  end
  buf
end

+ (Symbol) uuid_algorithm(algorithm)

Choose the UUID generation algorithms

Examples:

Select :random UUID generation algorithm

class Post < Couchbase::Model
  uuid_algorithm :random
  ...
end

See Also:

  • UUID#next

Since:

  • 0.0.1



284
285
286
# File 'lib/couchbase/model.rb', line 284

def self.uuid_algorithm(algorithm)
  self.thread_storage[:uuid_algorithm] = algorithm
end

+ (Object) view(*names)

Defines a view for the model

Examples:

Define some views for a model

class Post < Couchbase::Model
  view :all, :published
  view :by_rating, :include_docs => false
end

post = Post.find("hello")
post.by_rating.each do |r|
  # ...
end

Since:

  • 0.0.1



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/couchbase/model.rb', line 347

def self.view(*names)
  options = {:wrapper_class => self, :include_docs => true}
  if names.last.is_a?(Hash)
    options.update(names.pop)
  end
  is_spatial = options.delete(:spatial)
  names.each do |name|
    path = '_design/%s/_%s/%s' % [design_document, is_spatial ? 'spatial' : 'view', name]
    views[name] = lambda do |*params|
      params = options.merge(params.first || {})
      View.new(bucket, path, params)
    end
    singleton_class.send(:define_method, name, &views[name])
  end
end

+ (Array) views

All defined views within a class.

See Also:

Since:

  • 0.1.0



675
676
677
678
679
680
681
# File 'lib/couchbase/model.rb', line 675

def self.views
  @views ||= if self == Model
               @@views.dup
             else
               couchbase_ancestor.views.dup
             end
end

Instance Method Details

- (Hash) as_json(options = {})

Format the model for use in a JSON response

Since:

  • 0.5.2



737
738
739
# File 'lib/couchbase/model.rb', line 737

def as_json(options = {})
  attributes.merge({:id => @id}).as_json(options)
end

- (Hash) attributes

All the attributes of the current instance

Since:

  • 0.0.1



697
698
699
# File 'lib/couchbase/model.rb', line 697

def attributes
  @_attributes
end

- (Couchbase::Model, false) create(options = {})

Create this model and assign new id if necessary

Examples:

Create the instance of the Post model

p = Post.new(:title => 'Hello world', :draft => true)
p.create

Raises:

  • (Couchbase::Error::KeyExists)

    if model with the same id exists in the bucket

Since:

  • 0.0.1



511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/couchbase/model.rb', line 511

def create(options = {})
  @id ||= Couchbase::Model::UUID.generator.next(1, model.thread_storage[:uuid_algorithm])
  if respond_to?(:valid?) && !valid?
    return false
  end
  options = model.defaults.merge(options)
  value = (options[:format] == :plain) ?  @raw : attributes_with_values
  unless @meta
    @meta = {}
    if @meta.respond_to?(:with_indifferent_access)
      @meta = @meta.with_indifferent_access
    end
  end
  @meta['cas'] = model.bucket.add(@id, value, options)
  self
end

- (Object) create!(options = {})

Creates an object just like Couchbase::Model.{Model{Model#create but raises an exception if the record is invalid.

Raises:

  • (Couchbase::Error::RecordInvalid)

    if the instance is invalid

Since:

  • 0.5.1



534
535
536
# File 'lib/couchbase/model.rb', line 534

def create!(options = {})
  create(options) || raise(Couchbase::Error::RecordInvalid.new(self))
end

- (Couchbase::Model) delete(options = {})

Note:

This method will reset id attribute

Delete this object from the bucket

Examples:

Delete the Post model

p = Post.find('hello-world')
p.delete

Raises:

  • (Couchbase::Error::MissingId)

Since:

  • 0.0.1



606
607
608
609
610
611
612
# File 'lib/couchbase/model.rb', line 606

def delete(options = {})
  raise Couchbase::Error::MissingId, 'missing id attribute' unless @id
  model.bucket.delete(@id, options)
  @id = nil
  @meta = nil
  self
end

- (true, false) exists?

Check if this model exists in the bucket.

Since:

  • 0.0.1



649
650
651
# File 'lib/couchbase/model.rb', line 649

def exists?
  model.exists?(@id)
end

- (true, false) new?

Note:

true doesn't mean that record exists in the database

Check if the record have id attribute

See Also:

Since:

  • 0.0.1



623
624
625
# File 'lib/couchbase/model.rb', line 623

def new?
  !@id
end

- (true, false) persisted?

Returns Where on on this object persisted in the storage

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


628
629
630
# File 'lib/couchbase/model.rb', line 628

def persisted?
  !!@id
end

- (Object) read_attribute(attr_name)

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


288
289
290
# File 'lib/couchbase/model.rb', line 288

def read_attribute(attr_name)
  @_attributes[attr_name]
end

- (Model) reload

Reload all the model attributes from the bucket

Raises:

Since:

  • 0.0.1



724
725
726
727
728
729
# File 'lib/couchbase/model.rb', line 724

def reload
  raise Couchbase::Error::MissingId, 'missing id attribute' unless @id
  attrs = model.find(@id).attributes
  update_attributes(attrs)
  self
end

- (Couchbase::Model, false) save(options = {})

Create or update this object based on the state of #new?.

Examples:

Update the Post model

p = Post.find('hello-world')
p.draft = false
p.save

Use CAS value for optimistic lock

p = Post.find('hello-world')
p.draft = false
p.save('cas' => p.meta['cas'])

Since:

  • 0.0.1



558
559
560
561
562
563
564
565
566
567
# File 'lib/couchbase/model.rb', line 558

def save(options = {})
  return create(options) unless @meta
  if respond_to?(:valid?) && !valid?
    return false
  end
  options = model.defaults.merge(options)
  value = (options[:format] == :plain) ?  @raw : attributes_with_values
  @meta['cas'] = model.bucket.replace(@id, value, options)
  self
end

- (Object) save!(options = {})

Creates an object just like Couchbase::Model.{Model{Model#save but raises an exception if the record is invalid.

Raises:

  • (Couchbase::Error::RecordInvalid)

    if the instance is invalid

Since:

  • 0.5.1



575
576
577
# File 'lib/couchbase/model.rb', line 575

def save!(options = {})
  save(options) || raise(Couchbase::Error::RecordInvalid.new(self))
end

- (Object) to_key

Redefine (if exists) #to_key to use #key if #id is missing

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


852
853
854
855
# File 'lib/couchbase/model.rb', line 852

def to_key
  keys = [id || key]
  keys.empty? ? nil : keys
end

- (Object) to_param

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


857
858
859
860
861
862
# File 'lib/couchbase/model.rb', line 857

def to_param
  keys = to_key
  if keys && !keys.empty?
    keys.join('-')
  end
end

- (Couchbase::Model) update(attrs, options = {})

Update this object, optionally accepting new attributes.

Since:

  • 0.0.1



588
589
590
591
# File 'lib/couchbase/model.rb', line 588

def update(attrs, options = {})
  update_attributes(attrs)
  save(options)
end

- (Object) update_attributes(attrs)

Update all attributes without persisting the changes.

Since:

  • 0.0.1



706
707
708
709
710
711
712
713
714
# File 'lib/couchbase/model.rb', line 706

def update_attributes(attrs)
  if id = attrs.delete(:id)
    @id = id
  end
  attrs.each do |key, value|
    setter = :#{key}="
    send(setter, value) if respond_to?(setter)
  end
end

- (Object) write_attribute(attr_name, value)

Since:

  • 0.0.1

    require 'couchbase/model'

    class Post < Couchbase::Model

    attribute :title
    attribute :body
    attribute :draft

    end

    p = Post.new(:id => 'hello-world',

    :title => 'Hello world',
    :draft => true)

    p.save p = Post.find('hello-world') p.body = “Once upon the times.…” p.save p.update(:draft => false) Post.bucket.get('hello-world') #=> world”, “draft”=>false,

    #    "body"=>"Once upon the times...."


292
293
294
# File 'lib/couchbase/model.rb', line 292

def write_attribute(attr_name, value)
  @_attributes[attr_name] = value
end