Skip to content

Design Considerations

Alice Zoë Bevan–McGregor edited this page Jul 7, 2016 · 5 revisions

Marrow Mongo is not an AR-ODM.

Marrow Mongo is not meant as a drop-in replacement for an active record pattern mapper, such as MongoEngine. MongoEngine (and friends) wrap the objects provided by the underlying driver, acting as middleware, and heavily tie themselves to implementation details of the driver. This has consequences in terms of support (driver updates mandate wrapper updates) and can delay production upgrades of the underlying database service.

Instead, Marrow Mongo takes a supportive approach towards integration with the driver. Your own application code uses the underlying driver interfaces (such as collection.insert_one) but may use Marrow Mongo objects as stand-ins for standard arguments to the API. As examples, Document instances act as valid mappings and may be passed directly as a document to pymongo. (Additionally they preserve order, unlike standard dictionaries.)

In some cases where extension might be useful we allow for patching in of new functionality, such as adding a convenient tailing cursor method to Collection objects. This patching approach is always optional and explicit.

Import-time side-effects and registries.

MongoEngine uses an internal global registry of Document classes that have been constructed. Because this is triggered based on the use of a metaclass, the classes registered this way must be imported prior to being discoverable. Additionally, there is no support for namespaces, meaning for unambiguous resolution all Document subclasses must be uniquely named.

There are a number of consequences of this decision, notably that while classes can be referred to by their string name (in places such as a ReferenceField), there is no actual code dependency set up to ensure that it exists at the time the reference is used, i.e. an import. Your application model becomes highly dependant on correct import ordering, which is fragile.

This is atrocious, so Marrow Mongo uses an official registry that Python provides out of the box: entry points. All Document and Field subclasses that want to participate must be registered against the marrow.mongo.document or marrow.mongo.field entry point namespace, as appropriate. These will be automatically imported and made available directly under the "marrow.mongo" namespace package for application-level convenience, they support namespacing (i.e. "myapp.Foo" instead of just "Foo" as the plugin name) with namespaces exposed under the same "marrow.mongo" package. (from marrow.mongo.myapp import Foo for the corresponding example import.) The only requirement is that Document itself be imported from marrow.mongo.core where used; this ensures the namespace will be correctly set up. (Marrow Mongo's only import-time side-effect.)

This allows us to keep Marrow Mongo's own internal field definitions cleanly organized in separate modules, provides a consistent singular location to import Document and Field subclasses from, and eliminates conflicting name issues. It also allows us to store entry point names within the database, where needed, instead of full import path or bare class name, preserving the ability to easily refactor code. (Including not just moving the class, but even re-naming it without having to run a migration over your data!) This also removes several special cases MongoEngine has, such as special "self" references.

Principle of least surprise.

While the "there should be one, and preferably only one right way to do something" rule is generally true, sometimes having alternatives that a developer might reasonably expect "just work" can be even better. For example, in cases where you define a Document that embeds another Document, during construction (and through attribute assignment) you can just assign a dictionary. It'll be automatically cast to the type referenced in the Embed field definition, or, if multiple field types are registered, it'll use the dictionary's _cls key to determine which to use. (If multiple types are registered, and no _cls key is present, a ValueError will be raised instead mentioning the ambiguity.)

At Illico we use this behaviour to bundle example records with the model definitions themselves, as in this example:

from marrow.mongo.core import Document
from marrow.mongo import String, Embed

class Name(Document):
	first = String()
	last = String()


class Account(Document):
	EXAMPLE = {"name": {"first": "Test", "last": "User"}, "email": "[email protected]"}
	
	name = Embed(Name)
	email = String()

You can then easily Account.from_mongo(Account.EXAMPLE) (or even Account(**Account.EXAMPLE)) to "unpack" the example record.

Clone this wiki locally