class Poro::Contexts::MongoContext

The MongoDB Context Adapter.

Manages an object in MongoDB.

WARNING: At this time, only objects that follow nice tree hierarchies can be encoded. Cyclical loops cannot be auto-encoded, and need embedded objects to be managed with the parent pointers blacklisted.

WARNING: Embedded objects of the same kind–which are referenced via a DBRef, are re-fetched and re-saved every time the managing object is fetched or saved.

This adapter recursively encodes the object according to the following rules for each instance variable’s value:

  1. If the object can be saved as a primitive, save it that way.

  2. If the object is managed by a Mongo context, save and encode it as a DBRef.

  3. If the object is managed by another context, save and store the class and id in a hash.

  4. Otherwise, encode all instance variables and the class, in a Hash.

For Mongo represented objects, the instance variables that are encoded can be controlled via any combination of the save_attributes and save_attributes_blacklist properties. The Context will start with the save attributes (which defaults to all the instance variables), and then subtract out the attributes in the blacklist. Thus the blacklist takes priority.

Attributes

attempt_id_conversion[RW]

Normally, one uses BSON::ObjectId instances for the IDs on a stored Mongo object. However, one can design a database to use many different values as sthe primary key.

When this is set to true, fetch tries to convert the given ID into a BSON::ObjectId before doing a fetch. If the conversion fails, it tries using the raw value given. If this is set to false, then it always passes along the raw value, skipping the conversion step.

encode_symbols[RW]

If true, it encodes Symbol as a hash with a class name property, and then decodes them back into Symbol. If false, it converts them to String for storage. Defaults to false.

While one would think they want to preserve the type of the saved element, the change in storage method makes it harder to write Mongo queries on the data. Thus it is normally best to save symbols as strings for storage.

persistent_attributes_blacklist[RW]
persistent_attributes_whitelist[RW]

Public Class Methods

new(klass) click to toggle source

Takes the class for the context, and optionally the collection object up front. This can be changed at any time by setting the data store for the Context.

Calls superclass method Poro::Context::new
# File lib/poro/contexts/mongo_context.rb, line 38
def initialize(klass)
  # Require mongo.  We do it here so that it is only required when
  # we use this.  (How does this affect speed?  It seems this takes 1/30000 of a second.)
  require 'mongo'
  
  # Set-up the lists.
  @persistent_attributes_whitelist = nil
  @persistent_attributes_blacklist = nil
  
  # Some configuration variables
  @encode_symbols = false
  @attempt_id_conversion = true
  @set_encoding_method = :embedded_array
  
  
  # Initialize
  super(klass)
end

Public Instance Methods

convert_to_data(obj, state_info={}) click to toggle source
# File lib/poro/contexts/mongo_context.rb, line 126
def convert_to_data(obj, state_info={})
  transformed_obj = callback_transform(:before_convert_to_data, obj)
  
  data = route_encode(transformed_obj, state_info)
  
  callback_event(:after_convert_to_data, data)
  return data
end
convert_to_plain_object(data, state_info={}) click to toggle source
# File lib/poro/contexts/mongo_context.rb, line 115
def convert_to_plain_object(data, state_info={})
  transformed_data = callback_transform(:before_convert_to_plain_object, data)
  
  # If it is a root record, and it has no class name, assume this context's class name.
  transformed_data['_class_name'] = self.klass if( transformed_data && transformed_data.kind_of?(Hash) && !state_info[:embedded] )
  obj = route_decode(transformed_data, state_info)
  
  callback_event(:after_convert_to_plain_object, obj)
  return obj
end
data_store=(collection) click to toggle source

Set the data store to the given collection.

Calls superclass method
# File lib/poro/contexts/mongo_context.rb, line 58
def data_store=(collection)
  @@collection_map.delete(self.data_store && self.data_store.name) # Clean-up the old record in case we change names.
  @@collection_map[collection.name] = self unless collection.nil? # Create the new record.
  super(collection)
