Oliver's Blog

DevExtreme - Real World Patterns - Applying CQRS

This post is part of a series describing a demo project that employs various real-world patterns and tools to provide access to data in a MongoDB database for the DevExtreme grid widgets. You can find the introduction and overview to the post series by following this link.

The demo application implements the CQRS pattern for data access. The acronym CQRS stands for Command Query Responsibility Segregation, a really intimidating mouthful. Many articles have been written about CQRS, and I find most of them rather complicated. The basic idea of CQRS is to view any interaction with data as either a query that retrieves information or a command that modifies information.

An Example

If you think about a server-rendered web application, this distinction makes sense quickly. Let’s say a user’s browser requests a web page that needs to show some data from storage. If you were, for instance, using an ORM-based data access model, the rendering process for the page would involve querying the required data from the database, instantiating the ORM types, and including the relevant data from those objects in the output. Since most server-rendering environments start a new rendering process for each client request, in a clean new context, the intermediate data would be “forgotten” at this stage.

Now let’s say the user has looked at the data in the web page and applied a modification using an editor in the browser. She submits the modification by clicking a button and the server receives a new request. Now the server needs to query the relevant objects again using the ORM system, find out what changes have been submitted by the user, apply these changes to the ORM instances and persist them.

In this example, both the querying and the modification of data have been using the same techniques, and the same model to represent the data, i.e. the ORM types. In the querying case, much of the ORM functionality has never been used, and much of the data that might have been queried by the ORM’s access layer (think joined types) may not actually have been included in the output. That’s hardly the most efficient possible approach to supplying the query data in the web page!

In the modification case, the ORM functionality has been used to apply the changes. Most ORMs have change tracking features, for instance using UnitOfWork, which can detect changes to ORM instances and possibly be clever about persisting only those parts of instances that have actually changed. In the example above, the modification data from the submit request was applied to the ORM instance, which triggers that change tracking process. However, this is really superfluous because details about the changes can already be found in the submit data itself. Logic on the client can easily make sure that only modified fields are included in the submit data, or that submit data is restricted according to the business process implemented by the specific page we’re talking about here. Altogether we’d have to say that while the modification part of the logic makes better use of the ORM functionality than the querying part, it still isn’t the most efficient implementation imaginable.

Frequently, articles about CQRS focus very much on differences between the models used to represent (and possibly store) data for querying vs. modification purposes. Of course this is an important point, since CQRS opens up the possibility of optimizing data representations for individual query or modification operations. However, I also find it important to stress that the implementation of both query and modification operations can be simplified a lot if we view each operation in its own context instead of having one API for everything.

For more detail on CQRS, Martin Fowler’s post is a good starting point, and it provides several links to further information at the bottom.

Demo Details

In the demo, I have implemented the CQRS pattern along very simple lines. I have created separate services, command-service and query-service, to implement commands and queries. The two services share the same database, which is not a given when using CQRS but a viable if simple choice. I have not yet implemented the Event Sourcing pattern for this demo, but I intend to do that for a future iteration.

The command service implements two actions, create and update, like this (from command-values.js):

this.add("role:entitiesCommand, domain:values, cmd:create", (m, r) => {
  const seneca = this;

  m = fixObject(m);

  seneca.act({
    role: "validation",
    domain: "values",
    cmd: "validateOne",
    instance: m.instance
  }, (err, res) => {
    if (err) r(err);
    if (res.valid) {
      db(db => db.collection("values").insertOne(m.instance, (err, res) => {
        if (err) r(null, { err: err });
        else r(null, { id: res.insertedId.toHexString() });
      }));
    }
    else {
      r(null, { err$: "invalid" });
    }
  });
});

this.add("role:entitiesCommand, domain:values, cmd:update", (m, r) => {
  // ... validation as above

  db(db => db.collection("values").
    updateOne({ _id: new ObjectID(m.id) },
      { $set: m.instance }, null, (err, res) => {
      if (err) r(null, { err: err });
      else if (res.modifiedCount == 0) {
        r(null, { err: "unknownid" });
      }
      else r();
    }));

  // ...
});

The models used by the two service endpoints are the same, representing the complete object. This is mainly due to the fact that my interfaces aim to be compatible with the APIs used by the DevExtreme data layer, and on that level there are no standard distinctions of different data formats. However, in reality it would be possible and likely to have additional modification command implementations, for instance for different business processes. What’s more, updates could be more complex than this, by modifying or creating more than one object type.

The query service also has two endpoints (from query-values.js):

this.add("role:entitiesQuery, domain:values, cmd:list", (m, r) => {
  m = fixObject(m);

  db(async (db) => {
    try {
      r(null, await query(db.collection("values"), m.params));
    }
    catch(err) {
      r(null, { err$: err });
    }
  });
});

this.add("role:entitiesQuery, domain:values, cmd:fetch", (m, r) => {
  db(async (db) => {
    try {
      const res = await db.collection("values").findOne({ _id: new ObjectID(m.id) });
      if (res) r(null, replaceId(res));
      else r(null, { err$: "unknownid" });
    }
    catch(err) {
      r(null, { err$: err });
    }
  });
});

Obviously, the fetch query uses a standard MongoDB function to retrieve the result object, so the model of this object corresponds to that used in the database. However, the list query returns data formatted according to the requirements of the DevExtreme data layer, and the structure of this data can vary depending on the parameters used for a list query. In a full-blown application, there would likely be additional querying functions that return various different structures, but you can still see from this example that the data model doesn’t have to be used for all query and command operations.

Both command and query service have in common that their implementation is simple and straight-forward, partially due to the separation of concerns CQRS prescribes.

Published Mar 30 2017, 02:01 PM by
Bookmark and Share

Comments

No Comments
LIVE CHAT

Chat is one of the many ways you can contact members of the DevExpress Team.
We are available Monday-Friday between 7:30am and 4:30pm Pacific Time.

If you need additional product information, write to us at info@devexpress.com or call us at +1 (818) 844-3383

FOLLOW US

DevExpress engineers feature-complete Presentation Controls, IDE Productivity Tools, Business Application Frameworks, and Reporting Systems for Visual Studio, along with high-performance HTML JS Mobile Frameworks for developers targeting iOS, Android and Windows Phone. Whether using WPF, ASP.NET, WinForms, HTML5 or Windows 10, DevExpress tools help you build and deliver your best in the shortest time possible.

Copyright © 1998-2017 Developer Express Inc.
All trademarks or registered trademarks are property of their respective owners