Skip to content

User Guide

BryanHunt edited this page Oct 9, 2014 · 101 revisions

This guide will show you how to use the EMF Resource APIs for storing and retrieving EMF objects in MongoDB.

This user guide has been updated for version 0.8.0

OSGi Services

MongoEMF makes extensive use of OSGi declarative services to set up the framework. This makes the framework very flexible and thus highly extensible. Along with this flexibility comes a bit of complexity in the way all of the services are interrelated. The good news is that most users will only have to be concerned with configuring the MongoClientProvider and MongoDatabaseProvider services provided by eMongo. The services in this framework are typically never accessed directly by client code. Your code will indirectly use the services through the standard EMF API for loading and saving resources. This guide assumes you are already familiar with creating EMF models, resources, and resource sets.

MongoEMF URIs

All EMF resources are identified by a URI. The URI you use when persisting models to MongoDB must have the form:

mongodb://host[:port]/database/collection/{id}

  • host is the hostname of the server running MongoDB
  • port is the port the MongoDB server is listening on (optional)
  • database is the name of the MongoDB database to use
  • collection is the name of the MongoDB collection
  • id is the unique identifier of the object in MongoDB (optional on create / insert)

The URI path must have exactly three segments. Anything else will cause an IOException on a load() or save().

Create

To save a new object to MongoDB, call save() on a resource containing the EMF object having a URI as described above. When inserting a new object into MongoDB, the id segment of the URI is optional and typically the empty string. By default, MongoDB assigns a unique ID to an object when it is inserted into the database. In this mode, the URI of the EMF resource is automatically updated to include the MongoDB generated ID value. A URI with a MongoDB generated ID will look like: mongodb://localhost/app/users/4d6dc268b03b0db29961472c. It is also possible for the client to generate the ID and persist the object to MongoDB using that ID. The ID can be any string value that is unique within the MongoDB collection. A URI with a client generated ID might look like: mongodb://localhost/app/users/1.

Here is an example that inserts a new instance of a User object into MongoDB. The database is app and the collection is users.

ResourceSet resourceSet = resourceSetFactory.createResourceSet();

User user = ModelFactory.eINSTANCE.createUser();
user.setName("Test User");
user.setEmail("test@example.org");

Resource resource = resourceSet.createResource(URI.createURI("mongodb://localhost/app/users/"));
resource.getContents().add(user);

try
{
  user.eResource().save(null);
}
catch(IOException e)
{
  e.printStackTrace();
}

Retrieve

To retrieve an object from MongoDB, you load an EMF Resource using the unique URI for the object.

ResourceSet resourceSet = resourceSetFactory.createResourceSet();
Resource resource = resourceSet.getResource(URI.createURI("mongodb://localhost/app/users/4d6dc268b03b0db29961472c"), true);

User user = (User) resource.getContents().get(0);

Queries

There are currently two mechanisms to query data: using a simple query format, and using a MongoDB native format. The format available depends on which query bundle you include in your launch configuration. The simple query format has been deprecated in favor of the MongoDB native format. To use the MongoDB native queries, you must include the org.eclipselabs.mongoemf.query.mongo bundle in your launch configuration.

The general format of a query is shown below. The filter is the only required attribute. The values of the filter, projection, sort, and limit are passed directly to MongoDB. See the MongoDB documentation for details on how to construct your query.

{ 
  "filter" : { "attribute" : "value" },
  "projection" : { "attribute" : 1 },
  "sort" : { "attribute" : 1 },
  "limit" : 10
}

Example retrieving objects by query:

String query = "{ filter: { employeeType: \"manager\" }}";
ResourceSet resourceSet = resourceSetFactory.createResourceSet();
Resource resource = resourceSet.getResource(URI.createURI("mongodb://localhost/app/users/?" + query), true);

EReferenceCollection users = (EReferenceCollection) resource.getContents().get(0);

Update

To update an object in MongoDB, you simply save the Resource containing the object.

user.setEmail("mongo-emf@example.org");

try
{
  user.eResource().save(null);
}
catch(IOException e)
{
  e.printStackTrace();
}

Delete

To delete an object in MongoDB, you call delete() on the Resource containing the object.

try
{
  user.eResource().delete(null);
}
catch(IOException e)
{
  e.printStackTrace();
}

Collections

A collection of objects may be stored (contained) within a parent object, or stored as individual objects in a MongoDB collection. For objects that are contained as part of a parent object, when you operate on (load, save) the parent object, you operate on (load, save) the entire collection. When a collection is stored as individual objects in a MongoDB collection, each object is contained its own EMF Resource and must be managed individually by the client. If you allow MongoDB to generate the object ID, you may bulk insert a collection of objects in a single save() call. The resource is modified to contain a single Result object with a proxy to each of the inserted objects. You may iterate over the proxies to load each object into its own Resource.

