Skip to content

Commit f9661f8

Browse files
committed
Allow simple strings to define properties
This makes the result maps less verbose. For situations where property name and column name are different, the object form specification is still supported.
1 parent 376bf33 commit f9661f8

5 files changed

Lines changed: 56 additions & 88 deletions

File tree

README.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
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.
44

5-
## Motivation
6-
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.
5+
## Motivation: Direct, no-nonsense control over your database
6+
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.
77

8-
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.
8+
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.
99

1010
## Example
1111
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:
@@ -61,20 +61,16 @@ To teach JoinJS how to do this, you must create two result maps that describe yo
6161
var resultMaps = [
6262
{
6363
mapId: 'teamMap',
64-
idProperty: {name: 'id', column: 'id'},
65-
properties: [
66-
{name: 'name', column: 'name'}
67-
],
64+
idProperty: 'id',
65+
properties: ['name'],
6866
collections: [
6967
{name: 'players', mapId: 'playerMap', columnPrefix: 'player_'}
7068
]
7169
},
7270
{
7371
mapId: 'playerMap',
74-
idProperty: {name: 'id', column: 'id'},
75-
properties: [
76-
{name: 'name', column: 'name'}
77-
]
72+
idProperty: 'id',
73+
properties: ['name']
7874
}
7975
]
8076
```
@@ -85,7 +81,7 @@ Once you have created these result maps, you can simply call JoinJS to convert y
8581
var mappedResult = joinjs.map(resultSet, resultMaps, 'teamMap', 'team_');
8682
```
8783

88-
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.
84+
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.
8985

9086
## Installation
9187

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

10399
- `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`.
104100

105-
- `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'}`.
101+
- `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'}`.
106102
- `name` - property that identifies the mapped object
107103
- `column` - property that identifies the database record in the result set
108104

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

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

