Thinky and RethinkDB

If you're looking for alternatives to writing queries and modeling your data using ReQL (the RethinkDB query language), you might want to consider looking at Thinky, an open-source ORM (Object Relational Mapper) designed for RethinkDB. Thinky provides a number of features for defining schema, querying, and adding relationships to your models. The ORM uses the same syntax as RethinkDB's Node.js driver, which makes it a great alternative to developing applications using ReQL.

In this article, we will primarily concentrate on creating schemas and defining relations between your models.

The schema

If you’re already familiar with building schemas in Mongoose, an ODM (Object Document Mapper) for MongoDB, then Thinky will look familiar since the creation of field names, validations, and queries are similar. If you are not familiar with Mongoose, then read our articles introducing you to it here and our article covering the latest version: Mongoose 4.

Overall, Thinky and Mongoose work on similar principles, which is to provide you with an efficient and object-oriented way to model your data. The difference between the two is that an ODM like Mongoose concerns itself with the structure of data within documents or tables, while an ORM like Thinky goes further by modeling the relationships between them.

Thinky is a lightweight Node.js ORM that uses an alternative version of RethinkDB’s Node.js driver, rethinkdbdash, on the backend that has the added bonus of connection pools. While the ORM is not as fully featured as Mongoose, it enforces schema validations and creates indexes and tables automatically out of the box. One of the most useful features, however, is its four predefined relation methods that help you create relations between models, which we will discuss in more detail later.

To give you a small example of the similarities between Mongoose and Thinky, let’s look at a schema in Thinky and Mongoose in the context of creating characters and houses from the popular series of novels and TV series “Game of Thrones".

Thinky schema

var thinky = require("thinky");  
var type = thinky.type;  
var r = thinky.r;

var Character = thinky.createModel("Character", {  
  id: type.string(), // or String
  name: type.string(),
  createdAt: type.date().default(r.now())
// using RethinkDB’s r command through rethinkdbdash
// to set the date on the server during creation
});

var House = thinky.createModel("House", {  
  id: type.string(),
  houseName: type.string(),
  characterId: type.string()
});

Mongoose schema

var mongoose = require("mongoose");  
var Schema = mongoose.Schema();

var CharacterSchema = Schema({  
  _id: String, // or {type: String}
  name: String,
  createdAt: {type: Date, default: Date.now}
// uses JavaScript’s Date.now() method
});

var HouseSchema = Schema({  
  _id: String,
  houseName: String,
  characterId: String
});

var Character = mongoose.model("Character", CharacterSchema);  
var House = mongoose.model("House", HouseSchema);  

Looking at the example above, the advantage of using Thinky is that it defines our schemas, creates a model and assigns it a name, while creating the necessary tables within RethinkDB simultaneously. Whereas with Mongoose, you must define the schema then define the model.

Thinky's documentation provides some good schema examples with the different field type options (String, Number, Date, Array, Boolean, etc.) and chainable methods that you can use to define fields further.

Relationships and joining documents

An appealing feature of Thinky is its ability to help you assign relations between your models. The documentation for creating relations is not entirely clear; therefore, you might have to take a deep dive into its github issues for clarification. This section intends to explain how to define relationships between models and highlights some of the peculiarities and pitfalls you may encounter when using them. We will also provide you with a brief look at how they are interpreted in RethinkDB.

RethinkDB's Node.js driver natively allows us to define many-to-many and one-to-many relationships by using the eqJoins and zip ReQL commands. For a brief overview of joining tables in RethinkDB using the eqJoin command, we've provided an overview with examples here.

In general, the eqJoins and zip commands will take two tables, join them on foreign and primary keys (eqJoin), and merge them together returning you the joined documents. The syntax for this query would be as follows:

r.table("House").eqJoin("characterId", r.table("Character")).zip()  

However, leveraging the power of Thinky, we are provided with four predefined relation methods (hasOne, hasMany, belongsTo, and hasManyAndBelongsTo) that write all of the ReQL commands for us using RethinkDB’s joins capabilities behind the scenes. All we need to do is provide Thinky with the primary and foreign keys so that it knows where the joining should occur.

To define the relationships between our character and house models, all we have to write is the following:

Character.hasOne(House, "house", "id", "characterId");  
House.belongsTo(Character, "character", "characterId", "id");  

Using the relation methods, we are able to choose the tables (or models) where the relations shall occur (Character and House). Then, we create a custom field name for that relationship (house and character) and provide the primary and foreign keys from the tables where each should be joined.

If we need to add secondary indices, Thinky also makes this painless, since it does all the heavy lifting for you. The method ensureIndex, which under the hood wraps RethinkDB's indexCreate and indexWait commands together, checks to see if the index you defined exists and if it doesn't, creates it for you. Adding an index in our data just needs the following:

Character.ensureIndex("name");  
House.ensureIndex("houseName");  

