MorphiaUser Guide
Overview
Morphia
is a Fantom to MongoDB object mapping library.
Morphia
is an extension to the Mongo library that maps Fantom objects and their fields to and from MongoDB collections and documents.
Morphia
features include:
- All Fantom literals and BSON types supported by default,
- Support for embedded / nested Fantom objects,
- Extensible mapping - add your own custom Converters,
- Query Builder API.
Note: Morphia
has no association with Morphia - the Java to MongoDB mapping library. Well, except for the name of course!
Install
Install Morphia
with the Fantom Repository Manager ( fanr ):
C:\> fanr install -r http://repo.status302.com/fanr/ afMorphia
To use in a Fantom project, add a dependency to build.fan
:
depends = ["sys 1.0", ..., "afMorphia 1.0"]
Documentation
Full API & fandocs are available on the Status302 repository.
Quick Start
1). Start up an instance of MongoDB:
C:\> mongod MongoDB starting db version v2.6.0 waiting for connections on port 27017
2). Create a text file called Example.fan
:
using afMorphia using afBson using afIoc using afIocConfig @Entity class User { @Property ObjectId _id @Property Str name @Property Int age new make(|This|in) { in(this) } } class Example { @Inject { type=User# } Datastore? datastore Void main() { reg := RegistryBuilder().addModulesFromPod(Pod.find("afMorphia")).addModule(ExampleModule#).build.startup reg.injectIntoFields(this) micky := User { it._id = ObjectId() it.name = "Micky Mouse" it.age = 42 } // ---- Create ------ datastore.insert(micky) // ---- Read -------- q := Query().field("age").eq(42) mouse := (User) datastore.query(q).findOne echo(mouse.name) // --> Micky Mouse // ---- Update ----- mouse.name = "Minny" datastore.update(mouse) // ---- Delete ------ datastore.delete(micky) reg.shutdown } } class ExampleModule { @Contribute { serviceType=ApplicationDefaults# } static Void contributeAppDefaults(Configuration config) { config[MorphiaConfigIds.mongoUrl] = `mongodb://localhost:27017/exampledb` } }
3). Run Example.fan
as a Fantom script from the command line:
[afIoc] Adding module definitions from pod 'afMorphia' [afIoc] Adding module definition for afMorphia::MorphiaModule [afIoc] Adding module definition for afIocConfig::IocConfigModule [afIoc] Adding module definition for afMorphia::ExampleModule [afMongo] Alien-Factory _____ ___ ___ ___ ___ | | . | | . | . | |_|_|_|___|_|_|_ |___| |___|1.0.0 Connected to MongoDB v2.6.1 (at mongodb://localhost:27017) [afIoc] ___ __ _____ _ / _ | / /_____ _____ / ___/__ ___/ /_________ __ __ / _ | / // / -_|/ _ /===/ __// _ \/ _/ __/ _ / __|/ // / /_/ |_|/_//_/\__|/_//_/ /_/ \_,_/__/\__/____/_/ \_, / Alien-Factory IoC v2.0.0 /___/ IoC Registry built in 355ms and started up in 225ms Micky Mouse [afIoc] IoC shutdown in 12ms [afIoc] "Goodbye!" from afIoc!
Usage
MongoDB Connections
A Mongo Connection URL should be contributed as an application default. This supplies the default database to connect to, along with any default user credentials. Example, in your AppModule
:
@Contribute { serviceType=ApplicationDefaults# } static Void contributeAppDefaults(Configuration config) { config[MorphiaConfigIds.mongoUrl] = `mongodb://username:password@localhost:27017/exampledb` }
Morphia
uses the connection URL to create a pooled ConnectionManager. The ConnectionManager
, and all of its connections, are gracefully closed when IoC / BedSheet is shutdown.
Some connection URL options are supported:
mongodb://username:password@example1.com/database?maxPoolSize=50
mongodb://example2.com?minPoolSize=10&maxPoolSize=25
See ConnectionManagerPooled for more details.
Entities
An entity is a top level domain object that is persisted in a MongoDB collection.
Entity objects must be annotated with the @Entity facet. By default the MongoDB collection name is the same as the (unqualified) entity type name. Example, if your entity type is acmeExample::User
then it maps to a Mongo collection named User
. This may be overriden by providing a value for the @Entity.name
attribute.
Entity fields are mapped to properties in a MongoDB document. Use the @Property
facet to mark fields that should be mapped to / from a Mongo property. Again, the default is to take the property name and type from the field, but it may be overridden by facet values.
As all MongoDB documents define a unique property named _id
, all entities must also define a unique property named _id
. Example:
@Entity class MyEntity { @Property ObjectId _id ... }
or
@Entity { name="AnotherEntity" } class MyEntity { @Property { name="_id" } ObjectId wotever ... }
Note that a Mongo Id does not need to be an ObjectId
. Any object may be used, it just needs to be unique.
Datastore
A Datastore wraps a Mongo Collection and is your gateway to saving and reading Fantom objects to / from the MongoDB.
Each Datastore
instance is specific to an Entity type, so to inject a Datastore
you need to specify which Entity it is associated with. Use the @Inject.type
attribute to do this. Example:
@Inject { type=User# } Datastore datastore
Mapping
At the core of Morphia
is a suite of Converters that map Fantom objects to Mongo documents.
Standard Converters
By default, Morphia
provides support and converters for the following Fantom types:
afBson::Binary sys::Bool sys::Buf afBson::Code sys::Date sys::DateTime sys::Decimal sys::Duration sys::Enum sys::Float sys::Int sys::List sys::Map afBson::MaxKey afBson::MinKey null afBson::ObjectId sys::Regex sys::Range sys::Slot sys::Str afBson::Timestamp sys::Type sys::Uri
Embedded Objects
Morphia is also able to convert embedded, or nested, Fantom objects. Extending the example in Quick Start, here we substitute the Str
name for an embedded Name
object:
@Entity class User { @Property ObjectId _id @Property Name name @Property Int age new make(|This|in) { in(this) } } class Name { @Property Str firstName @Property Str lastName new make(|This|in) { in(this) } } ... micky := User { _id = ObjectId() age = 42 name = name { firstName = "Micky" lastName = "Mouse" } } mongoDoc := datastore.toMongoDoc(micky) echo(mongoDoc) // --> [_id:xxxx, age:42, name:[lastName:Mouse, firstName:Micky]]
Note that embedded Fantom types should not be annotated with @Entity
.
Custom Converters
If you want more control over how objects are mapped to and from Mongo, then contribute a custom converter. Do this by implementing Converter
and contributing an instance to the Converters
service.
Example, to store the Name
object as a simple hyphenated string:
const class NameConverter : Converter { override Obj? toFantom(Type fantomType, Obj? mongoObj) { // decide how you want to handle null values if (mongoObj == null) return null mong := ((Str) mongoObj).split('-') return Name { it.firstName = mong[0]; it.lastName = mong[1] } } override Obj? toMongo(Obj fantomObj) { name := (Name) fantomObj return "${name.firstName}-${name.lastName}" } }
Then contribute it in your AppModule:
@Contribute { serviceType=Converters# } static Void contributeConverters(Configuration config) { config[Name#] = NameConverter() }
To see it in action:
micky := User { it._id = ObjectId() it.age = 42 it.name = Name { it.firstName = "Micky" it.lastName = "Mouse" } } mongoDoc := datastore.toMongoDoc(micky) echo(mongoDoc) // --> [_id:xxxx, age:42, name:Micky-Mouse]
Storing Nulls in Mongo
When converting Fantom objects to Mongo, the ObjConverter decides what to do if a Fantom field has the value null
. Should it store a key in the MongoDb with a null
value, or should it not store the key at all?
To conserve storage space in MongoDB, by default ObjConverter
does not store the keys.
If you want to store null
values, then create a new ObjConverter
passing true
into the ctor, and contribute it in your AppModule: Example:
@Contribute { serviceType=Converters# } static Void contributeConverters(Configuration config) { config.overrideValue(Obj#, config.registry.createProxy(Converter#, ObjConverter#, [true]), "MyObjConverter") }
(A proxy is required due to the circular nature of Converters.)
See Storing null vs not storing the key at all in MongoDB for more details.
Query API
Querying a MongoDB for documents requires knowledge of their Query Operators. While simple for simple queries:
query := ["age": 42]
It can quickly grow unmanagable for larger queries. Example, this tangled mess is from the official documentation for the $and operator:
query := [ "\$and" : [ ["\$or": [["price": 0.99f], ["price": 1.99f]]], ["\$or": [["sale" : true ], ["qty" : ["\$lt": 20]]]] ] ]
For that reason Morphia provides a means to build and execute Query objects that rely on more meaningful method names. The simple example may be re-written as:
query := Query().field("age").eq(42)
Use a QueryExecutor as returned from the Datastore.query(...)
method to run the query.
datastore.query(query).findAll
Because you often create Query
objects to match fields, it can be helpful to squirrel away this little bit of code in its own method:
QueryCriterion field(Str fieldName) { Query().field(fieldName) }
The more complicated $and
example then becomes:
query := Query().and([ Query().or([ field("price").eq(0.99f), field("price").eq(1.99f) ]), Query().or([ field("sale ").eq(true), field("qty").lessThan(20) ]) ])
Which, even though slightly more verbose, should be much easier to construct and debug. And the autocomplete nature of IDEs such as F4 means you don't have to constantly consult the Mongo documentation.
Remarks
If you're looking for cross-platform MongoDB GUI client then look no further than Robomongo!
Release Notes
v1.0.0
- New: Introduced the Query Builder API.
- New:
IntSequences
services provides an alternative to unique ObjectIDs. - New: Mongo
Collections
maybe injected in the same manner asDatastore
objects, using the@Inject.type
attribute.
v0.0.10
- Chg: Deleted
@DatastoreType
, use@Inject
instead. - Chg: Updated to use IoC 2.0.0 and IoC Config 1.0.16
v0.0.8
- Chg: Updated to use IoC 1.7.6 and IoC Config 1.0.14
v0.0.6
- Chg: Updated to use IoC 1.7.0.
v0.0.4
- Chg:
@Entity
facet is now inherited. - Bug:
Datastore.findOne()
andDatastore.get()
could throw aNullErr
if checked was false.
v0.0.2
- New: Preview Release