The Martian Chronicles

Voyage: The adventure of persisting object models

Voyage is a small persistence framework, purely object oriented, intended to present a common API to most common development usages. It is just a small layer between your objects and the persistent mechanism. This layer provides some useful vocabulary for your objects.

Voyage

Voyage can work in two different programming styles (modes):

  1. Singleton mode: You have an unique repository in your image, which works as a singleton keeping all your data there. When you use this style, you can program using a “behavioral complete” approach: your instances respond to a certain vocabulary (see below to more details about vocabulary and usage).
  2. Instance mode: You can have an undetermined number of repositories living in your image. Of course, while some work can be done to keep the object vocabulary working as “standard use”, this mode requires you to make explicit which repository are you going to use.

Vocabulary

This is all the vocabulary you need to work with Voyage:

Singleton modeInstance modeFunction
savesave:stores an object into repository (insert or update)
removeremove:removes an object from repository
removeAllremoveAll:removes all objects of class from repository
selectAllselectAll: retrieves all objects of some kind
selectOne:selectOne:where:retrieves first object that matches the where clause
selectMany:selectMany:where:retrieves all objects that matches the where clause

Voyage-Mongo

Voyage is a common layer for different backends but currently it supports just two: an “in memory” layer (to fast prototype applications and to fist stage developments), and a Mongo database backend.

Mongo is a document oriented database that works well persisting complex object models, even if it is not an object database (like Gemstone, Magma or Omnibase). Because of that, we need an “Object-Document mapper” (the document equivalent to an ORM), and while it does not solve all the impedance mismatch issues, it fits a lot better with an object world, being able to preserve it’s dynamic nature.

1. Installation

To install Voyage you can go to Configurations Browser (in World Menu/Tools) and load “ConfigurationOfVoyageMongo”. Or alternatively you can execute in your workspace:

Gofer it
	url: ‘http://smalltalkhub.com/mc/estebanlm/Voyage/main’;
	package: ‘ConfigurationOfVoyageMongo’;
	load.
