-
Notifications
You must be signed in to change notification settings - Fork 6
User Guide
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
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.
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().
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();
}
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);
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);
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();
}
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();
}
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());
}
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();
}
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);
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)
{
...
}
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());
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);
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") }
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);
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.
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
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.
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
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