5

I create an API with mongoose in Node.js. I saved my data in a collection Transactions which gives some references from other collections objects:

const mongoose = require('mongoose');
const { Schema } = mongoose;

const transactionSchema = new Schema({
  status: String,
  _user: { type: Schema.Types.ObjectId, ref: 'User' },
  _borne: { type: Schema.Types.ObjectId, ref: 'Borne' },
  createdAt: Date,
  updatedAt: Date
});

When I do a query on transactions, I would get the Borne object instead of its id as it is saved in my database. I do not directly save it as a Borne object because some changes could appear in my Borne (or User) objects, and I would it to be saved on every Transaction objects.

So I tried to use virtual or path (override), but it doesn't change my output, and I also don't know if it's the right way to do it:

// In this example, I try to change the status output by "new status" to test if it works, and it doesn't
transactionSchema.path('status')
    .get(function(value) {
        return "new status";
    })
});

The output is the same as previously.


EDIT: Populate is the solution, but doesn't work

Currently, I'm loading my models like that in my index.js file:

const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const apn = require('apn');
const keys = require('./config/keys');

require('./app/models/Borne');
require('./app/models/User');
require('./app/models/Transaction');
require('./app/models/Comment');
require('./app/models/Notification');

const app = express();

const apnProvider = new apn.Provider(keys.apns.options);

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

mongoose.connect(keys.mongoURI, (err, database) => {
  if (err) return console.log(err);

  require('./app/routes')(app);

  const PORT = process.env.PORT || 8000;
  app.listen(PORT, () => {
    console.log('We are live on ' + PORT);
  });
});

Then, here is an example of model:

const mongoose = require('mongoose');
const { Schema } = mongoose;

const transactionSchema = new Schema({
  status: String,
  details: {
    amount: { type: Number }, // money
    quantity: { type: Number }, // power consumed
    date: { type: Date },
    city: { type: String }
  },
  logs: [
    {
      state: String,
      date: Date
    }
  ],
  _user: { type: Schema.Types.ObjectId, ref: 'User' },
  _borne: { type: Schema.Types.ObjectId, ref: 'Borne' },
  createdAt: Date,
  updatedAt: Date
});

mongoose.model('transactions', transactionSchema);

Finally, here is where I call the populate. It doesn't work:

const mongoose = require('mongoose');

const User = mongoose.model('users');
const Transaction = mongoose.model('transactions');
const Borne = mongoose.model('bornes');
const Comment = mongoose.model('comments');

module.exports = app => {
    app.get('/v1/user/:id/transactions', async (req, res) => {
        const ObjectID = require('mongodb').ObjectID;

        var id = req.params.id;
        var existingUser;
        if (req.params.id == 'me' && req.user) {
            id = req.user.id;
            existingUser = req.user;
        } else {
            existingUser = await User.findOne({ _id: new ObjectID(id) });
        }

        if (existingUser) {
            const transactions = await Transaction.find({
                _user: new ObjectID(id),
                status: { $nin: ['booked', 'charging', 'charged', 'left'] }
            }).populate('_user').populate('_borne').sort({ updatedAt: -1 });

            // ...

            res.status(200);
            res.send({
                statusCode: 200,
                data: transactions
            });
        }
    });
};
cusmar
  • 1,903
  • 1
  • 20
  • 39

2 Answers2

10

According to MongoDB Documentation, you have to "manually" make a second query if you want to get the object pointed by the reference.

But Mongoose offers the populate method, which allows you to replace references with the right documents.

Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s).

So, in your case, you could do something like that :

var transactionModel = mongoose.model('Transaction', transactionSchema);

transactionModel
  .find({})
  .populate('_user')
  .populate('_borne')
  .exec((err, transaction) => {
    if (err) return handleError(err);
    // deal with your transaction
  });


EDIT

I just read your edit, could you try that for me :

Remove all the require('./app/models/xxx') in your index.js file.

At the end of your models :

module.exports = mongoose.model('xxx', xxxSchema);

And then in your routes / controllers :

const User = require('/app/models/users');
const Borne = require('/app/models/borne');
const Transaction = require('/app/models/transaction');

So your models are created the same time as your schemas and you're sure that's the correct order.

Hope it helps,
Best regards

boehm_s
  • 5,254
  • 4
  • 30
  • 44
  • Do you have an error, a message that says something special, what is the behavior of the above code when you try it ? – boehm_s Feb 07 '18 at 14:36
  • I got the error: `MissingSchemaError: Schema hasn't been registered for model "User"` – cusmar Feb 07 '18 at 14:43
  • Well, it's another problem, it comes from the reference you make for the User collection : `_user: { type: Schema.Types.ObjectId, ref: 'User' }`. It says that there is no schema for the model 'User', which is needed to make the ref. It may be because your file containing your User schema has not been interpreted when your file containing your Transaction Schema did (as suggested in [this answer](https://stackoverflow.com/questions/20832126/missingschemaerror-schema-hasnt-been-registered-for-model-user)) – boehm_s Feb 07 '18 at 14:53
  • I updated my question with more details of why it is not working. Does it help you to help me? – cusmar Feb 08 '18 at 10:38
  • It doesn't work, can we talk by chat? It will be easier – cusmar Feb 08 '18 at 12:26
  • We got the error `MissingSchemaError: Schema hasn't been registered for model "User"`, but the model is at the top of the transactionRoutes file. So I don't understand where the error could come from, because the schema is defined – cusmar Feb 08 '18 at 12:28
  • Yes, by chat is alright and in french also, if your code is open source, it will be easier, otherwise if you could send me a minimum working example that reproduce the error, it would be great – boehm_s Feb 08 '18 at 12:45
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/164751/discussion-between-cusmar-and-boehm-s). – cusmar Feb 08 '18 at 12:46
0

I think to have a fast and robust query, you should take a look at Aggregate Query with moongodb.

Query the transaction collection (matching the correct _id) , then unwind _user property, make a lookup to the user collection (similar to JOIN in SQL), unwind _born collection, lookup the born collection.

Seems a bit complicated like this but it's very powerfull and fast.

OlivierTo
  • 254
  • 1
  • 11