/couchbase-orm

A Couchbase ORM based on ActiveModel for Rails compatibility

Primary LanguageRubyOtherNOASSERTION

Couchbase ORM for Rails

Build Status

Rails integration

To generate config you can use rails generate couchbase_orm:config:

$ rails generate couchbase_orm:config dev_bucket dev_user dev_password
  => create  config/couchbase.yml

It will generate this config/couchbase.yml for you:

    common: &common
      connection_string: couchbase://localhost
      username: dev_user
      password: dev_password

    development:
      <<: *common
      bucket: dev_bucket

    test:
      <<: *common
      bucket: dev_bucket_test

    # set these environment variables on your production server
    production:
      connection_string: <%= ENV['COUCHBASE_CONNECTION_STRING'] %>
      bucket: <%= ENV['COUCHBASE_BUCKET'] %>
      username: <%= ENV['COUCHBASE_USER'] %>
      password: <%= ENV['COUCHBASE_PASSWORD'] %>

Setup without Rails

If you are not using Rails, you can configure couchbase-orm with an initializer:

# config/initializers/couchbase_orm.rb
CouchbaseOrm::Connection.config = {
  connection_string: "couchbase://localhost"
  username: "dev_user"
  password: "dev_password"
  bucket: "dev_bucket"
}

Views are generated on application load if they don't exist or mismatch. This works fine in production however by default in development models are lazy loaded.

# config/environments/development.rb
config.eager_load = true

Examples

    require 'couchbase-orm'

    class Post < CouchbaseOrm::Base
      attribute :title, :string
      attribute :body,  :string
      attribute :draft, :boolean
    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')  #=> {"title"=>"Hello world", "draft"=>false,
                                    #    "body"=>"Once upon the times...."}

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        #=> "post-abcDE34"

Typing

The following types have been tested :

You can register other types in ActiveModel registry :

    class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime
      def serialize(value)
        value&.iso8601(3)
      end
    end

    ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal)

Validations

There are all methods from ActiveModel::Validations accessible in context of rails application. You can also enforce types using ruby conversion methods

    class Comment < Couchbase::Model
      attribute :author, :string
      attribute :body, :string

      validates_presence_of :author, :body
    end

Views (aka Map/Reduce indexes)

Views are defined in the model and typically just emit an attribute that can then be used for filtering results or ordering.

    class Comment < CouchbaseOrm::Base
      attribute :author :string
      attribute :body, :string
      view :all # => emits :id and will return all comments
      view :by_author, emit_key: :author

      # Generates two functions:
      # * the by_author view above
      # * def find_by_author(author); end
      index_view :author

      # You can make compound keys by passing an array to :emit_key
      # this allow to query by read/unread comments
      view :by_read, emit_key: [:user_id, :read]
      # this allow to query by view_count
      view :by_view_count, emit_key: [:user_id, :view_count]

      validates_presence_of :author, :body
    end

You can use Comment.find_by_author('name') to obtain all the comments by a particular author. The same thing, using the view directly would be: Comment.by_author(key: 'name')

When using a compound key, the usage is the same, you just give the full key :

   Comment.by_read(key: '["'+user_id+'",false]') # gives all unread comments for one particular user

   # or even a range !

   Comment.by_view_count(startkey: '["'+user_id+'",10]', endkey: '["'+user_id+'",20]') 
   
   # gives all comments that have been seen more than 10 times but less than 20

Check this couchbase help page to learn more on what's possible with compound keys : https://developer.couchbase.com/documentation/server/3.x/admin/Views/views-translateSQL.html

Ex : Compound keys allows to decide the order of the results, and you can reverse it by passing descending: true

    class Comment < CouchbaseOrm::Base19
      self.ignored_properties = [:old_name] # ignore old_name property in the model
      self.properties_always_exists_in_document = true # use is null for nil value instead of not valued for performance purpose, only possible if all properties always exists in document
    end

You can specify properties_always_exists_in_document to true if all properties always exists in document, this will allow to use is null instead of not valued for nil value, this will improve performance.

WARNING: If a document exists without a property, the query will failed! So you must be sure that all documents have all properties.

N1ql

