May 31, 2015

AngularJS + MongoDB: Goodbye middle tier? (part 2 of 2)


With the advent of client side JavaScript frameworks, it seems like the middle tier has become Java’s last resort. But as a Java developer myself, I have to admit there are compelling alternatives to a full-blown enterprise server. For this blog post, I want to push this thought to the limit and try out whether we really need the middle tier at all… Please read part 1 here first.

Working with AngularJS

I didn’t implement any fancy functionality in this example application; I didn’t have to.

In order to implement the payments “popup” windows, I used ngDialog as a small helper framework.

Working with Restangular

After proper setup, working with Restangular on a RESTHeart backend really is a joy as one fits the other like a glove:
  • Restangular provides CRUD actions (HTTP verbs) on a route provided which happen to match a RESTHeart / MongoDB collection.
  • Restangular works with attached (“restangularized”) objects as entities which directly implement CRUD functionality.
  • Restangular functions are implementes as promises for a very clean, non-blocking asynchronous programming model.
Let’s see this in action.

GET without parameter

In the CustomerListCtrl, when we request all customers, we do
angular.module('restangularOnMongoDbApp')
    .controller("CustomerListCtrl", function ($scope, Restangular) {
        Restangular.all("customers").getList().then(function(entities) {
          $scope.entities = entities;
        });
    });
Restangular is the dependency-injected Restangular service.

Restangular.all("customers").getList() starts the asynchronous GET request to fetch all entities in the customers collection, then() takes the callback which here sets the $scope model.

The same principles apply to every CRUD operation.

GET with parameter

Here’s how CustomerEditCtrl is implemented:
angular.module('restangularOnMongoDbApp')
    .controller('CustomerEditCtrl', function ($scope, $routeParams, $location, Restangular) {
        $scope.save = function () {
            ...
        };

        if ($routeParams.id === "new") {
            $scope.entity = Restangular.one("customers");
        }
        else {
            Restangular.one("customers", $routeParams.id).get().then(function (entity) {
                $scope.entity = entity;
            }, function (response) {
                $location.path("/customers").search({errorNotFound: $routeParams.id});
            });
        }
    });
In addition to the Restangular service, we also inject $routeParams to retrieve “#” URL parameters and $location to trigger page redirect in the AngularJS way. Please refer to the AngularJS documentation for details.

The edit page which is backed by this controller should behave like so:
  • With id = “new”, a new entity is created.
  • With any other id, the requested entity is fetched.
  • If the entity is not found, the user is redirected to the list view and an error message is shown.
Here, a couple of interesting operations are implemented:
  • Retrieving the one entity with the id provided happens through Restangular.one("customers", id).
  • Initializing a new entity is accomplished with Restangular.one("customers"). The returned object is “restangularized” and thus can be used in the save() function later.
  • Restangular operations optionally take a second function as an additional parameters for error handling. In this listing, this is used for the “redirect on error” functionality.

Write operations

Here’s the save() function of CustomerEditCtrl:
$scope.save = function() {
    $scope.entity.save().then(function() {
        $location.path("/customers");
    });
};
As mentioned earlier, “restangularized” objects come with a bunch of useful additional functions. Most prominently, they feature CRUD functions for HTTP verbs, plus an additional save() which would either trigger PUT (no id set yet) or POST (when an id is present). Note however that RESTHeart accepts PUT as an alias for POST anyway.

It’s important to remember that all Restangular functions work asynchronously and their outcome is processed in the promise callback function. Thus, subsequent actions must be implemented in the callback function.

For instance, if the redirect in above function would just be another procedural call after $scope.entity.save(), then, as the save() function returns immediately, redirect would possibly happen before save() has been committed. The outcome would then render outdated information.

With these few principles in mind, really any CRUD operation can be implemented with a simple Restangular function call.

Working with RESTHeart

RESTHeart’s API is really simple and straightforward if you get your head around the idea of RESTful CRUD. There’s actually no magic I could teach you here.

The documentation is also OK. Search for error codes and missed HTTP headers if you run into trouble.

Working with MongoDB

Thanks to RESTHeart, you will barely get in touch with MongoDB’s internals. However, you will have to adopt your thinking to the MongoDB way and to the world of document-oriented databases; this is especially true if you come from the relational DB world.

No schema – no constraints

MongoDB doesn't support the notion of a schema, hence there are no constraints, not even “not null” constraints nor data types. Without a middle-tier, you can literally put any kind of trash in your database.

For instance, in the example application, the only way really to mark the customer’s “name” field as “requried” is by applying the respective UI component attribute:
<input id="form:name" type="text" name="form:name" ng-model="entity.name" title="Name" required/>
However, UI layer constraints are completely unsafe as the user can easily disable them by manipulating HTML /JavaScript code.

