11 min read

alan-fitzsimmons-XaQhfvuCVz4-unsplash

Introduction to Vue Object-Relational Mapping (ORM)


Introduction to Vue Object-Relational Mapping (ORM)

Quite often applications use data that needs to be continually duplicated throughout the data store to provide referenced context to the data relationships inherent in the data.

For example, a Question and Answer website (like Quora and Stack Overflow for example) will have many questions posted by many authors, together with many answers posted by many authors to each individual question.

Author data such as author name can be associated with both asking questions and providing answers that needs to be repeatedly nested with each individual question or answer.

Apart from data redundancy, nested data in typical CRUD applications can quickly become exceedingly complex to manage - especially for data updates given each object of the original data throughout the entire data store requires the same update to be applied.

Custom logic will need to be developed to perform the update that will most likely become unwieldy to manage as the app scales or to debug if changes to the data schema occur after-the-fact, as is often the case during development.

There has to be a better way to develop and manage nested data relationships within the Vuex data store. And the answer is Vuex ORM - a Vuex plugin the provides Object-Relational Mapping capabilities and functionality to resolve the resultant difficulties of accessing and manipulating nested data resident in the store.

Nested data is split into mutually exclusive modules (relations) that are decoupled from each other whereby each module stores one and only one of the nested data relationships.

In the world of relational databases, this is called normalization. And in this sense, Veux ORM operationalizes the Vuex store no differently than normalized data tables within a relational database, and additionally, provides an encompassing ORM API to perform all standard CRUD operations upon the Vuex store state.

Vuex ORM also reduces boilerplate code by automatically setting up the mutations and getter required as well as simplifying your component code. For example, this.$store.state.commit("UPDATE_AUTHOR", { ... }) becomes Author.update({ ... }). Not only is that easier to write and to read, it adheres to the simplicity, intuitiveness and elegance of Vue.js in general.

Installation:

$ npm install @vuex-orm/core

$ yarn add @vuex-orm/core

Normalization Example:

To put Vuex ORM into a coding perspective, let's work through the simple Question and Answer application example noted at the outset. Questions are asked by individual authors that are answered many times by many individual authors.

The data object to be stored will resemble this structured pattern (whether the data object is created on-the-fly or as a response via an API request to a backend server):

[
  {
    id: 1,
    question: 'Question 1',
    author: { id: 1, name: 'Author 1' },
    answers: [
      {
        id: 1,
        answer: 'Answer 1',
        author: { id: 2, name: 'Author 2' }
      },
      {
        id: 2,
        answer: 'Answer 2',
        author: { id: 3, name: 'Author 3' }
      },
      // .....
    ]
  },
  {
    id: 2,
    question: 'Question 2',
    author: { id: 3, name: 'Author 3' },
    answers: [
      {
        id: 3,
        answer: 'Answer 3',
        author: { id: 4, name: 'Author 4' }
      },
      {
        id: 4,
        answer: 'Answer 4',
        author: { id: 1, name: 'Author 1' }
      },
      // .....
    ]
  },
  // .....
]

As we can readily see, author is duplicated numerous times for one single question object - the author of the question and the multiple authors of the multiple answers.

If we need to update author.name for Author 2 , we have to update every question asked by Author 2 AND update the nested answers of every question that Authorr 2 provided an answer for throughout the entire Vuex store.

Seems tedious and inefficient doesn't it? And prone to costly debugging time to ensure sufficiency, especially for edge cases. And if a coding error did slip through, the store would be compromised.

Let's now normalize the nested data array object into three separate objects in the store state (tables in relational database parlance) that comprise an items object whereby the ID of each item is the key and the item itself is the value:

{
  questions: {
    1: { id: 1, author_id: 1, question: 'Question 1' },
    2: { id: 2, author_id: 3, question: 'Question 2' },
    // .....
  },

  answers: {
    1: { id: 1, author_id: 2, question_id: 1, answer: 'Answer 1' },
    2: { id: 2, author_id: 3, question_id: 1, answer: 'Answer 2' },
    3: { id: 3, author_id: 4, question_id: 2, answer: 'Answer 3' },
    4: { id: 4, author_id: 1, question_id: 2, answer: 'Answer 4' },
    // .....
  },

  authors: {
    1: { id: 1, name: 'Author 1' },
    2: { id: 2, name: 'Author 2' },
    3: { id: 3, name: 'Author 3' },
    3: { id: 4, name: 'Author 4' },
    // .....
  }
}

Now, if we need to update name for Author 2, simply make a single update within the authors object in the state. Quick, efficient and far less prone to error.

Models:

So how do we go about creating the above normalized structure in the Veux state? We let Vuex ORM do its magic and we create Models that define the data schema of each top-level object as ES6 classes like so:

// Question Model
// @/models/Question.js
import { Model } from '@vuex-orm/core';
import Answer from './Answer';
import Author from './Author';

export default class Question extends Model {
  // Veux store module name
  static entity = 'questions';

  // model schema
  static fields () {
    return {
      id: this.increment(),
      author_id: this.attr(null),
      body: this.attr(''),

      // 'this.belongsTo' creates one-to-one inverse relationship
      // first arg is related Model, second arg is foreign key field name
      author: this.belongsTo(Author, 'author_id'),

      // 'this.hasMany' creates one-to-many relationship
      // first arg is related Model, second arg is foreign key of related model
      answer: this.hasMany(Answer, 'question_id')
    };
  }
}
// Answer Model
// @/models/Answer.js
import { Model } from '@vuex-orm/core';
import Author from './Author';

export default class Answer extends Model {
  // Veux store module name
  static entity = 'answers';

