-
Notifications
You must be signed in to change notification settings - Fork 3
Design Considerations
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.
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.
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.