Quest for the Best Nest

It comes as a shock to nobody that the data shared over the internet, from clients to servers and back again, is mind-bogglingly complex and large. Even within a simple application featuring only a few models, returning different iterations of server side data to the client can result in an unnecessarily intensive CRUD operation. Models feature a variety of different attributes and nested associations that connect in the backend, but not all of these associations need to be displayed to their fullest extent in the user interface. Making fewer and more efficient queries of a program’s database can improve the program’s performance, but which method of returning nested data yields the best results?

With a few simple lines of code, Ruby on Rails and ActiveRecord streamline the process of nesting data, creating associations between models, and querying databases with built-in methods and conventions. The has_many, belongs_to, and has_one macros, for instance, define how models are related to one another and present a library of helpful methods. An example of these associations might be present in a gardening app, which has a user model, a plant model, and plant care instructions model. A user has many plants, a plant belongs to a user and has many care instructions, and an instruction belongs to a plant. A single user would then have many plant care instructions for their entire garden overall. Without Active Record, CRUD operations can be clunkier and leave more room for errors when querying the database:

Without Active Record, the user’s id must be found manually.

With Active Record, the user’s information is reliable and associated with the plant automatically.

However, even with Active Record’s built-in associations, there are still challenges with passing too much data to the user, as well as passing nested data (all of the user’s plant care instructions, for instance). Fortunately, Active Record Serializers and the includes method can assist in a variety of these scenarios.

The includes method is one way to cut down on database querying, and can be used in a model’s controller in tandem with RESTful routing. This purposely versatile and broad method can be used for returning associated data from either the has_many or belongs_to model. The includes method can be used in the plant controller to return the associated user, or in the user controller to return the associated plants. Without Active Record or the includes method, either of these scenarios would have to query the database many times:

SELECT `users`.* FROM `users` ORDER BY `users`.`id`
SELECT `plants`.* FROM `plants` WHERE `plants`.`user_id` = 1
SELECT `plants`.* FROM `plants` WHERE `plants`.`user_id` = 2
SELECT `plants`.* FROM `plants` WHERE `plants`.`user_id` = 3

Or conversely,

SELECT `plants`.* FROM `plants` ORDER BY `plants`.`id`
SELECT `users`.* FROM `user` WHERE `users`.`plant_id` = 1
SELECT `users`.* FROM `user` WHERE `users`.`plant_id` = 2
SELECT `users`.* FROM `user` WHERE `users`.`plant_id` = 3

These queries indicate that associated data must be grabbed one by one. Instead of this, adding the includes method in the controller makes only two requests of the database–one to load the data from plants or users, and one to load the associated data linked by the foreign key.

This query would return all the plants and their associated user.

This query would return all users and their associated garden of plants.

The disadvantage of using the include method is that it requires further tailoring using the only method to avoid returning excessive data to the client. The Controller code quickly becomes messy and overreaching with all of the extra customization.

Fortunately, Rails provides Ruby users with the Active Model Serializer: a specialized class that offers a framework for tailoring and returning the exact data needed by the client. Serializers work with models’ associations and attributes specifically in order to simplify and compartmentalize the controller actions. Using Serializers to return associated or nested data allows Ruby on Rails code to be practical and modular. The Controller and the Serializer can do their jobs cleanly.

Active Model Serializers allow programmers to more easily customize the data they want to access from each model, instead of refining the data in the Controller or the frontend. In the following example, the User Serializer returns only the user’s username, leaving out other model attributes such as the id, password, and any created_at or updated_at timestamps. Using has_many and belongs_to macros, Serializers also automate the return of associated data between models. By bringing in a Plant Serializer, we can link the user and their plants, as well as trim down the information returned from both models.

Serializers can also return customized information through customized routing and methods. For instance, to return specific attributes of plants to the frontend, programmers might create a custom Serializer to return only planting dates:

A route is established in the config file, and then called upon in the plants controller:

Serializers can also handle returning data connected by a has_many through relationship (when the connecting Model features a belongs_to relationship to the two other models) simply by using the has_many macro. 

Unfortunately, Serializers have a failsafe put in place to prevent top models from returning nested data two levels deep (when the connected model features a belongs_to relationship to one model and a has_many relationship to the other). In our case, the User Serializer would be prevented from returning data from the Plant Care Instructions Model. Active Model puts this in place to prevent complexity and errors and improve performance. In a case like this, programmers would indeed have to use a Serializer for the first tier of returned data and an include method in the controller to return the second tier.

There are many considerations to take into account when returning nested data in Rails. The variety of options do require programmers to get specific about their final product, however, that specificity allow for flexibility, scaling, and improved performance when working with so much data.

Sources:

Gusto Engineering Blog

Active Record Guide