Now that we have prepared our models, relations, and indices, we are ready to start inserting data into our database. The only information that we must include is the name of the character and the name of the house they belong to.

{
  "createdAt": "2016-07-26T05:20:31.381Z",
  "id": "dc47cc90-f629-499b-8c45-efb1e987717c",
  "name": "Robert Baratheon"
}
{
  "houseName": "Baratheon",
  "id": "774b00a8-193b-468f-9784-919d56337baf",
  "characterId": "dc47cc90-f629-499b-8c45-efb1e987717c"
}

Since our hasOne relationship points to characterId as the foreign key field in our House table, Thinky will automatically populate that field with the id of the appropriate character from the Character table when both tables are saved using the saveAll() method.

In order to save these documents without running the risk of not inserting foreign keys, we must use the saveAll() method on our character and then pass in an object with the name of hasOne relationship we defined previously.

character.house = house;  
character.saveAll({house: true}).then(function(data) {...});  

Therefore, character.house joins the Character and House tables together. We assign it to house which will be the key where our house document will be stored when it is returned. Within the saveAll() method, we insert the name the document we want to join that was defined in our hasOne relation (house). Then, we set it to true in order to tell Thinky to save the house and the character tables together.

After we've inserted our documents, we can use the getJoin query method to return the joined documents.

Character.getJoin({house: true}).run().then(function(result) {  
    console.log(result);
});

In our query we call the getJoin method, which is also given an object with the name of the field we created in the hasOne relationship. It is also set to true so that Thinky knows which table to join to provide you the correct data. When we run this query, we get the following result:

{
  "createdAt": "2016-07-26T05:20:31.381Z",
  "house": {
    "houseName": "Baratheon",
    "id": "774b00a8-193b-468f-9784-919d56337baf",
    "characterId": "dc47cc90-f629-499b-8c45-efb1e987717c"
  },
  "id": "dc47cc90-f629-499b-8c45-efb1e987717c",
  "name": "Robert Baratheon"
}

If we ran the same query using RethinkDB's eqJoin command, it will produce a similar result. The only difference is that in the result above, the key house includes nested data from our joined table, whereas with eqJoin the data wouldn't be nested. RethinkDB's eqJoin command might provide a better solution than Thinky's getJoin method if you combine eqJoin with the zip and without commands. These commands will merge your documents together rather than storing them as nested data. (Refer to our article on RethinkDB joins for a more in depth discussion.)

Joining multiple documents

Sometimes you have tables that have documents containing the same foreign key id. Using a hasMany relation is the optimal solution, which has the same syntax as the hasOne relation. For our use case, we might consider that some characters belong to two houses (i.e. Jon Snow) and what hasMany will do is modify our house object into an array of objects.

Exclaimer: If you have two documents with the same foreign key and a hasOne relationship, Thinky will throw an error stating that you have more than one document with the same foreign key, so make sure that you have a hasMany relation defined beforehand.

So, in the House table we might have two houses with the same characterId that refer to Jon Snow as in the following:

{
  "houseName": "Targaryen",
  "id": "c129274e-44f6-4224-9c30-e7112f596121",
  "characterId": "f39e479e-5961-4dc8-b763-5c0397279c6a"
},
{
  "houseName": "Stark",
  "id": "4c459d81-3162-4bc7-acca-4bb5467ccd13",
  "characterId": "f39e479e-5961-4dc8-b763-5c0397279c6a"
}

Querying our database for Jon Snow, using the same query as we executed above, will produce the following:

{
  "createdAt": "2016-07-26T05:20:31.381Z",
  "house": [
    {
      "houseName": "Stark",
      "id": "4c459d81-3162-4bc7-acca-4bb5467ccd13",
      "characterId": "f39e479e-5961-4dc8-b763-5c0397279c6a"
    },
    {
      "houseName": "Targaryen",
      "id": "c129274e-44f6-4224-9c30-e7112f596121",
      "characterId": "f39e479e-5961-4dc8-b763-5c0397279c6a"
    }
  ],
  "id": "f39e479e-5961-4dc8-b763-5c0397279c6a",
  "name": "Jon Snow"
}

Thus, we are given an array of houses with Jon Snow's id as the characterId in the house array, which produces a nice result that will allow us to manipulate the data further. We did not have to change any of our queries, or our code, to implement the hasMany relation. This is nice when you want to write an application fast and without too many obscurities.

Get Thinky

So, we've looked at some of the ways we can model, query, create relations between tables, and store data using Thinky. While it does not have all the capabilities that Mongoose has for MongoDB, the author is actively adding new features in order to increase its functionality and usability. Overall, it provides you with a few shortcuts to create tables and relations between them, which reduces the amount of code you'd have to write if you decided to only use ReQL. Also, it makes your code readable and helps produce consistent results.

Image by Kalen Emsley