151147
Throws a `NotFoundError` if no object is mapped and `isRequired` is `true`.
148+
149+
## Resources
150+
- [JoinJS test suite](https://github.com/archfirst/joinjs/tree/master/test) - contains examples of various use cases
151+
- [Step-by-step tutorial](https://archfirst.org/joinjs-an-alternative-to-complex-orms/) - provides a hands-on introduction to JoinJS
152+
- [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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "join-js",
33
"description": "A library to map complex database joins to nested objects.",
4-
"version": "0.3.0",
4+
"version": "0.4.0",
55
"author": "Naresh Bhatia",
66
"license": "MIT",
77
"homepage": "https://github.com/archfirst/joinjs",

src/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ function injectResultInObject(result, mappedObject, maps, mapId, columnPrefix) {
117117

118118
// Copy other properties
119119
_.each(resultMap.properties, function(property) {
120+
// If property is a string, convert it to an object
121+
if (typeof property === 'string') {
122+
property = {name: property, column: property};
123+
}
124+
125+
// Copy only if property does not exist already
120126
if (!mappedObject[property.name]) {
121127

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

168+
// If property is a string, convert it to an object
169+
if (typeof idProperty === 'string') {
170+
idProperty = {name: idProperty, column: idProperty};
171+
}
172+
162173
// The default for column name is property name
163174
if (!idProperty.column) {
164175
idProperty.column = idProperty.name;

test/maps/domain-maps.js

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,26 @@ let domainMaps = [
66
createNew: function() {
77
return new domain.Country();
88
},
9-
idProperty: {name: 'code'},
10-
properties: [
11-
{name: 'name'}
12-
]
9+
idProperty: 'code',
10+
properties: ['name']
1311
},
1412

1513
{
1614
mapId: 'fundMap',
1715
createNew: function() {
1816
return new domain.Fund();
1917
},
20-
idProperty: {name: 'id'},
21-
properties: [
22-
{name: 'name'}
23-
]
18+
idProperty: 'id',
19+
properties: ['name']
2420
},
2521

2622
{
2723
mapId: 'holdingMap',
2824
createNew: function() {
2925
return new domain.Holding();
3026
},
31-
idProperty: {name: 'id'},
32-
properties: [
33-
{name: 'quantity'}
34-
],
27+
idProperty: 'id',
28+
properties: ['quantity'],
3529
associations: [
3630
{name: 'fund', mapId: 'fundMap', columnPrefix: 'fund_'},
3731
{name: 'security', mapId: 'securityMap', columnPrefix: 'security_'}
@@ -43,10 +37,8 @@ let domainMaps = [
4337
createNew: function() {
4438
return new domain.Security();
4539
},
46-
idProperty: {name: 'symbol'},
47-
properties: [
48-
{name: 'name'}
49-
],
40+
idProperty: 'symbol',
41+
properties: ['name'],
5042
associations: [
5143
{name: 'country', mapId: 'countryMap', columnPrefix: 'country_'}
5244
]
@@ -57,9 +49,9 @@ let domainMaps = [
5749
createNew: function() {
5850
return new domain.User();
5951
},
60-
idProperty: {name: 'id'},
52+
idProperty: 'id',
6153
properties: [
62-
{name: 'uid'},
54+
'uid',
6355
{name: 'firstName', column: 'first_name'},
6456
{name: 'lastName', column: 'last_name'}
6557
]

test/maps/test-maps.js

Lines changed: 19 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
let testMaps = [
22
{
33
mapId: 'noIdProperty',
4-
properties: [
5-
{name: 'name'}
6-
]
4+
properties: ['name']
75
},
86

97
{
108
mapId: 'withIdProperty',
11-
idProperty: {name: 'symbol'},
12-
properties: [
13-
{name: 'name'}
14-
]
9+
idProperty: 'symbol',
10+
properties: ['name']
1511
},
1612

1713
{
@@ -28,146 +24,114 @@ let testMaps = [
2824

2925
{
3026
mapId: 'customerMap',
31-
properties: [
32-
{name: 'name'}
33-
],
27+
properties: ['name'],
3428
collections: [
3529
{name: 'orders', mapId: 'orderMap', columnPrefix: 'order_'}
3630
]
3731
},
3832

3933
{
4034
mapId: 'orderMap',
41-
properties: [
42-
{name: 'total'}
43-
]
35+
properties: ['total']
4436
},
4537

4638
// Multiple one-to-one relationships with same target entity
4739
// Note that we cannot have recursive maps
4840
{
4941
mapId: 'personMap',
50-
properties: [
51-
{name: 'name'}
52-
],
42+
properties: ['name'],
5343
associations: [
5444
{name: 'father', mapId: 'shallowPersonMap', columnPrefix: 'father_'},
5545
{name: 'mother', mapId: 'shallowPersonMap', columnPrefix: 'mother_'}
5646
]
5747
},
5848
{
5949
mapId: 'shallowPersonMap',
60-
properties: [
61-
{name: 'name'}
62-
]
50+
properties: ['name']
6351
},
6452

6553
// one-to-one-to-one relationship
6654
// A ---> 1 B ---> 1 C
6755
{
6856
mapId: 'aMap',
69-
properties: [
70-
{name: 'prop'}
71-
],
57+
properties: ['prop'],
7258
associations: [
7359
{name: 'b', mapId: 'bMap', columnPrefix: 'b_'}
7460
]
7561
},
7662
{
7763
mapId: 'bMap',
78-
properties: [
79-
{name: 'prop'}
80-
],
64+
properties: ['prop'],
8165
associations: [
8266
{name: 'c', mapId: 'cMap', columnPrefix: 'c_'}
8367
]
8468
},
8569
{
8670
mapId: 'cMap',
87-
properties: [
88-
{name: 'prop'}
89-
]
71+
properties: ['prop']
9072
},
9173

9274
// one-to-one-to-many relationship
9375
// D ---> 1 E ---> * F
9476
{
9577
mapId: 'dMap',
96-
properties: [
97-
{name: 'prop'}
98-
],
78+
properties: ['prop'],
9979
associations: [
10080
{name: 'e', mapId: 'eMap', columnPrefix: 'e_'}
10181
]
10282
},
10383
{
10484
mapId: 'eMap',
105-
properties: [
106-
{name: 'prop'}
107-
],
85+
properties: ['prop'],
10886
collections: [
10987
{name: 'fCollection', mapId: 'fMap', columnPrefix: 'f_'}
11088
]
11189
},
11290
{
11391
mapId: 'fMap',
114-
properties: [
115-
{name: 'prop'}
116-
]
92+
properties: ['prop']
11793
},
11894

11995
// one-to-many-to-one relationship
12096
// G ---> * H ---> 1 I
12197
{
12298
mapId: 'gMap',
123-
properties: [
124-
{name: 'prop'}
125-
],
99+
properties: ['prop'],
126100
collections: [
127101
{name: 'hCollection', mapId: 'hMap', columnPrefix: 'h_'}
128102
]
129103
},
130104
{
131105
mapId: 'hMap',
132-
properties: [
133-
{name: 'prop'}
134-
],
106+
properties: ['prop'],
135107
associations: [
136108
{name: 'i', mapId: 'iMap', columnPrefix: 'i_'}
137109
]
138110
},
139111
{
140112
mapId: 'iMap',
141-
properties: [
142-
{name: 'prop'}
143-
]
113+
properties: ['prop']
144114
},
145115

146116
// one-to-many-to-many relationship
147117
// J ---> * K ---> * L
148118
{
149119
mapId: 'jMap',
150-
properties: [
151-
{name: 'prop'}
152-
],
120+
properties: ['prop'],
153121
collections: [
154122
{name: 'kCollection', mapId: 'kMap', columnPrefix: 'k_'}
155123
]
156124
},
157125
{
158126
mapId: 'kMap',
159-
properties: [
160-
{name: 'prop'}
161-
],
127+
properties: ['prop'],
162128
collections: [
163129
{name: 'lCollection', mapId: 'lMap', columnPrefix: 'l_'}
164130
]
165131
},
166132
{
167133
mapId: 'lMap',
168-
properties: [
169-
{name: 'prop'}
170-
]
134+
properties: ['prop']
171135
}
172136
];
173137

0 commit comments

Comments
 (0)