CompanyDecember 16, 2018

Introducing DataStax Node.js Mapper for Apache Cassandra®

Introducing DataStax Node.js Mapper for Apache Cassandra®

I'm excited to announce the release of the new Node.js Object Mapper for Apache Cassandra. This new object Mapper lets you interact with your data like you would interact with a set of documents.

Mapper Features

Getting Started

The Mapper is provided as part of the driver package.

const cassandra = require('cassandra-driver');
const Client = cassandra.Client; const Mapper = cassandra.mapping.Mapper;

const client = new Client({ contactPoints, localDataCenter, keyspace });

Create a Mapper instance and reuse it across your application. You can specify your model properties and how those are mapped to table columns can be defined in the MappingOptions.

const mapper = new Mapper(client, {
   models: { 'Video': { tables: ['videos'] } } });

A ModelMapper contains all the logic to retrieve and save objects from and to the database.

const videoMapper = mapper.forModel('Video');

Internally, the Mapper contains a single ModelMapper instance per model in your application, you can call mapper.forModel(name) each time you need a model mapper with no additional cost.

To retrieve a single object, use get() method of the ModelMapper. Note that execution methods return a Promise. On async functions you can await for the Promise to be fulfilled.

const video = await videoMapper.get({ videoId: myVideoId });

Use find() method to filter by one or more primary keys.

const userVideos = await videoMapper.find({ userId: myUserId });

Insert an object using insert() method.

await videoMapper.insert({ videoId, userId, addedDate, name });

Update an object using update() method.

await videoMapper.update({ videoId, userId, addedDate, name: newName });

Delete an object using remove() method.

await videoMapper.remove({ videoId });

Use find() in combination with relational operators on a field

// addedDate greater than a value await videoMapper.find({ userId, addedDate: q.gt(myDate) });

Keep in mind that both Mapper and Client instances are designed to be long lived. If you don't want to maintain both instances on separate fields, you can access the Client instance using the Mapper client property. For example, you can shutdown your Client before exiting your application by calling:

mapper.client.shutdown();

You can look at the Queries documentation for more examples of retrieving, saving objects and using query operators.

Defining Mappings

You can define how your application model is represented on your database by setting the MappingOptions.

In general, you should specify the table name(s) and the naming convention you are using on the CQL objects and your application models.

const UnderscoreCqlToCamelCaseMappings = cassandra.mapping.UnderscoreCqlToCamelCaseMappings;

const mappingOptions = {
   models: {
      'User': {
         tables: ['users'],
         mappings: new UnderscoreCqlToCamelCaseMappings()
      }
   }
};

// Create the Mapper using the mapping options const mapper = new Mapper(client, mappingOptions);

When a certain column or property doesn't match the naming convention, you can specify each column name and property name key-value pair, for example:

const mappingOptions = {
   models: {
      'User': {
         tables: ['users'],
         mappings: new UnderscoreCqlToCamelCaseMappings(),
         columns: {
           // The naming convention would be 'first_name': 'firstName'.
           // We override it here
           'firstname': 'firstName'
         }
      }
   }
};

Mapping to Multiple Tables

In order to get more efficient reads, you often need to denormalize your schema. Denormalization and duplication of data is a common data modeling pattern with Apache Cassandra and DataStax Enterprise.

The Mapper supports mapping a single model to multiple tables or views. These tables will be used for mutations when using insert(), update() and remove() methods, and the most suitable table or view will be used according to the keys specified.

To use multiple tables/views with the same model, specify the names in the MappingOptions. In the following example, the tables "videos", "user_videos" and "latest_videos" from the killrvideo schema are accessed using a single model "Video":

const mappingOptions = {
   models: {
      'Video': {
         tables: [ 'videos', 'user_videos', 'latest_videos' ],
         mappings: new UnderscoreCqlToCamelCaseMappings(),
         columns: {
            'videoid': 'videoId',
            'userid': 'userId'
         }
      }
   }
};

Then, when invoking ModelMapper methods multiple tables will be affected for mutations.

// The following invocation will create a batch inserting a row on each of the tables
await videoMapper.insert({ videoId, userId, addedDate, yyyymmdd, name });

Note that when working with multiple tables with the Mapper, by grouping multiple queries in a batch, you are moving the complexity of dealing with each individual mutation to the coordinator server node. If you are updating a large number of tables, it may be more beneficial to use individual ModelMapper instances per each table and execute the operations in parallel potentially using different coordinators.

You can read more about Defining mappings between your models and the tables in the documentation.

A look under the hood

If you are interested to see how the Mapper works, this section is for you!

Object mappers perform two main tasks:

  • Transform an object instance into a database query and parameters.
  • Map row results into object instances.

These operations have processing costs that can impact the application overall performance, specially considering the single-threaded nature of Node.js. Given that these operations are repeated each time a Mapper execution method is called, we looked for ways to optimize it.

We introduced an internal mechanism to cache the query, the function to obtain the parameters and the function to map results into object instances based on the Object shape. An object shape is determined by its properties and the operators used.

Using the following Mapper execution as an example:

await videoMapper.find({ userId: myUserId })

We can see that it will always produce the same query:

SELECT * FROM user_videos WHERE userid = ?

And the instructions needed to map the rows to video objects instances will be the same.

With that in mind, we generate the CQL query string alongside the JavaScript code needed to map the results (that is compiled into a function using vm module) and we add it to a radix tree, where the key is composed by the object shape. That way we can reuse the same CQL query and mapping function by only inspecting the object shape.

Wrap Up

The new Mapper is available in the DataStax Node.js Driver for Apache Cassandra version 4.0 and in the DataStax Enterprise Driver 2.0, included in the npm packages.

Check out the Mapper documentation for more information and there are code samples in the repository.

We hope the new Mapper enables developers to write applications more easily. Thanks to all who contributed code, wrote documentation, made feature requests and reported bugs. We encourage you to stay involved:

Share

One-stop Data API for Production GenAI

Astra DB gives JavaScript developers a complete data API and out-of-the-box integrations that make it easier to build production RAG apps with high relevancy and low latency.