Getting Started with Voyage: Storing Objects in MongoDB

A Hands-On Guide to Object-Document Mapping

This chapter provides a step-by-step walkthrough of Voyage, an object-to-document mapper for MongoDB. We'll explore a straightforward domain model: superheroes, their abilities, and equipment. You'll discover how to persist and retrieve domain objects with ease.

Establishing Database Connectivity

After setting up MongoDB, connecting to the database requires just a few lines of code:

| dbConnection |
dbConnection := VOMongoRepository 
    host: '127.0.0.1' 
    database: 'superHeroes'.
dbConnection enableSingleton.

For rapid prototyping scenarios where you don't need persistent storage, Voyage offers an in-memory repository that behaves identically to a real database connection:

| dbConnection |
dbConnection := VOMemoryRepository new. 
dbConnection enableSingleton

This approach enables transparent switching between storage backends during development. A common pattern is defining a repository setup method within your domain class:

Hero class >> establishConnection
    | repo |
    repo := VOMongoRepository 
        host: '127.0.0.1' 
        database: 'superHeroes'.
    repo enableSingleton.

Domain Model Overview

The following diagram illustrates the model structure we'll build throughout this tutorial.

Defining the Character Class

Let's implement the Character class to represent our superheroes:

Object subclass: #Character
    instanceVariableNames: 'name rank abilities'
    classVariableNames: ''
    package: 'SuperHeroes'
Character >> name
    ^ name
    
Character >> name: aString
    name := aString
    
Character >> rank
    ^ rank
    
Character >> rank: anObject
    rank := anObject
    
Character >> abilities
    ^ abilities ifNil: [ abilities := Set new ]
    
Character >> learnAbility: anAbility
    self abilities add: anAbility

Defining the Ability Class

Now we'll create the Ability class:

Object subclass: #Ability
    instanceVariableNames: 'name'
    classVariableNames: ''
    package: 'SuperHeroes'
Ability >> name
    ^ name
    
Ability >> name: aString
    name := aString

Implementing printOn: enhances debugging and object navigation.

Declaring Persistent Roots

To persist and query objects, we must designate which classes serve as entry points into our object graph. This is accomplished by implementing the class method isVoyageRoot. While we'll explore the implications later, for now we'll mark Character as a root:

Character class >> isVoyageRoot 
   ^ true

Now we can instantiate and persist our heroes:

Character new
   name: 'Spiderman';
   rank: 'epic';
   learnAbility: (Ability new name: 'Super-strength');
   learnAbility: (Ability new name: 'Wall-crawling');
   learnAbility: (Ability new name: 'Spider-sense');
   save.
Character new
   name: 'Wolverine';
   rank: 'epic';
   learnAbility: (Ability new name: 'Regeneration');
   learnAbility: (Ability new name: 'Adamantium claws');
   save.

Inspecting Persisted Data

We can examine how objects are stored directly in MongoDB:

> show dbs
local        0.078GB
superHeroes  0.078GB

> use superHeroes
switched to db superHeroes

> show collections
Character

Retrieving the first document shows how Voyage stores our data:

> db.Character.findOne()
{
    "_id" : ObjectId("d847065c56d0ad09b4000001"),
    "#version" : 688076276,
    "#instanceOf" : "Character",
    "rank" : "epic",
    "name" : "Spiderman",
    "abilities" : [
        {
            "#instanceOf" : "Ability",
            "name" : "Spider-sense"
        },
        {
            "#instanceOf" : "Ability",
            "name" : "Super-strength"
        },
        {
            "#instanceOf" : "Ability",
            "name" : "Wall-crawling"
        }
    ]
}

Notice that abilities are embedded directly within the character document rather than stored separately.

Querying Data

Back in Pharo, we can retrieve objects using various query methods:

Character selectAll.
> an OrderedCollection(a Character( Spiderman ) a Character( Wolverine ))

Character selectOne: [ :each | each name = 'Spiderman' ].
> a Character( Spiderman )

Character selectMany: [ :each | each rank = 'epic' ].
> an OrderedCollection(a Character( Spiderman ) a Character( Wolverine ))

Since MongoDB uses JSON internally, we can also pass dictionary-based criteria:

Character selectOne: { #name -> 'Spiderman' } asDictionary.
> a Character( Spiderman ) 

Character selectMany: { #rank -> 'epic' } asDictionary.
> an OrderedCollection(a Character( Spiderman ) a Character( Wolverine ))

More sophisticated queries support sorting and pagination:

Character
    selectMany: { #rank -> 'epic' } asDictionary 
    sortBy: { #name -> VOOrder ascending } asDictionary
    limit: 10
    offset: 0

Fundamental Operations

Counting Records

Character count.
> 2

Character count: [ :each | each name = 'Spiderman' ]
> 1

Removing Records

hero := Character selectAll anyOne.
hero remove.
> a Character

To remove all instances of a class:

Character removeAll.
> Character class

Introducing Additional Root Classes

Suppose our requirements evolve to require querying Ability instances independently. When introducing new root classes, you may need to refresh the database or perform migrations to load and re-save existing objects.

Any time you modify the database schema, reset it with:

VORepository current reset.

When to Designate New Roots

Consider two key factors when deciding whether a class needs root status:

  • Whether you need to query instances independently versus accessing them only through references
  • Whether shared instances should be deduplicated across the object graph—for example, if two characters could share the same ability and you want to ensure only one instance exists when loading both

Making Ability a Root Class

Ability class >> isVoyageRoot
    ^ true

Now we can persist abilities as standalone documents:

Ability new name: 'Flight'; save.
Ability new name: 'Super-strength'; save.

If newly saved objects don't appear in collections, reset the repository cache:

VORepository current reset.

After resetting and verifying:

> show collections
    Character
    Ability

We can now create characters that reference these shared ability instances. For testing purposes, let's clear existing characters:

| flight superPower |
flight := Ability selectOne: [ :each | each name = 'Flight']. 
superPower := Ability selectOne: [ :each | each name = 'Super-strength'].
Character new
    name: 'Superman'; rank: 'epic';
    learnAbility: flight; 
    learnAbility: superPower; 
    save.

Note that while abilities are saved as independent documents, they can still be referenced from multiple characters. Saving a character automatically persists its abilities too.

Querying the database reveals that characters now reference external ability documents instead of embedding them.

Handling Relationships

Voyage supports circular references between root objects but prevents them within embedded objects.

Enhancing the Character Class

We'll extend Character with equipment support. Note that root declarations are static—when a superclass is marked as a root, its subclasses share the same MongoDB collection. To give each subclass its own collection, you must explicitly mark each as a root.

Add an equipment instance variable to Character:

Object subclass: #Character
    instanceVariableNames: 'name rank abilities equipment' 
    classVariableNames: ''
    package: 'SuperHeroes'
Character >> equipment
    ^ equipment ifNil: [ equipment := Set new ]
   
Character >> equip: anItem 
    self equipment add: anItem

Reset the repository cache after structural changes:

VORepository current reset

Creating the Equipment Class Hierarchy

Define Equipment as a new root class:

Object subclass: #Equipment
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'SuperHeroes'

Equipment class >> isVoyageRoot
    ^ true

Create specialized subclasses:

Equipment subclass: #Weapon
    instanceVariableNames: ''
    classVariableNames: ''
    category: 'SuperHeroes'

Equipment subclass: #Armor
    instanceVariableNames: ''
    classVariableNames: ''
    category: 'SuperHeroes'

Now we can equip our characters:

Character new
    name: 'Iron-Man';
    rank: 'epic';
    equip: Armor new;
    save.

Examining the stored document:

> db.Character.find()[1]
{
    "_id" : ObjectId("d8475734421aa909b4000001"),
    "#instanceOf" : "Character",
    "#version" : NumberLong("2898020230"),
    "equipment" : [
        {
            "#instanceOf" : "Armor"
        }
    ],
    "rank" : "epic",
    "name" : "Iron-Man",
    "abilities" : null
}

Since we didn't mark Weapon and as separate roots, only the Equipment collection exists, containing both item types.

Equipment with Abilities

Some equipment may possess abilities—consider Thor's hammer. Let's extend Equipment:

Object subclass: #Equipment
    instanceVariableNames: 'abilities'
    classVariableNames: ''
    package: 'SuperPowers'

Equipment >> abilities
    ^ abilities ifNil: [ abilities := Set new ]
    
Equipment >> learnAbility: anAbility
    self abilities add: anAbility

Reset the repository after these changes:

VORepository current reset

Equip Iron-Man with a powered suit:

| hero flight superPower |
hero := Character selectOne: [ :each | each name = 'Iron-Man' ].
flight := Ability selectOne: [ :each | each name = 'Flight' ].
superPower := Ability selectOne: [ :each | each name = 'Super-strength' ].
hero equip: (Armor new
        learnAbility: flight;
        learnAbility: superPower;
        yourself);
save.

The database shows equipment documents:

> db.Equipment.find()[0]
{
    "_id" : ObjectId("d8475777421aa909b4000003"),
    "#instanceOf" : "Armor",
    "#version" : NumberLong("4204064627")
}

Equipment can contain references to abilities. Since Equipment is a root class, circular references within equipment are properly handled.

Tags: mongodb Voyage Pharo object-document-mapping NoSQL

Posted on Sun, 06 Sep 2026 16:43:36 +0000 by loftystew