Skip to content

Commit

Permalink
Allow simple strings to define properties
Browse files Browse the repository at this point in the history
This makes the result maps less verbose. For situations where property
name and column name are different, the object form specification is
still supported.
  • Loading branch information
nareshbhatia committed Aug 7, 2015
1 parent 376bf33 commit f9661f8
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 88 deletions.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

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.

## Motivation
ORMs generally 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.
## Motivation: Direct, no-nonsense control over your database
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 Java library called [MyBatis](http://mybatis.github.io/mybatis-3/). You can use any database driver or query builder (such as [Knex.js](http://knexjs.org/)) to query your database, however you use JoinJS to convert the returned results to a hierarchy of nested objects.
JoinJS takes a much simpler and straightforward approach inspired by a popular Java mapping framework called [MyBatis](http://mybatis.github.io/mybatis-3/) (see the post on [MyBatis vs. other ORMs](https://archfirst.org/mybatis-vs-other-orms/). You can use any database driver or query builder (such as [Knex.js](http://knexjs.org/)) to query your database, however you use JoinJS to convert the returned results into a hierarchy of nested objects.

## Example
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:
Expand Down Expand Up @@ -61,20 +61,16 @@ To teach JoinJS how to do this, you must create two result maps that describe yo
var resultMaps = [
{
mapId: 'teamMap',
idProperty: {name: 'id', column: 'id'},
properties: [
{name: 'name', column: 'name'}
],
idProperty: 'id',
properties: ['name'],
collections: [
{name: 'players', mapId: 'playerMap', columnPrefix: 'player_'}
]
},
{
mapId: 'playerMap',
idProperty: {name: 'id', column: 'id'},
properties: [
{name: 'name', column: 'name'}
]
idProperty: 'id',
properties: ['name']
}
]
```
Expand All @@ -85,7 +81,7 @@ Once you have created these result maps, you can simply call JoinJS to convert y
var 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](https://github.com/archfirst/joinjs/tree/master/test). Also check out the [Manage My Money](https://github.com/archfirst/manage-my-money-server) project for an example of a full-fledged application built with JoinJS and other libraries to manage personal finances.
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](https://github.com/archfirst/joinjs/tree/master/test). Follow the [step-by-step tutorial](https://archfirst.org/joinjs-an-alternative-to-complex-orms/) for a hands-on introduction. Once you have mastered the basics, check out the [Manage My Money](https://github.com/archfirst/manage-my-money-server) project to see how you can build a full-fledged application complete with a front-end using JoinJS and other useful libraries.

## Installation

Expand All @@ -102,11 +98,11 @@ ResultMaps are used to teach JoinJS how to map database results to objects. Each

- `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 {Object} (optional)` - specifies the name of the id property in the mapped object and in the result set. Default is `{name: 'id', column: 'id'}`.
- `idProperty {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 object
- `column` - property that identifies the database record in the result set

- `properties {Array} (optional)` - mappings for other properties. Each mapping contains:
- `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 object
- `column` - property name in the result set

Expand Down Expand Up @@ -149,3 +145,8 @@ This is a convenience method that maps a resultSet to a single object. It is use
Returns the mapped object or `null` if no object was mapped.

Throws a `NotFoundError` if no object is mapped and `isRequired` is `true`.

## Resources
- [JoinJS test suite](https://github.com/archfirst/joinjs/tree/master/test) - contains examples of various use cases
- [Step-by-step tutorial](https://archfirst.org/joinjs-an-alternative-to-complex-orms/) - provides a hands-on introduction to JoinJS
- [Manage My Money](https://github.com/archfirst/manage-my-money-server) - a full-fledged application complete with a front-end using JoinJS and other useful libraries
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "join-js",
"description": "A library to map complex database joins to nested objects.",
"version": "0.3.0",
"version": "0.4.0",
"author": "Naresh Bhatia",
"license": "MIT",
"homepage": "https://github.com/archfirst/joinjs",
Expand Down
11 changes: 11 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ function injectResultInObject(result, mappedObject, maps, mapId, columnPrefix) {

// Copy other properties
_.each(resultMap.properties, function(property) {
// If property is a string, convert it to an object
if (typeof property === 'string') {
property = {name: property, column: property};
}

// Copy only if property does not exist already
if (!mappedObject[property.name]) {

// The default for column name is property name
Expand Down Expand Up @@ -159,6 +165,11 @@ function createMappedObject(resultMap) {
function getIdProperty(resultMap) {
var idProperty = (resultMap.idProperty) ? resultMap.idProperty : {name: 'id', column: 'id'};

// If property is a string, convert it to an object
if (typeof idProperty === 'string') {
idProperty = {name: idProperty, column: idProperty};
}

// The default for column name is property name
if (!idProperty.column) {
idProperty.column = idProperty.name;
Expand Down
28 changes: 10 additions & 18 deletions test/maps/domain-maps.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,26 @@ let domainMaps = [
createNew: function() {
return new domain.Country();
},
idProperty: {name: 'code'},
properties: [
{name: 'name'}
]
idProperty: 'code',
properties: ['name']
},

{
mapId: 'fundMap',
createNew: function() {
return new domain.Fund();
},
idProperty: {name: 'id'},
properties: [
{name: 'name'}
]
idProperty: 'id',
properties: ['name']
},

{
mapId: 'holdingMap',
createNew: function() {
return new domain.Holding();
},
idProperty: {name: 'id'},
properties: [
{name: 'quantity'}
],
idProperty: 'id',
properties: ['quantity'],
associations: [
{name: 'fund', mapId: 'fundMap', columnPrefix: 'fund_'},
{name: 'security', mapId: 'securityMap', columnPrefix: 'security_'}
Expand All @@ -43,10 +37,8 @@ let domainMaps = [
createNew: function() {
return new domain.Security();
},
idProperty: {name: 'symbol'},
properties: [
{name: 'name'}
],
idProperty: 'symbol',
properties: ['name'],
associations: [
{name: 'country', mapId: 'countryMap', columnPrefix: 'country_'}
]
Expand All @@ -57,9 +49,9 @@ let domainMaps = [
createNew: function() {
return new domain.User();
},
idProperty: {name: 'id'},
idProperty: 'id',
properties: [
{name: 'uid'},
'uid',
{name: 'firstName', column: 'first_name'},
{name: 'lastName', column: 'last_name'}
]
Expand Down
74 changes: 19 additions & 55 deletions test/maps/test-maps.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
let testMaps = [
{
mapId: 'noIdProperty',
properties: [
{name: 'name'}
]
properties: ['name']
},

{
mapId: 'withIdProperty',
idProperty: {name: 'symbol'},
properties: [
{name: 'name'}
]
idProperty: 'symbol',
properties: ['name']
},

{
Expand All @@ -28,146 +24,114 @@ let testMaps = [

{
mapId: 'customerMap',
properties: [
{name: 'name'}
],
properties: ['name'],
collections: [
{name: 'orders', mapId: 'orderMap', columnPrefix: 'order_'}
]
},

{
mapId: 'orderMap',
properties: [
{name: 'total'}
]
properties: ['total']
},

// Multiple one-to-one relationships with same target entity
// Note that we cannot have recursive maps
{
mapId: 'personMap',
properties: [
{name: 'name'}
],
properties: ['name'],
associations: [
{name: 'father', mapId: 'shallowPersonMap', columnPrefix: 'father_'},
{name: 'mother', mapId: 'shallowPersonMap', columnPrefix: 'mother_'}
]
},
{
mapId: 'shallowPersonMap',
properties: [
{name: 'name'}
]
properties: ['name']
},

// one-to-one-to-one relationship
// A ---> 1 B ---> 1 C
{
mapId: 'aMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
associations: [
{name: 'b', mapId: 'bMap', columnPrefix: 'b_'}
]
},
{
mapId: 'bMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
associations: [
{name: 'c', mapId: 'cMap', columnPrefix: 'c_'}
]
},
{
mapId: 'cMap',
properties: [
{name: 'prop'}
]
properties: ['prop']
},

// one-to-one-to-many relationship
// D ---> 1 E ---> * F
{
mapId: 'dMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
associations: [
{name: 'e', mapId: 'eMap', columnPrefix: 'e_'}
]
},
{
mapId: 'eMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
collections: [
{name: 'fCollection', mapId: 'fMap', columnPrefix: 'f_'}
]
},
{
mapId: 'fMap',
properties: [
{name: 'prop'}
]
properties: ['prop']
},

// one-to-many-to-one relationship
// G ---> * H ---> 1 I
{
mapId: 'gMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
collections: [
{name: 'hCollection', mapId: 'hMap', columnPrefix: 'h_'}
]
},
{
mapId: 'hMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
associations: [
{name: 'i', mapId: 'iMap', columnPrefix: 'i_'}
]
},
{
mapId: 'iMap',
properties: [
{name: 'prop'}
]
properties: ['prop']
},

// one-to-many-to-many relationship
// J ---> * K ---> * L
{
mapId: 'jMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
collections: [
{name: 'kCollection', mapId: 'kMap', columnPrefix: 'k_'}
]
},
{
mapId: 'kMap',
properties: [
{name: 'prop'}
],
properties: ['prop'],
collections: [
{name: 'lCollection', mapId: 'lMap', columnPrefix: 'l_'}
]
},
{
mapId: 'lMap',
properties: [
{name: 'prop'}
]
properties: ['prop']
}
];

Expand Down

0 comments on commit f9661f8

Please sign in to comment.