end
fetch(id) click to toggle source
# File lib/poro/contexts/mongo_context.rb, line 91
def fetch(id)
  return nil if id.nil? #find_one(nil) returns the first object!  We don't want this.
  data = data_store.find_one( clean_id(id) )
  obj = convert_to_plain_object(data)
  callback_event(:after_fetch, obj)
  return obj
end
remove(obj) click to toggle source
# File lib/poro/contexts/mongo_context.rb, line 108
def remove(obj)
  callback_event(:before_remove, obj)
  data_store.remove( {'_id' => primary_key_value(obj)} )
  callback_event(:after_remove, obj)
  return obj
end
save(obj) click to toggle source
# File lib/poro/contexts/mongo_context.rb, line 99
def save(obj)
  callback_event(:before_save, obj)
  data = convert_to_data(obj)
  data_store.save(data)
  set_primary_key_value(obj, (data['_id'] || data[:_id])) # The pk generator uses a symbol, while everything else uses a string!
  callback_event(:after_save, obj)
  return obj
end

Private Instance Methods

clean_id(id) click to toggle source
PRIVATE ===============================
# File lib/poro/contexts/mongo_context.rb, line 138
def clean_id(id)
  # Attempt to convert to an ObjectID if it looks like it should be.
  if( self.attempt_id_conversion && !(id.kind_of?(BSON::ObjectId)) && BSON::ObjectId.legal?(id.to_s) )
    id = BSON::ObjectId.from_string(id.to_s)
  end
  return id
end
decode_array(array) click to toggle source

Decode an array, recursing through its elements.

# File lib/poro/contexts/mongo_context.rb, line 391
def decode_array(array)
  array.map {|o| self.convert_to_plain_object(o, :embedded => true)}
end
decode_bigint(bigint_data) click to toggle source

Decode an encoded bigint.

# File lib/poro/contexts/mongo_context.rb, line 412
def decode_bigint(bigint_data)
  return bigint_data['value'].to_i
end
decode_class(class_data) click to toggle source

Decode a class reference.

# File lib/poro/contexts/mongo_context.rb, line 396
def decode_class(class_data)
  return Util::ModuleFinder.find(class_data['name'])
end
decode_db_ref(dbref) click to toggle source

Decode a BSON::DBRef. If there is a context for the reference, it is wrapped in that object type. If there is no context, it is left as a DBRef so that it will re-save as a DBRef (otherwise it’ll save as a hash of that document!)

Note that one would think we’d be able to recognize any hash with ‘_id’ as an embedded document that needs a DBRef, and indeed we can. But we won’t know where to re-save it because we won’t know the collection anymore, so we add ‘_namespace’ to the record and strip it out on a save.

# File lib/poro/contexts/mongo_context.rb, line 435
def decode_db_ref(dbref)
  context = @@collection_map[dbref.namespace.to_s]
  if( context )
    value = context.data_store.db.dereference(dbref)
    return context.convert_to_plain_object(value, :embedded => false) # We want it to work like a standalone object, so don't treat as embedded.
  elsif self.data_store.db.collection_names.include?(dbref.namespace.to_s)
    value = self.data_store.db.dereference(dbref)
    value['_namespace'] = dbref.namespace.to_s
    return value
  else
    return dbref
  end
end
decode_foreign_managed_object(data) click to toggle source

Decode a foreign managed object. This is a matter of finding its Context and asking it to fetch it.

# File lib/poro/contexts/mongo_context.rb, line 464
def decode_foreign_managed_object(data)
  klass = Util::ModuleFinder.find(data['_class_name'])
  context = Context.fetch(klass)
  if( context )
    context.find(data['id'])
  else
    return data
  end
end
decode_hash(hash) click to toggle source

Decode a hash, recursing through its elements.

# File lib/poro/contexts/mongo_context.rb, line 383
def decode_hash(hash)
  return hash.inject({}) do |hash,(k,v)|
    hash[k] = self.convert_to_plain_object(v, :embedded => true)
    hash
  end
end
decode_self_managed_object(data) click to toggle source

