JoinJS is a JavaScript library to map complex database joins to nested objects. It's a simpler alternative to a full-blown Object-Relation Mapper (ORM), and gives you direct control over your database interactions.
Traditional ORMs introduce a thick layer of abstraction between objects and database tables. This usually hinders, rather than helps, developer productivity. In complex use cases, it is difficult enough to devise efficient queries, but with ORMs you also have to teach them to generate the same query. It takes extra time to do this and you may not be able to produce the same query. In the worst case scenario, the ORM may hit the database multiple times for something that you were able to do in a single query.
JoinJS takes a much simpler and straightforward approach inspired by a popular Java mapping framework called MyBatis (see the post on MyBatis vs. other ORMs. You can use any database driver or query builder (such as Knex.js) to query your database, however you use JoinJS to convert the returned results into a hierarchy of nested objects.
Suppose you have a one-to-many relationship between a Team
and its Players
. You want to retrieve all teams along with their players. Here's the query for to do this:
SELECT t.id AS team_id,
t.name AS team_name,
p.id AS player_id,
p.name AS player_name
FROM teams t
LEFT OUTER JOIN players p
ON t.id = p.team_id;
Assume that this query returns the following result set:
let resultSet = [
{ team_id: 1, team_name: 'New England Patriots', player_id: 1, player_name: 'Tom Brady' },
{ team_id: 1, team_name: 'New England Patriots', player_id: 2, player_name: 'Rob Gronkowski' },
{ team_id: 2, team_name: 'New York Jets', player_id: 3, player_name: 'Geno Smith' },
{ team_id: 2, team_name: 'New York Jets', player_id: 4, player_name: 'Darrelle Revis' }
];
You can use JoinJS to convert this result set in to an array of teams with nested players:
[
{
id: 1,
name: 'New England Patriots',
players: [
{ id: 1, name: 'Tom Brady' },
{ id: 2, name: 'Rob Gronkowski' }
]
},
{
id: 2,
name: 'New York Jets',
players: [
{ id: 3, name: 'Geno Smith' },
{ id: 4, name: 'Darrelle Revis' }
]
}
]
To teach JoinJS how to do this, you must create two result maps that describe your objects:
const resultMaps = [
{
mapId: 'teamMap',
idProperty: 'id',
properties: ['name'],
collections: [
{name: 'players', mapId: 'playerMap', columnPrefix: 'player_'}
]
},
{
mapId: 'playerMap',
idProperty: 'id',
properties: ['name']
}
]
Once you have created these result maps, you can simply call JoinJS to convert your result set in to objects:
let mappedResult = joinjs.map(resultSet, resultMaps, 'teamMap', 'team_');
That's it! It doesn't matter how deep or complex your object hierarchy is, JoinJS can map it for you. Read the documentation below for more details. You can find more examples in the test suite. Follow the step-by-step tutorial for a hands-on introduction. Once you have mastered the basics, check out the Manage My Money project to see how you can build a full-fledged application complete with a front-end using JoinJS and other useful libraries.
$ npm install --save join-js
Don't forget the dash in the package name (join-js
).
Using with ES5:
var joinjs = require('join-js').default;
Using with ES6:
import joinjs from 'join-js';
ResultMaps are used to teach JoinJS how to map database results to objects. Each result map focuses on a single object. The properties of a ResultMap are described below. You can find several examples in the test suite.
mapId {String}
- A unique identifier for the map
createNew {function} (optional)
- A function that returns a blank new instance of the mapped object. Use this property to construct a custom object instead of a generic JavaScript Object
.
idProperty {String | Object | Array(String|Object)} (optional)
- specifies the name of the id property in the mapped object and in the result set. Default is id
, which implies that the name of the id property in the mapped object as well as the column name in the result set are both id
. If the two names are different, then you must specify the Object form, e.g. {name: 'id', column: 'person_id'}
.
name
- property that identifies the mapped objectcolumn
- property that identifies the database record in the result setIn addition, you can specify composite key by passing an array of string and/or object, e.g. ['person_id', {name: 'language', column: 'language_id'}]
properties {Array} (optional)
- names of other properties. For any property that has a different name in the mapped object vs. the result set, you must specify the object form, e.g. {name: 'firstName', column: 'first_name'}
. The properties of the object form are:
name
- property name in the mapped objectcolumn
- property name in the result setassociations {Array} (optional)
- mappings for associations to other objects. Each mapping contains:
name
- property name of the association in the mapped objectmapId
- identifier of the result map of the associated objectcolumnPrefix (optional)
- a prefix to apply to every column of the associated object. Default is an empty string.collections {Array} (optional)
- mappings for collections of other objects. Each mapping contains:
name
- property name of the collection in the mapped objectmapId
- identifier of the result map of the associated objectscolumnPrefix (optional)
- a prefix to apply to every column of the associated object. Default is an empty string.JoinJS exposes two very simple functions that give you the full power to map any result set to one of more JavaScript objects.
Maps a resultSet to an array of objects.
resultSet {Array}
- an array of database resultsmaps {Array}
- an array of result mapsmapId {String}
- mapId of the top-level objects in the resultSetcolumnPrefix {String} (optional)
- prefix that should be applied to the column names of the top-level objectsReturns an array of mapped objects.
This is a convenience method that maps a resultSet to a single object. It is used when your select query is expected to return only one result (e.g. SELECT * FROM table WHERE id = 1234
).
resultSet {Array}
- an array of database resultsmaps {Array}
- an array of result mapsmapId {String}
- mapId of the top-level object in the resultSetcolumnPrefix {String} (optional)
- prefix that should be applied to the column names of the top-level objectsisRequired {boolean} (optional)
- is it required to have a mapped object as a return value? Default is true
.Returns the mapped object or null
if no object was mapped.
Throws a NotFoundError
if no object is mapped and isRequired
is true
.