(Smalltalk at: #ConfigurationOfVoyageMongo) load.

That will load all you need to persist your object model into a Mongo database.

Create a repository

Start a new repository or connect with an existing one is very simple, you just need to do:

repository := VOMongoRepository 
	host: ‘mongo.db.url’ 
	database: ‘databaseName’.

Alternatively you can specify the port to connect by using protocol VOMongoRepository class>>host:port:database:. You can keep that instance for storage/retrieval, but probably you prefer to use the Singleton mode. To enable your repository to be accessed all around the image, you can execute:

repository enableSingleton.

WARNING: executing this line will remove older repositories from Singleton mode!

NOTE: In this document, we are going to cover Voyage in Singleton mode, but using it in Instance mode should be straightforward (just take a look at VORepository persistence protocol).

2. Storing objects

Basic storage

Let’s say we want to store an Association (yes, the example is dumb, but is the simplest I can think of). To do that, we need to declare Association to be storable as root (first entrance point) of our repository:

Association class>>#isVoyageRoot
	^true

Then, you just need to execute:

anAssociation := #answer->42.
anAssociation save.

This will generate a collection named “point” in your database, with a document with the following structure:

{ 
	"_id" : ObjectId("a05feb630000000000000000"), 
	"#instanceOf" : "Association", 
	"#version" : NumberLong("3515916499"), 
	"key" : ‘answer’, 
	"value" : 42
}

As you can see, there are some “extra information” to allow the object to be recognized:

  1. #instanceOf keeps the type (Class) of the stored instance. This is useful for reconstructing complex hierarchies (I’ll talk a bit more about this later).
  2. #version keeps a marker of the version commited. This property is used for refreshing cached data in the application. If there is not #version field the application will refresh always the object with data from database (and you will lose a lot of performance).

Embedding objects

Now, most objects you use are not so simple as associations, but more complex graphs with other complex object instances inside. Let’s say that we want to keep rectangles. Each rectangle contains two points, so, is a graph like this:

Model

To store this, we add:

Rectangle class>>#isVoyageRoot
	^true

And execute:

aRectangle := 42@1 corner: 10@11.
aRectangle save.

This will generate a document like this:

{ 
	"_id" : ObjectId("ef72b5810000000000000000"), 
	"#instanceOf" : "Rectangle", 
	"#version" : NumberLong("2460645040"), 
	"origin" : { 
		"#instanceOf" : "Point", 
		"x" : 42, 
		"y" : 1 
	}, 
	"corner" : { 
		"#instanceOf" : "Point", 
		"x" : 10, 
		"y" : 20 
	}
}

As you can see, for many cases, you do not need to do anything special to store complex graphs (but you can enhance and/or modify the way you persist objects, see “Enhancing storage” section below).

Referencing other roots

Some times, your objects are graphs who contains other root objects (for instance, you could want to keep “users” and “roles” in different collections, and of course, an user has a collection of roles).

To do that, you just need to declare the former embedded objects as roots.

In our rectangle example, let’s suppose we want to keep into a separated collection the points (we call them referenced instead embedded).

After we add #isVoyageRoot to Point, we can save our rectangle, and we will get:

In collection “rectangle”:

{ 
	"_id" : ObjectId("7c5e772b0000000000000000"), 
	"#instanceOf" : "Rectangle", 
	"#version" : 423858205, 
	"origin" : { 
		"#collection" : "point", 
		"#instanceOf" : "Point", 
		"__id" : ObjectId("7804c56c0000000000000000") 
	}, 
	"corner" : { 
		"#collection" : "point", 
		"#instanceOf" : "Point", 
		"__id" : ObjectId("2a731f310000000000000000") 
	} 
}

In collection “point” (two documents):

{ 
	"_id" : ObjectId("7804c56c0000000000000000"), 
	"#version" : NumberLong("4212049275"), 
	"#instanceOf" : "Point", 
	"x" : 42, 
	"y" : 1 
}

{ 
	"_id" : ObjectId("2a731f310000000000000000"), 
	"#version" : 821387165, 
	"#instanceOf" : "Point", 
	"x" : 10, 
	"y" : 20 
}

Enhancing storage

You can change the way your objects are stored by adding magritte descriptions.

For instance, always with our rectangle example (but with points embedded, not referenced), let’s say:

  1. I want to use a different collection name: “rectanglesForTest” instead “rectangle”.
  2. I know that I will store always points in my collection, and therefore the #instanceOf information is redundant.
  3. I know that “origin” and “corner” attributes are going to be always points, so #instanceOf informations is redundant there too.

Rectangle class>>mongoContainer 
	<mongoContainer>
	
	^VOMongoContainer new 
		collectionName: 'rectanglesForTest';
		kind: Rectangle;
		yourself

Rectangle class>>mongoOrigin
	<mongoDescription>

	^VOMongoToOneDescription new
		attributeName: ‘origin’;
		kind: Point;
		yourself

Rectangle class>>mongoCorner
	<mongoDescription>

	^VOMongoToOneDescription new
		attributeName: ‘corner’;
		kind: Point;
		yourself

Then, you can save your rectangle and you will see that the document generated (now in “rectanglesForTest”), it will look more or less like this:

{ 
	"_id" : ObjectId("ef72b5810000000000000000"), 
	"#version" : NumberLong("2460645040"), 
	"origin" : { 
		"x" : 42, 
		"y" : 1 
	}, 
	"corner" : { 
		"x" : 10, 
		"y" : 20 
	}
}

You can do other fancy stuff with this, like:

  • #beEager. You can declare referenced instanced to be loaded eagerly (default is lazy).
  • #beLazy. You can declare referenced instances to be loaded lazily.
  • #kindCollection:. For VOMongoToManyDescription, it allows you to change the resulting collection kind.
  • #convertNullTo:. When you are retrieving an object (or a collection of objects) which value is Null (nil), you can instead evaluate the valuable you are passing as an argument.

3. Retrieving objects (querying)

So far, we can write objects to our database. But, how about to retrieve them?

WARNING: This examples will not work if you have “mixed” the persisted documents with the previous examples (with referenced and embedded points, for instance). I recommend you to drop all collections and “start clean” with the embedded approach.

  • For retrieving all objects from your “rectangle” collection:
Rectangle selectAll.
  • For retrieving first object that fulfills a condition:
Rectangle selectOne: { ‘origin.x’->42 } asDictionary.

Notice that here we are accessing an embedded object property!

  • For retrieving all object that fulfills a condition:
Rectangle selectMany: { ‘origin.x’->42 } asDictionary.

Querying with MongoQueries

Since some versions ago, MongoTalk (the base layer of Voyage-Mongo) provides a cool way of doing queries:

Point selectOne: [ :each | each x = 42 and: [ each y > 50] ].

This looks easier than using a dictionary, but still has some minor issues, when querying embedded object. In that case, we still can use a block statement, but we need to access directly to the embedded attribute:

Rectangle selectMany: [ :each | (each at: ‘origin.x’) = 42 ]

This cover almost all the functionality you can currently get by using Voyage with Mongo. In next documents we will cover some of the possible applications of this framework, and the design implementation.

Posted by Esteban at 14 June 2013, 1:45 pm with tags pharo, voyage, mongo link