Models

Important to any ODM/ORM is a way to provide utility and validation for the documents that your application is storing. The goals of micromongo’s models are to:

  • focus utility & validation into one object
  • make utility, validation, and persistence part of documents being fetched from the database
  • decrease code repetition by allowing queries and save to use a single managed connection pool

Despite mongodb’s schemaless nature, there is often still a need to maintain some type and value guarantees on certain fields or maintain field requirements on certain documents. For instance, an Image being persisted to a database might contain all different kinds of EXIF keys and values, but an integer filesize (in bytes), URI, filetype, etc. could be required of all such documents.

The Model Base

All user defined models should inherit from micromongo.models.Model, which is also imported into the top level micromongo namespace:

class micromongo.Model(*d, **dd)

Micromongo Model object.

classmethod Model.new(*args, **kwargs)

Create a new instance of this model based on its spec and either a map or the provided kwargs.

classmethod Model.find(*args, **kwargs)

Run a find on this model’s collection. The arguments to Model.find are the same as to pymongo.Collection.find.

Model.save()

Save this object to the database. Behaves very similarly to whatever collection.save(document) would, ie. does upserts on _id presence. If methods pre_save or post_save are defined, those are called. If there is a spec document, then the document is validated against it after the pre_save hook but before the save.

Model.validate()

Validate this object based on its spec document.

The following keys cannot be used without colliding with names that Models use internally or externally. You should avoid these names if possible in your documents:

new, find, save, validate, keys, items, values, iterkeys, iteritems, itervalues, update, clear, pre_save, post_save, collection, database, spec

Many of these are to maintain a dict-like interface. You can use a micromongo model in anything that accepts map-like objects, but they do not inherit from dict, so be careful when passing into C-code that expects an explicit dict.

Spec Documents

Spec documents are dictionaries defined on Model classes that are a mapping of document keys to Field objects that describe arbitrary validation on those fields as well as requirements for any document of the model’s type.

class micromongo.spec.Field(required=False, default=None, type=None)

A base Field type, which itself can be used pretty reasonably to get type coersion, field defaults, etc. If required is True, then validation will fail if this field is NOT present in the document. default is the default value to give this field in a new document; if it is None and the field is not required, the field is not added to new documents. type is an object that can perform validation on this field; see documentation for Field.typecheck.

Field.typecheck(t)

Create a typecheck from some value t. This behaves differently depending on what t is. It should take a value and return True if the typecheck passes, or False otherwise. Override pre_validate in a child class to do type coercion.

  • If t is a type, like basestring, int, float, or a tuple of base types, then a simple isinstance typecheck is returned.
  • If t is a list or tuple of instances, such as a tuple or list of integers or of strings, it’s treated as the definition of an enum and a simple “in” check is returned.
  • If t is callable, t is assumed to be a valid typecheck.
  • If t is None, a typecheck that always passes is returned.

If none of these conditions are met, a TypeError is raised.

The way this looks in a micromongo Model is:

from pprint import pprint
from micromongo import Model, Field

class TestModel(Model):
    collection = 'test.test'
    spec = {
        'docid': Field(type=int, default=0),
        'req': Field(required=True),
        'enum': Field(type=['foo', 'bar'], default='foo'),
        'baz': Field(type=float),
    }

t = TestModel.new()
# t.items() => [('req', None), ('docid', 0), ('enum', 'foo')]

As you can see, docid and enum were given their defaults. req was given a default of None because it is required but no default was given. baz was not added to the document, but if it is, its value will have to be an instance of float, or else saving the document will fail during validation.

Creating Custom Field Types

You can create your own custom Field types by subclassing from Field. Suppose your needs include some kind of distance from a known point which must be 0 or more:

class PositiveNumberField(Field):
    """A field that checks that values are positive numbers, and also
    coerces values set on the field to float if it isn't already a
    number."""
    def __init__(self, **kwargs):
        def _type(value):
            if isinstance(value, (int, long, float)) and value >= 0:
                return True
            return False
        kwargs['type'] = _type
        super(PositiveIntegerField, self).__init__(**kwargs)

    def pre_validate(self, value):
        if isinstance(value, (int, long, float)):
            return value
        try:
            return float(value)
        except ValueError:
            return value

This field will still take required and default, but now the typecheck is embedded within the field class itself. It also overrides the pre_validate method to provide some type coercion:

Field.pre_validate(value)

A hook called before a value is validated. Implement this to do type coercion; say, to create an IntegerField that attempts to turn values into integers, or other more complex manipulations.

Document-level Validation

Sometimes, validation has to take into consideration the values of more than one field. Suppose you have a User model where you want either a twitter_id or an email_address field to be present and not the empty string. You can do this by implementing a pre_save hook on your Model object:

class User(Model):
    collection = 'auth.user'
    spec = {
        'username' : Field(required=True, default="", type=str),
        'password' : Field(required=True, default="", type=str),
        'twitter_id' : Field(required=True, default=""),
        'email_address': Field(required=True, default=""),
    }

    def pre_save(self):
        if not any((self.twitter_id, self.email_address)):
            raise ValueError("User objects require either a twitter_id or"
                             "email_address.")

Validation failures in general raise ValidationError, so if you are doing validation in your pre_save hook, you will want to raise this on failures too.

You can also use pre_save to cache or calculate certain fields based off of others in the model. A text document meant to be saved in markdown can have a calculated field prerendered that is set in this hook.

Registration Access

Micromongo keeps track of all models that have been registered. For applications that might want to use this data, a function providing a mapping of their database location to the model class is provided:

micromongo.models.registered_models()

Return the AccountingMeta’s model mapping, which is a dictionary of keys in the form of “db.collection” to model classes.

Table Of Contents

Previous topic

Tutorial

Next topic

Backend

This Page