diff --git a/lib/ChainFind.js b/lib/ChainFind.js index 25dd07fc..9deed57c 100644 --- a/lib/ChainFind.js +++ b/lib/ChainFind.js @@ -132,6 +132,21 @@ function ChainFind(Model, opts) { } return promise.fail(cb); }, + eager: function () { + // This will allow params such as ("abc", "def") or (["abc", "def"]) + var associations = _.flatten(arguments); + + // TODO: Implement eager loading for Mongo and delete this. + if (opts.driver.config.protocol == "mongodb:") { + throw new Error("MongoDB does not currently support eager loading"); + } + + opts.__eager = _.filter(opts.associations, function (association) { + return ~associations.indexOf(association.name); + }); + + return this; + }, all: function (cb) { opts.driver.find(opts.only, opts.table, opts.conditions, { limit : opts.limit, @@ -153,8 +168,38 @@ function ChainFind(Model, opts) { data[idx] = instance; if (--pending === 0) { - return cb(null, data); + return (opts.__eager && opts.__eager.length ? eagerLoading : cb)(null, data); + } + }); + }; + + var eagerLoading = function (err, data) { + var pending = opts.__eager.length; + var idMap = {}; + var count = 0; + + var ids = _.map(data, function (instance) { + var id = instance[opts.id[0]]; + // Create the association arrays + for (var i = 0, association; association = opts.__eager[i]; i++) { + instance[association.name] = []; } + + idMap[id] = count++; + return id; + }); + + _.map(opts.__eager, function (association) { + opts.driver.eagerQuery(association, opts, ids, function (err, instances) { + for (var i = 0, instance; instance = instances[i]; i++) { + // Perform a parent lookup with $p, and initialize it as an instance. + data[idMap[instance.$p]][association.name].push(association.model(instance)); + } + + if (--pending === 0) { + return cb(null, data); + } + }); }); }; diff --git a/lib/Drivers/DML/mysql.js b/lib/Drivers/DML/mysql.js old mode 100755 new mode 100644 index 9997d1b4..467ecdfe --- a/lib/Drivers/DML/mysql.js +++ b/lib/Drivers/DML/mysql.js @@ -143,6 +143,24 @@ Driver.prototype.find = function (fields, table, conditions, opts, cb) { this.execSimpleQuery(q, cb); }; +Driver.prototype.eagerQuery = function (association, opts, ids, cb) { + var desiredKey = Object.keys(association.field); + var assocKey = Object.keys(association.mergeAssocId); + + var where = {}; + where[desiredKey] = ids; + + var query = this.query.select() + .from(association.model.table) + .select(opts.only) + .from(association.mergeTable, assocKey, opts.id) + .select(desiredKey).as("$p") + .where(association.mergeTable, where) + .build(); + + this.execSimpleQuery(query, cb); +}; + Driver.prototype.count = function (table, conditions, opts, cb) { var q = this.query.select() .from(table) diff --git a/lib/Drivers/DML/postgres.js b/lib/Drivers/DML/postgres.js index d5545627..12807a11 100644 --- a/lib/Drivers/DML/postgres.js +++ b/lib/Drivers/DML/postgres.js @@ -185,6 +185,24 @@ Driver.prototype.find = function (fields, table, conditions, opts, cb) { this.execSimpleQuery(q, cb); }; +Driver.prototype.eagerQuery = function (association, opts, ids, cb) { + var desiredKey = Object.keys(association.field); + var assocKey = Object.keys(association.mergeAssocId); + + var where = {}; + where[desiredKey] = ids; + + var query = this.query.select() + .from(association.model.table) + .select(opts.only) + .from(association.mergeTable, assocKey, opts.id) + .select(desiredKey).as("$p") + .where(association.mergeTable, where) + .build(); + + this.execSimpleQuery(query, cb); +}; + Driver.prototype.count = function (table, conditions, opts, cb) { var q = this.query.select().from(table).count(null, 'c'); diff --git a/lib/Drivers/DML/sqlite.js b/lib/Drivers/DML/sqlite.js index 13587ff1..a5c5bc34 100644 --- a/lib/Drivers/DML/sqlite.js +++ b/lib/Drivers/DML/sqlite.js @@ -127,6 +127,24 @@ Driver.prototype.find = function (fields, table, conditions, opts, cb) { this.db.all(q, cb); }; +Driver.prototype.eagerQuery = function (association, opts, ids, cb) { + var desiredKey = Object.keys(association.field); + var assocKey = Object.keys(association.mergeAssocId); + + var where = {}; + where[desiredKey] = ids; + + var query = this.query.select() + .from(association.model.table) + .select(opts.only) + .from(association.mergeTable, assocKey, opts.id) + .select(desiredKey).as("$p") + .where(association.mergeTable, where) + .build(); + + this.execSimpleQuery(query, cb); +}; + Driver.prototype.count = function (table, conditions, opts, cb) { var q = this.query.select() .from(table) diff --git a/test/integration/model-find-chain.js b/test/integration/model-find-chain.js index 83638c5e..1bc44b09 100644 --- a/test/integration/model-find-chain.js +++ b/test/integration/model-find-chain.js @@ -6,6 +6,7 @@ var common = require('../common'); describe("Model.find() chaining", function() { var db = null; var Person = null; + var Dog = null; var setup = function () { return function (done) { @@ -36,6 +37,30 @@ describe("Model.find() chaining", function() { }; }; + var setup2 = function () { + return function (done) { + Dog = db.define("dog", { + name: String, + }); + Dog.hasMany("friends"); + Dog.hasMany("family"); + + ORM.singleton.clear(); // clear cache + + return helper.dropSync(Dog, function () { + Dog.create([{ + name : "Fido", + friends : [{ name: "Gunner" }, { name: "Chainsaw" }], + family : [{ name: "Chester" }] + }, { + name : "Thumper", + friends : [{ name: "Bambi" }], + family : [{ name: "Princess" }, { name: "Butch" }] + }], done); + }); + }; + }; + before(function (done) { helper.connect(function (connection) { db = connection; @@ -479,6 +504,76 @@ describe("Model.find() chaining", function() { }); }); + describe(".eager()", function () { + before(setup2()); + + // TODO: Remove this code once the Mongo eager loading is implemented + var isMongo = function () { + if (db.driver.config.protocol == "mongodb:") { + (function () { + Dog.find().eager("friends").all(function () { + // Should not ever run. + }); + }).should.throw(); + + return true; + } + return false; + }; + + it("should fetch all listed associations in a single query", function (done) { + if (isMongo()) { return done(); }; + + Dog.find({ name: ["Fido", "Thumper"] }).eager("friends").all(function (err, dogs) { + should.equal(err, null); + + should(Array.isArray(dogs)); + + dogs.length.should.equal(2); + + dogs[0].friends.length.should.equal(2); + dogs[1].friends.length.should.equal(1); + done(); + }); + }); + + it("should be able to handle multiple associations", function (done) { + if (isMongo()) { return done(); }; + + Dog.find({ name: ["Fido", "Thumper"] }).eager("friends", "family").all(function (err, dogs) { + should.equal(err, null); + + should(Array.isArray(dogs)); + + dogs.length.should.equal(2); + + dogs[0].friends.length.should.equal(2); + dogs[0].family.length.should.equal(1); + dogs[1].friends.length.should.equal(1); + dogs[1].family.length.should.equal(2); + done(); + }); + }); + + it("should work with array parameters too", function (done) { + if (isMongo()) { return done(); }; + + Dog.find({ name: ["Fido", "Thumper"] }).eager(["friends", "family"]).all(function (err, dogs) { + should.equal(err, null); + + should(Array.isArray(dogs)); + + dogs.length.should.equal(2); + + dogs[0].friends.length.should.equal(2); + dogs[0].family.length.should.equal(1); + dogs[1].friends.length.should.equal(1); + dogs[1].family.length.should.equal(2); + done(); + }); + }); + }); + describe(".success()", function () { before(setup());