loading...
Cover image for Creating an ODM with JavaScript

Creating an ODM with JavaScript

ecarriou profile image Erwan Carriou Updated on ・6 min read

I think the first time I heard about ORM was 10 years ago. I discovered this technique on a NetBeans tutorial that explained how to generate Java classes from a MySQL database. I made some tests and it worked pretty well. I really liked the concepts but not the execution. I was quite frustrated with the generation step because I had always to regenerate classes for every structure update of the database. This problem was in fact related to the language used for the mapping that needs to be compiled. I said to myself that it could be simpler to use a dynamic language that could generate these classes at runtime. That’s why I started at that time to create my own ORM with JavaScript. It worked quite well but I get stuck with a big limitation: MySQL. The relational structure of the tables did not match with JavaScript native objects. So the mapping was not as easy as I wanted.

But things changed a few years later when NoSQL databases became more and more popular. We could use pure JSON objects as documents and we could manage NoSQL data as native JavaScript objects.

I will show you in this post how it is easy to create now an ODM (Object-Document Mapping) with JavaScript.

My first ODM

Let’s start by choosing a NoSQL database. We will use my favorite one, I call it the universal database: {}.

const db = {};

It is light, can work on a server or on a browser. Everything I like!

Now that we have the database, let’s stop a minute to think about object creation in JavaScript. Generally we use many parameters to create an object, like this:

const luke = new Jedi('Luke', 'Skywalker');

But we could also pass one object as parameter:

const luke = new Jedi({
  firstName: 'Luke',
  lastName: 'Skywalker'
});

Did you notice that this parameter looks like a document? That is the main idea of ODM: use a document as a parameter of the class constructor.

Now we have that in mind, let’s create the class that will manage the mapping between documents and class instances:

class ODM {
  constructor(document) {
    // get class name
    const name = this.constructor.name;

    // add unique id
    if (!document._id) document._id = Math.random().toString();

    // create document
    if (!db[name]) db[name] = {};
    db[name][document._id] = document;

    // define accessors
    const configuration = {};
    Object.keys(document).forEach((prop) => {
      configuration[prop] = {
        get() {
          return db[name][document._id][prop];
        },
        set(value) {
          db[name][document._id][prop] = value;
        }
      };
    });

    // set accessors
    Object.defineProperties(this, configuration);
  }
}

In this class we made several things:

  • we get the name of the collection in the database: in our case the class name,
  • we generate a unique id for the document,
  • we add the document in the database and
  • we create getter and setter for every property of the instance that will manage the related document in the database.

Now let’s do some tests with it:

// create a Jedi class
class Jedi extends ODM { };

// create an instance with a document
const luke = new Jedi({
  _id: 'luke',
  firstName: 'Luke',
  lastName: 'Skywaker'
}); 

// update the instance
luke.lastName = 'Skywalker';

// check that the value has been changed in the database
db.Jedi.luke.lastName;
// => 'Skywalker'

We have now a complete synchronization between documents and instances. And we did that with only 30 lines of code!

Documents exportation

Let go further. And if want to export documents? It is very easy to do that:

db.export = (name) => {
  return JSON.stringify(db[name]);
};

In our case we suppose that all the documents are JSON valid so that we can export them with a native JavaScript API.

Now let’s make some tests with it:

// create the Jedi class
class Jedi extends ODM { };

// create an object with a document
const luke = new Jedi({
  _id: 'luke',
  firstName: 'Luke',
  lastName: 'Skywaker'
}); 

db.export('Jedi');
// => '{\"luke\":{\"firstName\":\"Luke\",\"lastName\":\"Skywaker\",\"_id\":\"luke\"}}'

In this example we export all the documents created for a specific class. That means that we can now serialize all the objects into a string. Pretty cool, does it?

Documents importation

Now we will do something a little more complicated with the importation of documents. When we import documents on a specific collection, we want to create their related objects:

// create classes list
const classes = {}; 

db.import = (name, documents) => {
  db[name] = JSON.parse(documents);

  // create instances
  Object.keys(db[name]).forEach((id) => {
    new classes[name](db[name][id]);
  });
};

Now let’s update a bit the main class for that purpose:

// create instances list
const instances = {}; 

class ODM {

  constructor(document) {
    // get class name
    const name = this.constructor.name;

    // add unique id
    if (!document._id) document._id = Math.random().toString();

    // create document
    if (!db[name]) db[name] = {};
    db[name][document._id] = document;

    // define accessors
    const configuration = {};
    Object.keys(document).forEach((prop) => {
      configuration[prop] = {
        get() {
          return db[name][document._id][prop];
        },
        set(value) {
          db[name][document._id][prop] = value;
        }
      };
    });

    // set accessors
    Object.defineProperties(this, configuration);

    // add it to the list of instances
    instances[document._id] = this;
  }
}