Decode a self managed object

# File lib/poro/contexts/mongo_context.rb, line 450
def decode_self_managed_object(data)
  # Get the class and id.  Note these are auto-stripped by instantiate_object.
  class_name = data['_class_name']
  id = data['_id']
  # Instantiate.
  obj = instantiate_object(class_name, data)
  # Set the pk
  self.set_primary_key_value(obj, id)
  # Return
  return obj
end
decode_set(set_data) click to toggle source

Decode the set depending on if it was encoded as an array or as a raw object.

# File lib/poro/contexts/mongo_context.rb, line 418
def decode_set(set_data)
  if( set_data.include?('values') )
    return Set.new(set_data['values'])
  else
    return decode_unmanaged_object(set_data)
  end
end
decode_symbol(symbol_data) click to toggle source

Decode a symbol reference. If this users of the Context expect a Symbol to be encoded as a Symbol, then decode it as a Symbol. Otherwise the users of the Context wil be expecting a String.

# File lib/poro/contexts/mongo_context.rb, line 403
def decode_symbol(symbol_data)
  if self.encode_symbols
    return symbol_data['value'].to_sym
  else
    return symbol_data['value'].to_s
  end
end
decode_unmanaged_object(data) click to toggle source

Decode the given unmanaged object. If the class cannot be found, then just give back the underlying hash.

# File lib/poro/contexts/mongo_context.rb, line 476
def decode_unmanaged_object(data)
  begin
    klass = Util::ModuleFinder.find(data['_class_name']) # The class name is autostripped in instantiate_object
    return instantiate_object(klass, data)
  rescue NameError
    return data
  end
end
encode_array(array) click to toggle source

Recursively encode an array’s contents.

# File lib/poro/contexts/mongo_context.rb, line 249
def encode_array(array)
  return array.map {|o| self.convert_to_data(o, :embedded => true)}
end
encode_bigint(bigint) click to toggle source

Encodes a big-int, which is too big to be natively encoded in BSON.

# File lib/poro/contexts/mongo_context.rb, line 264
def encode_bigint(bigint)
  return {'_class_name' => 'Bignum', 'value' => bigint.to_s}
end
encode_class(klass) click to toggle source

Encode a class.

# File lib/poro/contexts/mongo_context.rb, line 254
def encode_class(klass)
  return {'_class_name' => klass.class, 'name' => klass.name}
end
encode_db_ref(hash) click to toggle source

Encode a hash that came from a DBRef dereferenced and decoded by this context.

This will save the hash when its owning object is saved!

# File lib/poro/contexts/mongo_context.rb, line 283
def encode_db_ref(hash)
  namespace = hash['_namespace'].to_s
  id = hash['_id']
  mongo_db = self.data_store.db
  if( mongo_db.collection_names.include?(namespace) )
    h = hash.dup # We want to be non-destructive here!
    h.delete['_namespace']
    mongo_db[namespace].save(h)
  end
  return BSON::DBRef.new(namespace, id)
end
encode_foreign_managed_object(obj) click to toggle source

Encode an object managed by a completely different kind of context.

# File lib/poro/contexts/mongo_context.rb, line 329
def encode_foreign_managed_object(obj)
  obj_context = Context.fetch(obj)
  obj_context.save(obj)
  obj_id = obj_context.primary_key_value(obj)
  return {'id' => obj_id, '_class_name' => obj.class.name, 'managed' => true}
end
encode_hash(hash) click to toggle source

Recursively encode a hash’s contents.

# File lib/poro/contexts/mongo_context.rb, line 241
def encode_hash(hash)
  return hash.inject({}) do |hash,(k,v)|
    hash[k] = self.convert_to_data(v, :embedded => true)
    hash
  end
end
encode_mongo_managed_object(obj) click to toggle source

Encode an object managed by this kind of context. It encodes as a DBRef if it is in the same database, and as a foreign managed object if not.