Here is an example of bulk insert:

ResourceSet resourceSet = resourceSetFactory.createResourceSet();
Resource resource = resourceSet.createResource(URI.createURI("mongodb://localhost/app/users/"));

for(int i = 0; i < 10; i++)
{
  User user = ModelFactory.eINSTANCE.createUser()
  user.setName("User " + i);
  user.setEmail("user" + i + "@example.org");
  resource.getContents().add(user);
}

try
{
  resource.save(null);
}
catch(IOException e)
{
  e.printStackTrace();
}

ECollection eCollection = (ECollection) resource.getContents().get(0);

for(EObject eObject : eCollection.getValues()
{
  User user = (User) eObject;
  System.out.println("User " + user.getName() + " has URI: " + user.eResource.getURI());
}

EMF References

The way in which EMF references are persisted in MongoDB depends on the settings you specified when you created your EMF Ecore and Generator models. Two Ecore settings that affect persistence are: Containment, and Resolve Proxies. The Generator setting that affects persistence is: Containment Proxies. There are three types of references to consider: non-containment, containment, and bi-directional cross-document containment.

###Non-containment References A non-containment reference is modeled by setting Containment = false in the Ecore model. A non-containment reference can be to any other object in the database, a file, or on some other server. The target object may be contained by the referencing object, or could be in some other resource. Non-containment references are always persisted as a proxy. If the target object is in a separate resource from the referencing object, the target object must be persisted, on creation, before the object with the reference so that the proxy URI contains the ID of the target object.

###Containment References A containment reference is modeled by setting Containment = true in the Ecore model. A containment reference is persisted as a nested object in the same MongoDB document as the referencing object if Resolve Proxies = false in the Ecore model or Containment Proxies = false in the Generator model. If Resolve Proxies = true in the Ecore model and Containment Proxies = true in the Generator model, the reference will be persisted as a proxy if the target object is contained in its own Resource (cross-document containment). If the target object is not contained in its own Resource, it will be persisted as a nested object of the referencing object.

###Bi-directional Cross-document Containment References Bi-directional cross-document containment references need special consideration when it comes to saving the objects. For the proxies to be properly persisted, three calls to save() must be made on creation. One of the two objects must be saved twice. The other object must be saved once between the two saves to the other object.

For example, consider a bi-directional reference between a Person and an Address, the code to save the two objects would be as follows:

user.setAddress(address);

try
{
  address.save(null);
  user.save(null);
  address.save(null);
}
catch(IOException e)
{
  e.printStackTrace();
}

Load Options

OPTION_PROXY_ATTRIBUTES

Value type: Boolean

When you load an object with cross-document references, they will be proxies. When you access the reference, EMF will resolve the proxy and you can then access the attributes. This can cause performance problems for example when expanding a tree where you only need a name attribute to display the children and then only resolve the next child to be expanded. Setting this option to Boolean.TRUE will cause the proxy instance to have its attribute values populated so that you can display the child names in the tree without resolving the proxy.

Example:

resourceSet.getLoadOptions().put(MongoURIHandlerImpl.OPTION_PROXY_ATTRIBUTES, Boolean.TRUE);

OPTION_QUERY_CURSOR

Value type: Boolean

Queries return a Result with proxies to all objects found by default. Setting this option to Boolean.TRUE will cause queries to return a MongoCursor instead. You can then iterate using the MongoCursor. One advantage to using a cursor is that the objects will be demand created as you iterate over the cursor.

Example:

resourceSet.getLoadOptions().put(MongoURIHandlerImpl.OPTION_QUERY_CURSOR, Boolean.TRUE);
MongoCursor cursor = (MongoCursor) resourceSet.getResource(uri, true).getContents().get(0);

for(EObject eObject : cursor)
{
  ...
}

OPTION_READ_PREFERENCE

Value type: com.mongodb.ReadPreference

This option may be used when you wish to read from a particular server in a MongoDB replica set.

Example:

resourceSet.getLoadOptions().put(MongoURIHandlerImpl.OPTION_READ_PREFERENCE, ReadPreference.secondaryPreferred());

Save Options

OPTION_SERIALIZE_DEFAULT_ATTRIBUTE_VALUES

Value type: Boolean

EMF's default serialization is designed to conserve space by not serializing attributes that are set to their default value. This is a problem when attempting to query objects by an attributes default value. By setting this option to Boolean.TRUE, all attribute values will be stored to MongoDB.

Example:

HashMap<String, Object> options = new HashMap<String, Object>(1);
options.put(MongoURIHandlerImpl.OPTION_SERIALIZE_DEFAULT_ATTRIBUTE_VALUES, Boolean.TRUE);
resoure.save(options);

OPTION_USE_ID_ATTRIBUTE_AS_PRIMARY_KEY

Value type: Boolean

If it is set to Boolean.TRUE and the ID was not specified in the URI, the value of the ID attribute will be used as the MongoDB _id if it exists.

Example:

PrimaryObject primaryObject = ModelFactory.eINSTANCE.createPrimaryObject();
primaryObject.setIdAttribute("Attribute ID");
HashMap<String, Object> options = new HashMap<String, Object>();
options.put(MongoURIHandlerImpl.OPTION_USE_ID_ATTRIBUTE_AS_PRIMARY_KEY, Boolean.TRUE);
Resource resource = resourceSet.createResource(URI.createURI("mongodb://localhost/junit/PrimaryObject/"));
resource.getContents().add(primaryObject);
resource.save(options);
> db.PrimaryObject.find()
{ "_id" : "Attribute ID", "_eClass" : "http://www.eclipselabs.org/mongo/emf/junit#//PrimaryObject", "idAttribute" : "Attribute ID", "_timeStamp" : NumberLong("1322351923981") }

OPTION_WRITE_CONCERN

Value type: com.mongodb.WriteConcern

If set, the value must be an instance of WriteConcern and will be passed to MongoDB when the object is inserted into the database, or updated.

Example:

HashMap<String, Object> options = new HashMap<String, Object>(1);
options.put(MongoURIHandlerImpl.OPTION_WRITE_CONCERN, WriteConcern.SAFE);
resoure.save(options);

Type Converters

Several EMF attribute types (EDataType) can be serialized into MongoDB natively and without any conversion. All other types are serialized as a string in MongoDB using the standard EMF string conversion functions generated in your model's EPackage. For some types, it may be more efficient to store them in MongoDB as another native type such as int or long. To support this custom type conversion, you register instances of IValueConverter with the IConverterService. Please see the Extender Guide for details on creating a value converter.

Modeling Restrictions

There is one minor restriction that you must follow when creating your EMF Ecore model. The following keys are reserved for internal use and may not be used as attribute or reference names:

  • _id
  • _eId
  • _eClass
  • _eProxyURI
  • _timeStamp

JUnit Testing

The org.eclipselabs.mongo.emf.developer.junit bundle contains useful classes that can make writing your unit tests easier.

###MongoConfigurator

This class will configure a IMongoProvider for localhost. Simply create a component definition in your junit bundle with MongoConfigurator as the implementation and declare a dependency on ConfigurationAdmin.

###MongoDatabase

This class is a JUnit @Rule that will clear your MongoDB database after each test. The rule requires the name of the database to be passed as a parameter to its constructor. Here is an example on how to use that rule:

@Rule
public MongoDatabase database = new MongoDatabase();

###MongoUtil

This utility class contains functions for creating a ResourceSet, getting one or more objects from the database, getting the ID of an object, and comparing two EObjects. This class has extensive JavaDoc comments that explain the usage of each function.

###ServiceLocator

This class is a JUnit @Rule that can be used to wait for the specified service before running your unit tests. You may use the instance of the rule to get a reference to the service.

@Rule
public ServiceLocator<IResourceSetFactory> factoryLocator = new ServiceLocator<IResourceSetFactory>(IResourceSetFactory.class);

###ServiceTestHarness

This class can be used as a base class for your JUnit test that depends on OSGi services. It uses the mechanism of setting service references in static fields as described in the book OSGi and Equinox: Creating highly Modular Java Systems on page 135.

Launch Configurations

When creating a launch configuration, you will typically need the following bundles and their dependencies:

  • org.eclipselabs.mongoemf.api
  • org.eclipselabs.mong.emf.builders
  • org.eclipselabs.mongoemf.converter
  • org.eclipselabs.mongoemf.streams
  • org.eclipselabs.mongoemf.handlers
  • org.eclipselabs.mongoemf.query.mongo
  • org.eclipselabs.emongo.api
  • org.eclipselabs.emongo.components
  • org.eclipselabs.emodeling.api
  • org.eclipselabs.emodeling.components
  • org.eclipse.equinox.ds : Auto-start = true
  • org.eclipse.equinox.cm : Auto-start = true

Debugging

I typically debug problems by looking at the list of active components (declarative services) using the ls command if you are running Equinox. If any of the services are unsatisfied other than idFactory and uriMapping, you are probably missing a service configuration on either the clientProvider or databaseProvider. Here is the list of components in a running system.

osgi> ls
All Components:
ID	State			Component Name
1	Active		org.eclipselabs.mongoemf.query.mongodb
2	Active		org.eclipselabs.mongoemf.handlers.mongoURIHandlerProvider
3	Active		org.eclipselabs.mongoemf.streams.factory
4	Active		org.eclipselabs.mongoemf.converter
5	Active		org.eclipselabs.emongo.clientProvider
6	Active		org.eclipselabs.emongo.databaseProvider
7	Unsatisfied	org.eclipselabs.emongo.idFactory
8	Active		org.eclipselabs.emongo.components.client.metatype
9	Active		org.eclipselabs.emongo.components.database.metatype
10	Active		org.eclipselabs.emongo.components.id.metatype
11	Active		org.eclipselabs.emodeling.factory
12	Active		org.eclipselabs.emodeling.uriHandlerConfigurator
13	Unsatisfied	org.eclipselabs.emodeling.components.uriMapping
14	Active		org.eclipselabs.mongoemf.exampleComponent
15	Active		org.eclipselabs.mongoemf.example.config
16	Active		org.eclipselabs.mongoemf.builders.factory

To check the configuration of the clientProvider and databaseProvider components, use the comp command if you are running Equinox.

Here is an example of a properly configured clientProvider:

osgi> comp 5
	Component[
	name = org.eclipselabs.emongo.clientProvider
	activate = activate
	deactivate = deactivate
	modified = 
	configuration-policy = require
	factory = null
	autoenable = true
	immediate = true
	implementation = org.eclipselabs.emongo.components.MongoClientProviderComponent
	state = Unsatisfied
	properties = 
	serviceFactory = false
	serviceInterface = [org.eclipselabs.emongo.MongoClientProvider]
	references = {
		Reference[name = LogService, interface = org.osgi.service.log.LogService, policy = dynamic, cardinality = 0..1, target = null, bind = bindLogService, unbind = unbindLogService]
	}
	located in bundle = org.eclipselabs.emongo.components_1.0.0.201309151141 [16]
]
Dynamic information :
  The component is satisfied
  All component references are satisfied
  Component configurations :
    Configuration properties:
      client_id = example
      service.pid = org.eclipselabs.emongo.clientProvider-1396664578390-0
      service.factoryPid = org.eclipselabs.emongo.clientProvider
      objectClass = String[org.eclipselabs.emongo.MongoClientProvider]
      uri = mongodb://localhost
      component.name = org.eclipselabs.emongo.clientProvider
      component.id = 12
    Instances:
      org.eclipse.equinox.internal.ds.impl.ComponentInstanceImpl@d21e93b
    	Bound References:
        String[org.osgi.service.log.LogService,org.eclipse.equinox.log.ExtendedLogService]
      		-> org.eclipse.equinox.log.internal.ExtendedLogServiceImpl@2d572739

Here is an example of a properly configured databaseProvider

osgi> comp 6
	Component[
	name = org.eclipselabs.emongo.databaseProvider
	activate = activate
	deactivate = deactivate
	modified = 
	configuration-policy = require
	factory = null
	autoenable = true
	immediate = true
	implementation = org.eclipselabs.emongo.components.MongoDatabaseProviderComponent
	state = Unsatisfied
	properties = 
	serviceFactory = false
	serviceInterface = [org.eclipselabs.emongo.MongoDatabaseProvider]
	references = {
		Reference[name = MongoClientProvider, interface = org.eclipselabs.emongo.MongoClientProvider, policy = static, cardinality = 1..1, target = null, bind = bindMongoClientProvider, unbind = null]
	}
	located in bundle = org.eclipselabs.emongo.components_1.0.0.201309151141 [16]
]
Dynamic information :
  The component is satisfied
  All component references are satisfied
  Component configurations :
    Configuration properties:
      alias = example
      database = test
      service.pid = org.eclipselabs.emongo.databaseProvider-1396664578412-1
      service.factoryPid = org.eclipselabs.emongo.databaseProvider
      objectClass = String[org.eclipselabs.emongo.MongoDatabaseProvider]
      component.name = org.eclipselabs.emongo.databaseProvider
      component.id = 13
      MongoClientProvider.target = (client_id=example)
    Instances:
      org.eclipse.equinox.internal.ds.impl.ComponentInstanceImpl@4d8b9e26
    	Bound References:
        String[org.eclipselabs.emongo.MongoClientProvider]
      		-> org.eclipselabs.emongo.components.MongoClientProviderComponent@93bf890