class DatastaxRails::Base

DatastaxRails

DatastaxRails-based objects differ from Active Record objects in that they specify their attributes directly on the model. This is necessary because of the fact that Cassandra column families do not have a set list of columns but rather can have different columns per row. (This is not strictly true any more, but it's still not as nailed down as SQL.) By specifying the attributes on the model, getters and setters are automatically created, and the attribute is automatically indexed into SOLR.

Primary Keys

Several types of primary keys are supported in DSR. The most common type used is UUID. In general, incrementing numbers are not used as there is no way to guarantee a consistent one-up number across nodes. The following will cause a unique UUID to be generated for each model. This works best if you are using the RandomPartitioner in your Datastax cluster.

class Person < DatastaxRails::Base
  uuid :id
end

You don't have to use a uuid. You can use a different column as your primary key.

class Person < DatastaxRails::Base
  self.primary_key = 'userid'
  string :userid
end

Attributes

Attributes are specified near the top of the model. The following attribute types are supported:

The following options may be specified on the various types to control how they are indexed into SOLR:

NOTES:

EXAMPLE:

class Person < DatastaxRails::Base
  uuid    :id
  string  :first_name
  string  :user_name
  text    :bio
  date    :birthdate
  boolean :active
  timestamps
end

Schemas

DSR will automatically manage both the Cassandra and Solr schemas for you based on the attributes that you specify on the model. You can override the Solr schema if you want to have something custom. There is a rake task that manages all of the schema information. It will create column families and columns as needed and upload the Solr schema when necessary. If there are changes, it will automatically kick off a reindex in the background.

As of Cassandra 1.2, there is no way to remove a column. Cassandra 2.0 supports it, but it hasn't been implemented in DSR yet.

TODO: Need a way to remove ununsed column families.

Creation

DatastaxRails objects accept constructor parameters either in a hash or as a block. The hash method is especially useful when you're receiving the data from somewhere else, like an HTTP request. It works like this:

user = User.new(name: "David", occupation: "Code Artist")
user.name # => "David"

You can also use block initialization:

user = User.new do |u|
  u.name = "David"
  u.occupation = "Code Artist"
end

And of course you can just create a bare object and specify the attributes after the fact:

user = User.new
user.name = "David"
user.occupation = "Code Artist"

Consistency

Cassandra has a concept of consistency levels when it comes to saving records. For a detailed discussion on Cassandra data consistency, see: www.datastax.com/documentation/cassandra/1.2/cassandra/dml/dml_config_consistency_c.html

DatastaxRails allows you to specify the consistency when you save and retrieve objects.

user = User.new(name: 'David')
user.save(consistency: 'ALL')

User.create(params[:user], {consistency: :local_quorum})

User.consistency(:local_quorum).where(name: 'David')

The default consistency level in DatastaxRails is QUORUM for writes and for retrieval by ID. SOLR only supports a consistency level of ONE. See the documentation for SearchMethods#consistency for a more detailed explanation.

The overall default consistency for a given model can be overridden by setting the default_consistency property.

class Model < DatastaxRails::Base
  self.default_consistency = :local_quorum
end

The default consistency for all models can be selected by setting the property on DatastaxRails::Base.

DatastaxRails::Base.default_consistency = :one

Conditions

Conditions are specified as a hash representing key/value pairs that will eventually be passed to SOLR or as a chained call for greater_than and less_than conditions. In addition, fulltext queries may be specified as a string that will eventually be parsed by SOLR as a standard SOLR query.

A simple hash without a statement will generate conditions based on equality using boolean AND logic. For instance:

Student.where(first_name: "Harvey", status: 1)
Student.where(params[:student])

A range may be used in the hash to use a SOLR range query:

Student.where(grade: 9..12)

An array may be used in the hash to construct a SOLR OR query:

Student.where(grade: [9,11,12])

Inequality can be tested for like so:

Student.where_not(grade: 9)
Student.where(:grade).greater_than(9)
Student.where(:grade).less_than(10)

NOTE that Solr inequalities are inclusive so really, the second example above is retrieving records where grace is greater than or equal to 9. Be sure to keep this in mind when you do inequality queries.

Fulltext searching is natively supported. All string and text fields are automatically indexed for fulltext searching.

Post.fulltext('Apple AND "iPhone 4s"')

See the documentation on {DatastaxRails::SearchMethods} for more information and examples.

Overwriting default accessors

All column values are automatically available through basic accessors on the object, but sometimes you want to specialize this behavior. This can be done by overwriting the default accessors (using the same name as the attribute) and calling read_attribute(attr_name) and write_attribute(attr_name, value) to actually change things.

class Song < DatastaxRails::Base
  # Uses an integer of seconds to hold the length of the song

  def length=(minutes)
    write_attribute(:length, minutes.to_i * 60)
  end

  def length
    read_attribute(:length) / 60
  end
end

You can alternatively use self[:attribute]=(value) and self[:attribute] instead of write_attribute(:attribute, value) and read_attribute(:attribute).

Dynamic attribute-based finders

Dynamic finders have been removed from Rails. As a result, they have also been removed from DSR. In its place, the find_by method can be used:

Student.find_by(name: 'Jason')

NOTE: there is a subtle difference between the following that does not exist in ActiveRecord:

Student.find_by(name: 'Jason')
Student.where(name: 'Jason').first

The difference is that the first is escaped so that special characters can be used. The second method requires you to do the escaping yourself if you need it done. As an example,

Company.find_by(name: 'All*') #=> finds only the company with the literal name 'All*'
Company.where(name: 'All*').first #=> finds the first company whose name begins with All

See DatastaxRails::FinderMethods for more information

Facets

DSR support both field and range facets. For additional detail on facets, see the documentation available under the {DatastaxRails::FacetMethods} module. The result is available through the facets accessor.

results = Article.field_facet(:author)
results.facets #=> {"author"=>["vonnegut", 2. "asimov", 3]}

Model.field_facet(:author)
Model.field_facet(:author, sort: 'count', limit: 10, mincount: 1)
Model.range_facet(:price, 500, 1000, 10)
Model.range_facet(:price, 500, 1000, 10, include: 'all')
Model.range_facet(:publication_date, "1968-01-01T00:00:00Z", "2000-01-01T00:00:00Z", "+1YEAR")

Range Gap syntax for dates: +1YEAR, +5YEAR, +5YEARS, +1MONTH, +1DAY

Useful constants:

DatastaxRails::FacetMethods::BY_YEAR (+1YEAR) DatastaxRails::FacetMethods::BY_MONTH (+1MONTH) DatastaxRails::FacetMethods::BY_DAY (+1DAY)

Model.range_facet(:publication_date,

"1968-01-01T00:00:00Z",
"2000-01-01T00:00:00Z",
DatastaxRails::FacetMethods::BY_YEAR)

Collections

Cassandra supports the notion of collections on a row. The three types of supported collections are set, list, and map.

By default collections hold strings. You can override this by passing a :holds option in the attribute definition. Sets can hold anything other than other collections, however, a given collection can only hold a single type of values.

NOTE: There is a limitation in Cassandra where only the first 64k entries of a collection are ever returned with a query. Therefore, if you put more than 64k entries in a collection you will lose data.

Set

A set is an un-ordered collection of unique values. This collection is fully searchable in Solr.

class User < DatastaxRails::Base
  uuid   :id
  string :username
  set    :emails
end

The default set will hold strings. You can modify this behavior like so:

class Student < DatastaxRails::Base
  uuid   :id
  string :name
  set    :grades, holds: :integers
end

User.where(emails: 'jim@example.com') #=> Returns all users where jim@example.com is in the set
user = User.new(name: 'Jim', emails: ['jim@example.com'])
user.emails << 'jim@example.com'
user.emails #=> ['jim@example.com']

List

An ordered collection of values. They do not necessarily have to be unique. The collection will be fully searchable in Solr.

class Student < DatastaxRails::Base
  uuid :id
  string :name
  list :classrooms, holds: integers
end

Student.where(classrooms: 307) #=> Returns all students that have a class in room 307.
student = Student.new(name: 'Sally', classrooms: [307, 305, 301, 307])
student.classrooms << 304
student.classrooms #=> [307, 305, 301, 307, 304]

Map

A collection of key/value pairs where the key is a string and the value is the specified type. The collection becomes available in Solr as dynamic fields.

class Student < DatastaxRails::Base
  uuid :id
  string :name
  map :scores_, holds: :integers
end

student = Student.new(:name 'Sally')
student.scores['midterm'] = 98
student.scores['final'] = 97
student.scores #=> {'scores_midterm' => 98, 'scores_final' => 97}
Student.where(scores_final: 97) #=> Returns all students that scored 97 on their final

Note that the map name gets prepended to the key. This is how Solr maps it's dynamic fields into the cassandra map. For this reason, it's usually a good idea to put an underscore (_) at the end of the map name to prevent collisions.

Exceptions

See the documentation for {DatastaxRails::SearchMethods} for more examples of using the search API.

Attributes

column_family[W]

Sets the column family name

@param [String] column_family the name of the column family in cassandra

attributes[R]
key[RW]
loaded_attributes[R]

Public Class Methods

new(attributes = {}, _options = {}) { |self| ... } click to toggle source
# File lib/datastax_rails/base.rb, line 436
def initialize(attributes = {}, _options = {})
  defaults = self.class.column_defaults.dup
  defaults.each { |_k, v| v.duplicable? ? v.dup : v }

  @attributes = initialize_attributes(defaults)
  @column_types = self.class.columns_hash

  init_internals
  init_changed_attributes
  populate_with_current_scope_attributes

  assign_attributes(attributes) if attributes

  yield self if block_given?
  run_callbacks :initialize unless _initialize_callbacks.empty?
end

Private Class Methods

attribute_names() click to toggle source

Returns an array of attribute names as strings

# File lib/datastax_rails/base.rb, line 621
def attribute_names
  @attribute_names ||= attribute_definitions.keys.map(&:to_s)
end
Also aliased as: column_names
base_class() click to toggle source
# File lib/datastax_rails/base.rb, line 604
def base_class
  klass = self
  klass = klass.superclass while klass.superclass != Base
  klass
end
column_family() click to toggle source

Returns the column family name. If it has been set manually, the set name is returned. Otherwise returns the pluralized version of the class name.

Returns [String] the name of the column family

# File lib/datastax_rails/base.rb, line 588
def column_family
  @column_family || name.underscore.pluralize
end
column_names()
Alias for: attribute_names
columns() click to toggle source
# File lib/datastax_rails/base.rb, line 626
def columns
  @columns ||= attribute_definitions.values
end
compute_type(type_name) click to toggle source

Returns the class type of the record using the current module as a prefix. So descendants of MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.

# File lib/datastax_rails/base.rb, line 678
def compute_type(type_name)
  if type_name.match(/^::/)
    # If the type is prefixed with a scope operator then we assume that
    # the type_name is an absolute reference.
    ActiveSupport::Dependencies.constantize(type_name)
  else
    # Build a list of candidates to search for
    candidates = []
    name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
    candidates << type_name

    candidates.each do |candidate|
      begin
        constant = ActiveSupport::Dependencies.constantize(candidate)
        return constant if candidate == constant.to_s
      rescue NameError => e
        # We don't want to swallow NoMethodError < NameError errors
        raise e unless e.instance_of?(NameError)
      end
    end

    fail NameError, "uninitialized constant #{candidates.first}"
  end
end
construct_finder_relation(options = {}, scope = nil) click to toggle source
# File lib/datastax_rails/base.rb, line 666
def construct_finder_relation(options = {}, scope = nil)
  relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
  relation = scope.merge(relation) if scope
  relation
end
default_page_size() click to toggle source

SOLR always paginates all requests. There is no way to disable it, so we are setting the default page size to an arbitrarily high number so that we effectively remove pagination. If you instead want a model set to something more sane, then override this method in your model and set it. Of course, the page size can always be raised or lowered for an individual request.

class Model < DatastaxRails::Base
  def self.default_page_size
    30
  end
end
# File lib/datastax_rails/base.rb, line 641
def default_page_size
  100_000
end
find_by_id(id) click to toggle source
# File lib/datastax_rails/base.rb, line 610
def find_by_id(id)
  scoped.with_cassandra.find(id)
rescue RecordNotFound
  nil
end
inspect() click to toggle source

Returns a string like 'Post(id:integer, title:string, body:text)'

Calls superclass method
# File lib/datastax_rails/base.rb, line 655
def inspect
  if self == Base
    super
  else
    attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
    "#{super}(#{attr_list})"
  end
end
logger() click to toggle source
# File lib/datastax_rails/base.rb, line 616
def logger
  Rails.logger
end
models() click to toggle source
# File lib/datastax_rails/base.rb, line 592
def models
  descendants.reject(&:abstract_class?)
end
payload_model?() click to toggle source
# File lib/datastax_rails/base.rb, line 596
def payload_model?
  ancestors.include?(DatastaxRails::PayloadModel)
end
search_ids(&block) click to toggle source
# File lib/datastax_rails/base.rb, line 645
def search_ids(&block)
  search = solr_search(&block)
  search.raw_results.map(&:primary_key)
end
wide_storage_model?() click to toggle source
# File lib/datastax_rails/base.rb, line 600
def wide_storage_model?
  ancestors.include?(DatastaxRails::WideStorageModel)
end

Public Instance Methods

==(other) click to toggle source
Calls superclass method
# File lib/datastax_rails/base.rb, line 538
def ==(other)
  super ||
    other.instance_of?(self.class) &&
      id.present? &&
      other.id.eql?(id)
end
attribute_names() click to toggle source
# File lib/datastax_rails/base.rb, line 549
def attribute_names
  self.class.attribute_names
end
Also aliased as: column_names
column_names()
Alias for: attribute_names
eql?(other) click to toggle source
# File lib/datastax_rails/base.rb, line 545
def eql?(other)
  self == (other)
end
freeze() click to toggle source

Freeze the attributes hash such that associations are still accessible, even on destroyed records.

# File lib/datastax_rails/base.rb, line 504
def freeze
  @attributes.freeze
  self
end
frozen?() click to toggle source

Returns true if the attributes hash has been frozen.

# File lib/datastax_rails/base.rb, line 510
def frozen?
  @attributes.frozen?
end
hash() click to toggle source
# File lib/datastax_rails/base.rb, line 534
def hash
  id.hash
end
init_changed_attributes() click to toggle source
# File lib/datastax_rails/base.rb, line 494
def init_changed_attributes
  # Intentionally avoid using #column_defaults since overridden defaults
  # won't get written unless they get marked as changed
  self.class.columns.each do |c|
    attr, orig_value = c.name, c.empty_value
    @changed_attributes[attr] = nil if _field_changed?(attr, orig_value, @attributes[attr.to_s])
  end
end
init_internals() click to toggle source
# File lib/datastax_rails/base.rb, line 478
def init_internals
  pk = self.class.primary_key
  @attributes[pk] = nil unless @attributes.key?(pk)

  @association_cache = {}
  @attributes_cache = {}
  @previously_changed = {}.with_indifferent_access
  @changed_attributes = {}.with_indifferent_access
  @loaded_attributes = Hash[@attributes.map { |k, _v| [k, true] }].with_indifferent_access
  @readonly = false
  @destroyed = false
  @marked_for_destruction = false
  @destroyed_by_association = nil
  @new_record = true
end
init_with(coder) click to toggle source

Initialize an empty model object from coder. coder must contain the attributes necessary for initializing an empty model object. For example:

class Post < DatastaxRails::Base
end

post = Post.allocate
post.init_with('attributes' => { 'title' => 'hello world' })
post.title # => 'hello world'
# File lib/datastax_rails/base.rb, line 463
def init_with(coder)
  Types::DirtyCollection.ignore_modifications do
    @attributes   = initialize_attributes(coder['attributes'])
    @column_types_override = coder['column_types']
    @column_types = self.class.columns_hash

    init_internals

    @new_record = false
    run_callbacks :find
    run_callbacks :initialize
  end
  self
end
inspect() click to toggle source

Returns the contents of the record as a nicely formatted string.

# File lib/datastax_rails/base.rb, line 515
def inspect
  # We check defined?(@attributes) not to issue warnings if the object is
  # allocated but not initialized.
  inspection = if defined?(@attributes) && @attributes
                 self.class.column_names.map do |name|
                   if has_attribute?(name)
                     "#{name}: #{attribute_for_inspect(name)}"
                   end
                 end.compact.join(', ')
               else
                 'not initialized'
               end
  "#<#{self.class} #{inspection}>"
end
to_param() click to toggle source
# File lib/datastax_rails/base.rb, line 530
def to_param
  id.to_s if persisted?
end

Private Instance Methods

populate_with_current_scope_attributes() click to toggle source
# File lib/datastax_rails/base.rb, line 560
def populate_with_current_scope_attributes
  return unless self.class.scope_attributes?

  self.class.scope_attributes.each do |att, value|
    send("#{att}=", value) if respond_to?("#{att}=")
  end
end