The difference with the previous class is that we add now the created instance in the list instances.

Let ‘s test it:

// create Jedi class
classes.Jedi = class Jedi extends ODM {};

// import Jedi documents
db.import('Jedi', '{\"luke\":{\"firstName\":\"Luke\",\"lastName\":\"Skywalker\",\"_id\":\"luke\"}}');

// access the created instance
instances.luke.firstName;
// => 'Luke'

We can now deserialize datas into objects. Moreover we can also know the exact number of created objects at any moments, it is the number of objects in my instances list.

Managing data relationships

And what about relations? In NoSQL world, we can simulate relations by using the id of a document as a value of a property to create a link. If we follow this pattern, managing relations becomes very simple:

class ODM {

  constructor(document) {
    // get class name
    const name = this.constructor.name;

    // add unique id
    if (!document._id) document._id = Math.random().toString();

    // create document
    if (!db[name]) db[name] = {};
    db[name][document._id] = document;

    // define accessors
    const configuration = {};
    Object.keys(document).forEach((prop) => {
      configuration[prop] = {
        get() {
          const value = db[name][document._id][prop];
          // return an instance or a value
          return value.indexOf('@') !== -1 ? instances[value.replace('@','')] : value;
        },
        set(value) {
          if (classes[value.constructor.name]) {
            // store the id of the instance
            db[name][document._id][prop] = value._id;
          } else {
            db[name][document._id][prop] = value;
          }
        }
      };
    });

    // set accessors
    Object.defineProperties(this, configuration);

    // add it to the list of instances
    instances[document._id] = this;
  }
}

To distinguish a value from a link, we add this new rule: if a value begins with @, it means that it represents the id of a document.

Let’s create now a link between objects:

const vador = new classes.Jedi({
  _id: 'vador',
  'firstName': 'Dark',
  'lastName': 'Vador'
)};

const luke = new classes.Jedi({
  _id: 'luke',
  'firstName': 'Luke',
  'lastName': 'Skywalker',
  'father': '@vador'
)};

luke.father.lastName;
// => 'Vador'

Now, let’s do this link at API level:

const vador = new classes.Jedi({
  _id: 'vador',
  'firstName': 'Dark',
  'lastName': 'Vador'
});

const luke = new classes.Jedi({
  _id: 'luke',
  'firstName': 'Luke',
  'lastName': 'Skywalker'
});

// set father link  
luke.father = vador;

db.export('Jedi');  
// => '{\"vador\":{\"_id\":\"vador\",\"firstName\":\"Dark\",\"lastName\":\"Vador\"},
// \"luke\":{\"_id\":\"luke\",\"firstName\":\"Luke\",\"lastName\":\"Skywalker\",\"father\":\"@vador\"}}'

As you see, we can create one-to-one relationship very easily with ODM.

Conclusion

ODM is a technique that you have to use more often in your code, it is not complicated and very powerful. Because of the heavy coupling between documents and objets, you know at every moments what are the components of your application, how many they are and what data they manage.

If you think harder, you realize that in fact ODM is a way to manage the store of your application. Every kind of components has its own store (i.e. collection in the database) and can be managed like in Redux. But here you are at the functional level (you manage functional objects), not at the technical level (where you manage data).

I made some CodePen examples so that you can start to play right now with ODM:

If you want to go deeper, you can have a look at System Runtime, a JavaScript library that I have created that applies all the pattern I talked about in this post.


Credits: cover image by Sebastien Gabriel.

Posted on by:

ecarriou profile

Erwan Carriou

@ecarriou

I like JavaScript, Web Standards, Open Web Platform, UX, Diversity in Tech

Discussion

markdown guide
 

Bonjour,
Je me penche dessus aujourd'hui et je n'y arrive pas...
Je crée deux instances "vador" et "luke" de new classes.Jedi(), je set luke.father = vador; j'ai bien la prop luke.father.lastName == 'vador'mais quand je dump la BDD cela n’apparaît pas.
Un fiddle ici si quelqu'un a du temps ^ : jsfiddle.net/molokoloco/w82srpra/

 

En fait c’est normal comme tu rajoutes la propriété après l’instance de la classe.

Pour que cela marche avec l’exemple, il faut rajouter la propriété father dans le paramètre du constructeur (cad il faut que la propriété soit déjà présente dans cet objet). Et là tu aura bien father dans l’export.

 

Ok, je comprends, merci
Par contre là, je produis un nouveau bug... Si je dé-commente la propriété "father" dans la création de l'instance luke (cf jsfiddle.net/w82srpra/60/) et bien le père apparaît comme étant lui même o_O

Il y avait effectivement un bug, je l’ai corrigé. Le code de l’article a été mis à jour. Cela devrait être bon maintenant.