Deep associations in sails mongo using populate me

2019-03-30 15:25发布

问题:

I am new to sails.js and I am using "sails.js with Mongodb". I am having problem with deep associations using populate in my sails app.

I have a relationship like this:

 Category has many to many relationship with Article.

 City has one to many relationship with Areas.

 Article has one to one relationship with City and Areas.

Category.js

module.exports = {

schema: true,

attributes: {

    //add referecnce to other article
    Articles: {
            collection: 'Article', 
            via:'ref_category_id' 
      },

    category_name: { 
        type:'string',  
        required: true, 
        unique: true },
  }
};

Article.js

module.exports = {

schema: true,

attributes: {

       //adding reference to category
        ref_category_id: {
                    collection:'Category',
                    via:'Articles'
         },

    //adding reference to city
     ref_city_id: {
                    model:'City'
       },

    //add a reference to area
         ref_area_id: {
                  model:'Areas'
        },

      //adding reference to tags
      tags: {
           collection: 'Tag', 
           via:'articles'
       },

      title:{ type:'string',required:true},

      blurb: { type: 'string'},

      description:{ type:'string'}
    }
 }; 

City.js

module.exports = {

        schema: true,

        attributes: {


        Areas: {
                collection: 'Areas', 
                via:'ref_city_id'
        },

         ref_article_id:{
                    model:'Article'
         },


        city_name: { type:'string',
                   required:true,
                   unique:true }
    }
 };

Areas.js

 module.exports = {

   schema: true, 

   attributes: {

        area_name: { type:'string',required:true},

        latitude: { type:'float'},

        longitude: { type:'float'},

        ref_city_id: {
                model: 'City' 
        },

        ref_article_id:{
             model:'Article'
         }

     }
 };

Tag.js

   module.exports = {

      schema:false,

      attributes: {

         //adding reference to article 
          articles: {
                  collection: 'Article', 
                  via:'tags'
          },

      tag_name:{type:'string',
                required:true,
                unique:true
         }
     }
  };

CategoryController.js

  searchCategory: function(req, res, next) {

    var category_name = req.param('category_name');

    Category.find({category_name:{'like': '%'+category_name+'%'}}).populate('Articles').exec(function(err, category) 
    {

        if(err)
        {
         return res.json({'status':486,'status_message':'Server Error'});
        }
        else
        { 
            if(category.length > 0)
            {
                 var c = parseInt(category[0].Articles.length,10);

                console.log(c);
                var i = parseInt('0',10);

                for (i=0; i<c; i++)
                {
                    console.log('i value in loop = ' + i);
                    Article.find({id:category[0].Article[i].id}).populateAll().exec(function(err,article_info) {
                        if(err)
                        {
                            return res.send(err);
                        }
                        else
                        {
                            console.log(article_info);
                            console.log('-------------------------------------------');

                            res.json(article_info); 
                            console.log(' I value = ' + i); 
                        }
                    }); 

                } 
                //console.log(category);
                //return res.json({'status':479,'status_message':'Success..!!','category_info':category});
            }
            else
            {
                return res.json({'status':489,'status_message':'failure..!! No categories found..!!'});
            }
        }
    });
}

POSTMAN Request:

 http://localhost:1337/category/searchCategory

 {"category_name":"travel"}

This is my json response: Here I am getting only article which is mapped with category. but i wanted to display the values of city, areas and tags which are mapped to article.

  {
   "status": 479,
   "status_message": "Success..!!",
   "category_info": [
     {
        "Articles": [
            {
                "ref_city_id": "55a766d0a29811e875cb96a1",
                "ref_area_id": "55a78b69578393e0049dec43",
                "title": "title",
                "blurb": "blurb",
                "description": "Description",
                "createdAt": "2015-07-16T12:36:36.778Z",
                "updatedAt": "2015-07-16T12:48:20.609Z",
                "id": "55a7a55439ace79e0512269d"
            },
        ],
        "category_name": "Cooking ",
        "id": "55a6b26aee9b41de747547bb",
        "createdAt": "2015-07-15T19:20:10.670Z",
        "updatedAt": "2015-07-15T19:20:10.670Z"
    }
  ]
}

How to do the deep nested associations using populate? or Is any other way to achieve this?

Please Can anyone help me to achieve this.

Thanks in advance.

回答1:

Yes, there is another way to do this.

Make sure you require the following npm module.

var nestedPop = require('nested-pop');

 

searchCategory: function(req, res, next) {
  var category_name = req.param('category_name');
  Category.find({category_name:{'like': '%'+category_name+'%'}})
  .populate('Articles')
  .exec(function(err, category) {
    if(err) return res.json({'status':486,'status_message':'Server Error'});
    if(category.length > 0) {
      var c = parseInt(category[0].Articles.length,10);
      console.log(c);
      var i = parseInt('0',10);
      for (var i = 0; i < c; i++) {
        console.log('i value in loop = ' + i);
        Article.find({id:category[0].Article[i].id})
        .populateAll()
        .exec(function(err, article_info) {
          if(err) return res.send(err);
          return nestedPop(article_info, {
            ref_city_id: {
              as: 'City',
              populate: [
                'things',
                'you',
                'want',
                'to',
                'populate',
                'for',
                'city'
              ],
            },
            ref_area_id: {
              as: 'Area', // or Areas (whatever the model name is)
              populate: [
                'things',
                'you',
                'want',
                'to',
                'populate',
                'for',
                'area'
              ]
            }
          }).then(function(article_info) {
            console.log(article_info);
            console.log('------------------------------------------');
            res.json(article_info); 
            console.log(' I value = ' + i);
          });
        }); 
      }
    } else {
      return res.json({'status':489,'status_message':'failure..!! No categories found..!!'});
    }
  });
}

Go to https://www.npmjs.com/package/nested-pop for more documentation.



回答2:

It is not currently supported, but is seems that the waterline team is working on it.

If you read the comments on this issue, you will see that there is an opened pull request based on this gist.


You could also do it manually, but you want to retrieve many information and the asynchronous code could quickly be very messy.

There are many tools to help you to keep the code readable:

  • named callback functions
  • promises (already available in Waterline)
  • async
  • lodash

Use the ones you feel more confortable with. There is an example using promises and lodash in this answer. I will not assume which tools you want to use, but you can update the question if you are still blocked later.

Category has only one relation while Article has many. I think in your situation, you should load the the categories without using populate(). Then you could loop threw the articles ids of each category, load each article with populate() (or populateAll()) and override category.Articles with the result.


EDIT. To avoid the Can\'t set headers after they are sent. error, you can use a counter to be sure to send the response only when all asynchronous functions have been executed. You are currently sending the response twice.

var articles = [];
var cpt = 0;    
for (i=0; i<c; i++) {
    Article.find({id:category[0].Article[i].id}).populate('ref_category_id').populate('tags').populate('ref_city_id').populate('ref_area_id').exec(function(err,article_info) {
        // Simplified callback function
        cpt++;  // Increment every time the callback function is executed
        articles.push(article_info);
        if (cpt === c) {  // We should have "c" executions of the callback function before sending the result in the response
            // This is working if `category` is an instance of `Category`,
            // but in your controller, `category` contains an array of `Category`.
            // Do you want to return a single Category or should you rename `category` into `categories` ?
            category.Articles = articles;
            res.json(category);
        }
    });
}

Once you understand how works the asynchronous execution flow, you should improve the code with the tools I mentioned above.