# File lib/poro/contexts/mongo_context.rb, line 313
def encode_mongo_managed_object(obj)
  # If in the same data store, we do a DBRef.  This is the usual case.
  # But we do need to save it if it is stored in a different database!
  obj_context = Context.fetch(obj)
  if( obj_context.data_store.db == self.data_store.db )
    obj_context.save(obj)
    obj_id = obj_context.primary_key_value(obj)
    obj_collection_name = obj_context.data_store.name
    return BSON::DBRef.new(obj_collection_name, obj_id)
  else
    # Treat as if in a foreign database
    return encode_foreign_managed_object(obj)
  end
end
encode_self_managed_object(obj) click to toggle source

Encode an object managed by this context.

# File lib/poro/contexts/mongo_context.rb, line 302
def encode_self_managed_object(obj)
  data = hashify_object(obj, instance_variables_to_save(obj))
  data['_id'] = primary_key_value(obj) unless primary_key_value(obj).nil?
  data_store.pk_factory.create_pk(data) # Use the underlying adapter's paradigm for lazily creating the pk.
  data['_class_name'] = obj.class.name
  return data
end
encode_set(set) click to toggle source

Encodes a Set as either :raw, :embedded_array, :array.

# File lib/poro/contexts/mongo_context.rb, line 269
def encode_set(set)
  method = @set_encoding_method
  if( method == :raw )
    return encode_unmanaged_object(set)
  elsif( method == :embedded_array )
    return {'_class_name' => 'Set', 'values' => self.convert_to_data(set.to_a, :embedded => true)}
  else
    return self.convert_to_data(set.to_a, :embedded => true)
  end
end
encode_symbol(sym) click to toggle source

Encodes a symbol.

# File lib/poro/contexts/mongo_context.rb, line 259
def encode_symbol(sym)
  return {'_class_name' => 'Symbol', 'value' => sym.to_s}
end
encode_unmanaged_object(obj) click to toggle source

Encode an object not managed by a context.

# File lib/poro/contexts/mongo_context.rb, line 296
def encode_unmanaged_object(obj)
  ivars = obj.instance_variables.map {|ivar| ivar.to_s[1..-1].to_sym}
  return hashify_object(obj, ivars)
end
hashify_object(obj, ivars) click to toggle source

Turns an object into a hash, using the given list of instance variables.

# File lib/poro/contexts/mongo_context.rb, line 177
def hashify_object(obj, ivars)
  data = ivars.inject({}) do |hash, ivar_name|
    ivar_sym = ('@' + ivar_name.to_s).to_sym
    value = obj.instance_variable_get(ivar_sym)
    hash[ivar_name.to_s] = self.convert_to_data(value, :embedded => true)
    hash
  end
  data['_class_name'] = obj.class.name
  return data
end
instance_variables_to_save(obj) click to toggle source

The computed list of instance variables to save, taking into account white lists, black lists, and primary keys.

# File lib/poro/contexts/mongo_context.rb, line 148
def instance_variables_to_save(obj)
  white_list = if( self.persistent_attributes_whitelist.nil? )
    obj.instance_variables.map {|ivar| ivar.to_s[1..-1].to_sym}
  else
    self.persistent_attributes_whitelist
  end
  black_list = self.persistent_attributes_blacklist || []
  # Note that this is significantly faster with arrays than sets.
  # TODO: Only remove the primary key if it is not in the white list!
  return white_list - black_list - [self.primary_key]
end
instantiate_object(klass_or_name, attributes) click to toggle source

Creates an object of a given class or class name, using the given hash of of attributes and encoded values.

# File lib/poro/contexts/mongo_context.rb, line 190
def instantiate_object(klass_or_name, attributes)
  # Translate class name.
  klass = Util::ModuleFinder.find(klass_or_name)
  
  # Allocate the instance (use allocate and not new because we have all the state variables saved).
  obj = klass.allocate
  
  # Iterate over attributes injecting.
  attributes.each do |name, encoded_value|
    next if name.to_s == '_class_name' || name.to_s == '_id'
    ivar_sym = ('@' + name.to_s).to_sym
    value = self.convert_to_plain_object(encoded_value, :embedded => true)
    obj.instance_variable_set(ivar_sym, value)
  end
  
  # Return the result.
  return obj