Like views, it's possible to use N1QL to process some requests used for filtering results or ordering.

    class Comment < CouchbaseOrm::Base
      attribute :author, :string
      attribute :body, :string
      n1ql :by_author, emit_key: :author

      # Generates two functions:
      # * the by_author view above
      # * def find_by_author(author); end
      index_n1ql :author

      # You can make compound keys by passing an array to :emit_key
      # this allow to query by read/unread comments
      n1ql :by_read, emit_key: [:user_id, :read]
      # this allow to query by view_count
      n1ql :by_view_count, emit_key: [:user_id, :view_count]

      validates_presence_of :author, :body
    end

Basic Active Record like query engine

class Comment < CouchbaseOrm::Base
      attribute :title, :string
      attribute :author, :string
      attribute :category, :string
      attribute :ratings, :number
end

Comment.where(author: "Anne McCaffrey", category: ['S-F', 'Fantasy']).not(ratings: 0).order(:title).limit(10)

# Relation can be composed as in AR:

amc_comments = Comment.where(author: "Anne McCaffrey")

amc_comments.count

amc_sf_comments = amc_comments.where(category: 'S-F')

# pluck is available, but will query all object fields first

Comment.pluck(:title, :ratings)

# To load the ids without loading the models

Comment.where(author: "David Eddings").ids

# To delete all the models of a relation

Comment.where(ratings: 0).delete_all

scopes

Scopes can be written as class method, scope method is not implemented yet. They can be chained as in AR or mixed with relation methods.

class Comment < CouchbaseOrm::Base
      attribute :title, :string
      attribute :author, :string
      attribute :category, :string
      attribute :ratings, :number

      def self.by_author(author)
        where(author: author)
      end
end

Comment.by_author("Anne McCaffrey").where(category: 'S-F').not(ratings: 0).order(:title).limit(10)

Operators

Several operators are available to filter numerical results : _gt, _lt, _gte, _lte, _ne

Comment.where(ratings: {_gt: 3})

Range in the where

You can specify a Range of date or intger in the where clause

Person.where(birth_date: DateTime.new(1980, 1, 1)..DateTime.new(1990, 1, 1))
Person.where(age: 10..20)

Person.where(age: 10...20) # to exclude the upper bound

Associations and Indexes

There are common active record helpers available for use belongs_to and has_many

    class Comment < CouchbaseOrm::Base
        belongs_to :author
    end

    class Author < CouchbaseOrm::Base
        has_many :comments, dependent: :destroy

        # You can ensure an attribute is unique for this model
        attribute :email, :string
        ensure_unique :email
    end

By default, has_many uses a view for association, but you can define a type option to specify an association using N1QL instead:

class Comment < CouchbaseOrm::Base
    belongs_to :author
end

class Author < CouchbaseOrm::Base
    has_many :comments, type: :n1ql, dependent: :destroy
end

Nested

Attributes can be of type nested, they must specify a type of NestedDocument. The NestedValidation triggers nested validation on parent validation.

    class Address < CouchbaseOrm::NestedDocument
      attribute :road, :string
      attribute :city, :string
      validates :road, :city, presence: true
    end

    class Author < CouchbaseOrm::Base
        attribute :address, :nested, type: Address
        validates :address, nested: true
    end

Model can be queried using the nested attributes

    Author.where(address: {road: '1 rue de la paix', city: 'Paris'})

Array

Attributes can be of type array, they must contain something that can be serialized and deserialized to/from JSON. You can enforce the type of array elements. The type can be a NestedDocument

    class Book < CouchbaseOrm::NestedDocument
      attribute :name, :string
      validates :name, presence: true
    end 

    class Author < CouchbaseOrm::Base
        attribute things, :array
        attribute flags, :array, type: :string
        attribute books, :array, type: Book

        validates :books, nested: true
    end

Performance Comparison with Couchbase-Ruby-Model

Basically we migrated an application from Couchbase Ruby Model to Couchbase-ORM (this project)

  • Rails 5 production
  • Puma as the webserver
  • Running on a 2015 Macbook Pro
  • Performance test: siege -c250 -r10 http://localhost:3000/auth/authority

The request above pulls the same database document each time and returns it. A simple O(1) operation.

Stat Couchbase Ruby Model Couchbase-ORM
Transactions 2500 hits 2500 hits
Elapsed time 12.24 secs 6.82 secs
Response time 0.88 secs 0.34 secs
Transaction rate 204.25 trans/sec 366.57 trans/sec
Request Code ruby-model-app couch-orm-app