  // model schema
  static fields () {
    return {
      id: this.increment(),
      author_id: this.number(null),
      question_id: this.number(null),
      body: this.string(''),

      // 'this.belongsTo' creates one-to-one inverse relationship
      // first arg is related Model, second arg is foreign key field name
      author: this.belongsTo(Author, 'author_id')
    };
  }
}
// Author Model
// @/models/Author.js
import { Model } from '@vuex-orm/core';

export default class Author extends Model {
  // Veux store module name
  static entity = 'authors';

  // model schema
  static fields () {
    return {
      id: this.increment(),
      name: this.string('')
    };
  }
}

Let's discuss Models in greater detail. As we can see, each Model class extends Vuex ORM Model class.

Vuex ORM automatically creates an entities module and all Model data is stored in this namespace. static entity defines each Model's state that resides within the entities state namespace.

Using Author Model above as an example, the Author Model state can be accessed by store.state.entities.authors. NOTE: static entity is "Class Property" syntax that requires the @babel/plugin-proposal-class-properties compiler.

static fields () returns the Model data schema. this.attr() represents the most generic field type - also referred to as an Attribute. Other primitive type attributes are this.string(), this.number() and this.boolean().

The Attribute argument eg. this.attr(null) this.string('') is the default value of the field. this.increment() is a number field type that is auto-incremented. The id attribute is the primary key for the Model.

Further, we use two of the more common data relationships, one-to-one this.belongsTo() and one-to-many this.hasMany(). The first argument is the related Model and the second argument is the foreign key that references the primary key of related Model.

There are numerous data relationships discussed in the documentation https://vuex-orm.github.io/vuex-orm/guide/model/relationships.html

Database Registration:

As noted above, Vuex ORM automatically creates an entities module in the Vuex store. This is accomplished by registering the Models to a Vuex ORM database instance and then installing the database to Vuex as a plugin via the Vuex ORM install method.

// @/database/index.js
import { Database } from '@vuex-orm/core';
import Answer from '@/models/Answer';
import Author from '@/models/Author';
import Question from '@/models/Question';

// create new Vuex ORM database instance
const database = new Database();

// register Models with database instance
database.register(Answer);
database.register(Author);
database.register(Question);

export default database;
// @/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import VuexORM from '@vuex-orm/core'
import database from '@/database';

Vue.use(Vuex);

// create store and register database through Vuex ORM
const store = new Vuex.Store({
  plugins: [VuexORM.install(database)]
});

export default store;

The resulting state tree structure inside the Veux store will be:

{
  entities: {
    answers: {
      data: {}
    },
    authors: {
      data: {}
    },
    questions: {
      data: {}
    }
  }
}

CRUD Operations:

Now that we have created the Models, registered them with the Vuex ORM database and installed the Vuex ORM database within the Vuex store, we are ready to create, read, update and delete data in the store via the Vuex ORM API.

Insert:

The Vuex ORM API provides three methods for insertions, namely, insert, create and new that behave slightly differently depending upon the insertion context. insert will append new data to the existing data. create will replace all the existing data with the new data. In both cases, we pass an object containing the Model schema data items to be inserted with a data key:

Question.insert({
  data: [
    {
      body: 'Question 1',
      author_id: 1,
      author: {
        name: 'Author 1'
      }
    }
  ]
});

// 'store.state.entities' object
{
  questions: {
    data: {
      '1': {
        id: 1,
        author_id: 1,
        body: 'Question 1',
        answer: null,
        author: null
      }
    }
  },

  answers: {
    data: {}
  },

  authors: {
    data: {
      '1': {
        id: 1,
        name: 'Author 1'
      }
    }
  }
}

<Model>.new() will append a new record with all fields filled with the default values defined by the attribute argument.

Read:

There are a number of methods to retrieve data from the store. A sampling is presented below:

// get all authors
const authors = Author.all();
// get author with id of 3
const author = Author.find(3);
// get author with name 'Author 2'
const author = Author.query().where('name', 'Author 2').get();
// get authors by name descending
const authors = Author.query().orderBy('name', 'desc').get();
// get all answers to 'Question 1' with question and author data'
const answers = Answers.query().where('question_id', 1).with('questions').with('authors').get();

Update:

The update method will update existing data with a where condition representing the primary key or a Function determining the target data together with a data object with the updated data.

// update 'Author 1' name
Author.update({
  where: 1,  // primary key
  data: { name: 'new name' }
})
//update 'Author 2' answer to 'Question 1'
Answer.update({
  where(answer) => {
    return answer.question_id === 1 && answer.author_id === 2;
  },
  data: {
    { body: 'new answer' }
  }
})

Delete:

The delete method will delete existing data with an argument representing the primary key or a Function determining the target data.

// delete 'Author 1 with primary key'
Author.delete(1);
// delete 'Author 3' answer to 'Question 2'
Answer.delete((answer) => {
  return answer.author_id === 3 && answer.question_id === 2;

})

One can also delete all data for a specific Model and delete the entire entities state data.

// delete all authors
Author.deleteAll();
// delete all 'entities' data
store.dispatch('entities/deleteAll');

Additional Resources:

The above discussion has covered the basics of the Vuex ORM to familiarize you with how to create the store state tree for normalized data via Models and common CRUD API methods to access the normalized state data.

But Vuex ORM has deeper capabilities and broader possibilities that you may wish to consider such the Vuex ORM Axios plugin for example to perform backend server api calls and Vuex store commits in a single step: Author.api().get('api/authors/').

To take advantage of everything the Vuex ORM has to offer, take the time and dig deeper into official Vuex ORM documentation:
https://vuex-orm.github.io/vuex-orm/
https://github.com/vuex-orm/vuex-orm/