end
mongo_primitive?(obj) click to toggle source

If the object is a MongoDB compatible primitive, return true.

# File lib/poro/contexts/mongo_context.rb, line 161
def mongo_primitive?(obj)
  return(
    obj.kind_of?(Integer) ||
    obj.kind_of?(Float) ||
    obj.kind_of?(String) ||
    obj.kind_of?(Time) ||
    (!self.encode_symbols && obj.kind_of?(Symbol)) ||
    obj.kind_of?(TrueClass) ||
    obj.kind_of?(FalseClass) ||
    obj.kind_of?(NilClass) ||
    obj.kind_of?(BSON::ObjectId) ||
    obj.kind_of?(BSON::DBRef)
  )
end
route_decode(data, state_info={}) click to toggle source

Route the decoding of data from mongo.

# File lib/poro/contexts/mongo_context.rb, line 339
def route_decode(data, state_info={})
  if( data && data.kind_of?(Hash) && data['_class_name'] )
    return route_decode_stored_object(data, state_info)
  else
    return route_decode_stored_data(data, state_info)
  end
end
route_decode_stored_data(data, state_info={}) click to toggle source

If the data doesn’t directly encode an object, then this method knows how to route the decoding.

# File lib/poro/contexts/mongo_context.rb, line 349
def route_decode_stored_data(data, state_info={})
  if( data.kind_of?(Hash) )
    return decode_hash(data)
  elsif( data.kind_of?(Array) )
    return decode_array(data)
  elsif( data.kind_of?(BSON::DBRef) ) # This is a literal that we want to intercept.
    return decode_db_ref(data)
  else # mongo_primitive?(data) # Explicit check not necessary.
    return data
  end
end
route_decode_stored_object(data, state_info={}) click to toggle source

If the data directly encodes an object, then this methods knows how to route decoding.

# File lib/poro/contexts/mongo_context.rb, line 363
def route_decode_stored_object(data, state_info={})
  class_name = data['_class_name'].to_s
  if( class_name == 'Class' )
    return decode_class(data)
  elsif( class_name == 'Symbol' )
    return decode_symbol(data)
  elsif( class_name == 'Bignum' )
    return decode_bigint(data)
  elsif( class_name == 'Set' )
    return decode_set(data)
  elsif( class_name == self.klass.to_s )
    return decode_self_managed_object(data)
  elsif( class_name && data['managed'] )
    return decode_foreign_managed_object(data)
  else
    return decode_unmanaged_object(data)
  end
end
route_encode(obj, state_info={}) click to toggle source

Routes the encoding of an object to the appropriate method.

# File lib/poro/contexts/mongo_context.rb, line 212
def route_encode(obj, state_info={})
  if( obj.kind_of?(klass) && !state_info[:embedded] )
    return encode_self_managed_object(obj)
  elsif( obj.kind_of?(Hash) && obj.has_key?('_namespace') )
    return encode_db_ref
  elsif( obj.kind_of?(Hash) )
    return encode_hash(obj)
  elsif( obj.kind_of?(Array) )
    return encode_array(obj)
  elsif( obj.kind_of?(Class) )
    return encode_class(obj)
  elsif( obj.kind_of?(Bignum) )
    return encode_bigint(obj)
  elsif( obj.kind_of?(Set) )
    return encode_set(obj)
  elsif( self.encode_symbols && obj.kind_of?(Symbol) )
    return encode_symbol(obj)
  elsif( Context.managed_class?(obj.class) && Context.fetch(obj.class).kind_of?(self.class) )
    return encode_mongo_managed_object(obj)
  elsif( Context.managed_class?(obj.class))
    return encode_foreign_managed_object(obj)
  elsif( mongo_primitive?(obj) )
    return obj
  else
    return encode_unmanaged_object(obj)
  end
end