In the same way, the user can put a value of any kind into the account’s “date” field; even an arbitrary String.

Transaction boundaries: Don’t think relational!

A document-based data store such as MongoDB relies on embedded documents rather than relations established with foreign keys; hence, there is also no normalization, and the smallest transactional entity is the whole document, possibly consisting of layers upon layers of embedded documents! This has a huge impact on the overall system architecture and user experience.

In the example application for instance, a customer holds its payments as embedded documents:
{
    "_id": ObjectID("5568e53e11c14fee4e39d469"),
    "name": "Max",
    "address": "First Street",
    "city": "Los Angeles",
    "payments": [
        {
            "amount": "100",
            "date": "05/31/2015"
        },
        {
            "amount": "200"
        }
    ],
    "_etag": ObjectID("5568e53e11c14fee4e39d46a")
}
Thus, the transaction comprises the entire customer, not individual payments. For the user experience, this has two consequences:
  • A payment always has to exist as a child of a customer.
  • A payment cannot be manipulated independently of its customer. The customer and all its associated payments must be persisted as a whole.
In the example application, this is reflected by the navigation behavior: CRUD operations on a payment don’t open in a separate page; rather, they open a popup and are executed within the page of a customer. This helps making the user aware that in order to commit his changes, he has to save the entire customer at the end.

This makes the view truly stateful, which is the opposite of a stateless view, as suggested for a RESTful navigation. Yes, even though the backend API is RESTful, the UI navigation is not.

This design is not bad per se; otherwise, MongoDB wouldn’t be that successful. However, it’s very important to judge whether this design is appropriate for your use case. The span of transactional boundaries is a business decision. If you need per-entity-boundaries, MongoDB is not the right choice.

Also, you shouldn’t try to trick the system, e.g. by introducing pseudo-foreign key properties into documents. You still wouldn’t be able to guarantee ACID transactions, and even less so in a distributed environment where MongoDB is supposed to shine.

Conclusion

Of course, due to security concerns, real world applicability of direct access to the backend from client-side GUI approaches zero, but that is not what this article aims to show. Rather, it’s an interesting thought experiment showing that the more simplified and lightweight a service interface is, the more ease of development is increased, and that this is especially true for a RESTful API.

Indeed, the whole tech stack is really optimized for rapid application development:
  • AngularJS provides a mature ecosystem for powerful abstractions, such as Restangular.
  • RESTHeart makes working with MongoDB really easy.
  • MongoDB works great as a simplistic data store for junks of data.
As for the individual components used in this application:
  • Setting up Restangular for use with RESTHeart is not trivial, but after that, its straightforward programming model really shines. I’d recommend this for any REST API access with AngularJS.
  • RESTHeart works really well with MongoDB. It also covers more sophisticated needs such as concurrency control and security. As of May 2015, its homepage still suffers from partially incomplete documentation though.
  • MongoDB really is the show-stopper in this example tech stack, even beyond the obvious security concerns. Its conceptual differences to relational DBs will be a huge impediment as soon as you need constraints or more fine-grained transaction control which I assert is true for typical enterprise projects. There are quite a few articles out there discussing the pros and cons of MongoDB for certain scenarios, e.g. here. Don’t take a rash decision on the DB you want to use.
The example application proved nonetheless that building an application without middle tier, based on AngularJS + MongoDB, is possible and very straightforward. Despite not being production ready, this tech-stack could still be interesting to quickly build an internal document store UI or to rapidly prototype an AngularJS application. As the interface to the backend is pure REST, you could later introduce a thin “proxy” middle tier serving the same RESTful API. I think it would also be very interesting to re-implement the application with a “RESTified” relational database so as to avoid the restrictions added by MongoDB.

At least it proved that keeping the middle tier thin is a very good means to increase ease of development. For server side developers, e.g. Java EE developers, this is especially impressive. We should really embrace the changes initiated by more sophisticated client-side UIs and more powerful clients and aim for lightweight middle tier implementations. This includes, in my opinion, to balance a full-blown Java EE server against e.g. a very lightweight Node.js Express server. There’s just no more “one size fits all” solution.

Do you agree with any of my assertions? Did you find this article interesting? Please let me know your opinion in the comments section below.

Feel free to view the demo project’s complete source code on its GitHub repository.


Pages: 1 2

2 comments:

  1. Very good article. FYI, RESTHeart's documentation has moved here: https://softinstigate.atlassian.net/wiki/display/RH/Documentation

    ReplyDelete