Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loop through results and run another query for each result... Best practice? #838

Closed
joshparolin opened this issue Aug 16, 2013 · 33 comments

Comments

@joshparolin
Copy link

Hey all. I'm having trouble looping through a results set and then running another query for each result in order to attach some additional data.

Below is my code. "Pages" is a result from a Sequelize query. For each page, I'm trying to attach it's respective mentions. What's the best practice here?

Oddly, the below code actually renders the association to my view on localhost. But when pushed to Heroku, it does not.

// pages == an object    
    // for each page, get mentions
   for(var k in pages) {
      // build query
     var query = 'SELECT m.url, b.name, b.icon_path FROM "Page_has_Mentions" h ';
         query +='join "Mentions" m on m.id = h.mention_id ';
         query +='join "Blogs" b on b.id = m.blog_id ';
         query +='where h.page_id = \'' + pages[k].id + '\'';
      // get and attach mentions
     global.db.sequelize.query(query)
     .success(function(mentions) {
       pages[k].mentions = mentions;
     });
    }
@durango
Copy link
Member

durango commented Aug 16, 2013

Use the querychainer for multiple queries like that :)

http://sequelizejs.com/documentation#utils-querychainer

I would also escape pages[k].id or at the very least parseInt() it.

To escape I believe you can do global.db.sequelize.Utils.format(['SELECT * FROM hello WHERE world = ?', 'Earth'])

Which will escape earth :)

@durango
Copy link
Member

durango commented Aug 16, 2013

You could also build your query up to do a where h.page_id IN (<array of pages[k].id>) as well :) This way you're only executing one query but that's less of sequelize and more of SQL in general.

@joshparolin
Copy link
Author

Beautiful, thanks @durango.

  • I implemented the query chainer and it works like a charm.
  • I also implemented escaping for any user inputted query parameters. I couldn't find anything in the docs, but found this blog to be helpful with the escaping piece: http://redotheweb.com/2013/02/20/sequelize-the-javascript-orm-in-practice.html
  • With regards to using IN (), I need each result set separately so it can be attached to each respective HTML element separately. IN () would be great if I just needed to display the results together. Maybe there's a way to still achieve that using IN (), eh?

Can't thank you enough for the help. Saved me a lot of time and headache.

@jagged3dge
Copy link

Can someone please post a snippet how to achieve this using QueryChainer? I've tried myself, but I seem to not get it right.

@mickhansen
Copy link
Contributor

@jagged3dge at this point i'd suggest you use promises, we are moving away from the QueryChainer

@jagged3dge
Copy link

Thank you for the response Mick. I'm using ~2.0.0-beta.2 at this moment. I have tried to adapt from the promises example code on the documentation page. However, I'm still facing the same problem: the response is sent without the additional data.

Here's what I've tried:

    db.User.sync()
    .then(function() {
      return req.User.getPosts({
        include: [
          //{ model: db.PostLikes },
          { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
          { model: db.Game, attributes: ['name'] }
        ]
      })
    })
    .then(function(posts) {
      console.log(posts)
      for (p in posts) {

        db.PostLikes.sync()

        .then(function() {
          return db.PostLikes.count({ where: { PostId: posts[p].id } })
        })

        .then(function(likes) {
          console.log('Blog #' + posts[p].id + ' has ' + likes + ' likes')
          posts[p].likes = likes
          console.log('posts[' + posts[p].id + '] = ', posts[p])
        })
      }
      return posts
    }).then(function(posts) {
      // DEV: Send 404 if no 'likes' count has been added to the blogpost data. Remove for production.
      if (posts[0].likes) {
        res.json(posts)
      } else {
        res.send(404)
      }
    })

@mickhansen
Copy link
Contributor

You need to return a promise in your second then since you're doing async
stuff, you could do something like.

var promises = [];
for (p in posts) {
promises.push(sync().then().etc))
}
return Promise.all(promises);

On Wed, May 21, 2014 at 11:48 AM, jagged3dge notifications@github.comwrote:

Thank you for the response Mick. I'm using ~2.0.0-beta.2 at this moment. I
have tried to adapt from the promises example code on the documentation
page http://sequelizejs.com/blog/state-of-v1-7-0#promises. However, I'm
still facing the same problem: the response is sent without the additional
data.

Here's what I've tried:

db.User.sync()
.then(function() {
  return req.User.getPosts({
    include: [
      //{ model: db.PostLikes },
      { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
      { model: db.Game, attributes: ['name'] }
    ]
  })
})
.then(function(posts) {
  console.log(posts)
  for (p in posts) {

    db.PostLikes.sync()

    .then(function() {
      return db.PostLikes.count({ where: { PostId: posts[p].id } })
    })

    .then(function(likes) {
      console.log('Blog #' + posts[p].id + ' has ' + likes + ' likes')
      posts[p].likes = likes
      console.log('posts[' + posts[p].id + '] = ', posts[p])
    })
  }
  return posts
}).then(function(posts) {
  // DEV: Send 404 if no 'likes' count has been added to the blogpost data. Remove for production.
  if (posts[0].likes) {
    res.json(posts)
  } else {
    res.send(404)
  }
})


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43735046
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

I tried your method, but I'm getting a "ReferenceError: Promise is not defined." I noticed that Sequelize uses Bluebird internally. What do I need to require to utilise the Promise object?

@mickhansen
Copy link
Contributor

Right you need to do something like var Promise = require('bluebird') or
you could do var Promise = Sequelize.Promise

On Wed, May 21, 2014 at 1:20 PM, jagged3dge notifications@github.comwrote:

I tried your method, but I'm getting a "ReferenceError: Promise is not
defined." I noticed that Sequelize uses Bluebird internally. What do I need
to require to utilise the Promise object?


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43742334
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

I tried using

var Promise = require('bluebird')
// throws: Cannot find module 'bluebird'
var Promise = db.Sequelize.Promise
//throws: Possibly unhandled TypeError: Cannot call method 'all' of undefined at D:\............\posts.js:58:22

@mickhansen
Copy link
Contributor

Well you have to install bluebird. Sequelize.Promise is only available on
master (and upcomming 2.0.0-dev12)

On Wed, May 21, 2014 at 1:28 PM, jagged3dge notifications@github.comwrote:

I tried using

var Promise = require('bluebird')
// throws: Cannot find module 'bluebird'var Promise = db.Sequelize.Promise//throws: Possibly unhandled TypeError: Cannot call method 'all' of undefined at D:............\posts.js:58:22


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43742873
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Thanks for the tip to install 'bluebird'. Apparently, I'm just a dork. LOL!

Modified my code to the following.

    db.User.sync()
    .then(function() {
      return req.User.getPosts({
        include: [
          //{ model: db.PostLikes },
          { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
          { model: db.Game, attributes: ['name'] }
        ]
      })
    })
    .then(function(posts) {

      for (p in posts) {

        promises = []

        promises.push(

          db.PostLikes.sync()

          .then(function() {
            return db.PostLikes.count({ where: { PostId: posts[p].id } })
          })

          .then(function(likes) {
            console.log('Blog #' + posts[p].id + ' has ' + likes + ' likes')
            posts[p].likes = likes
            console.log('posts[' + posts[p].id + '] = ', posts[p])
          })
      }
      return Promise.all(promises)

    }).then(function(posts) {
      console.log(posts) // Should result in a list of posts with 'likes' attribute, but displays '[ undefined ]' instead
      res.send(404) // Temporary response while testing
    })

The 'posts' parameter in the final '.then()' results in an undefined. I'm really new to Promises and it's tough trying to wrap my head around this. I'm sure it's something really simple that I'm missing here. Can you shed some light on this?

@mickhansen
Copy link
Contributor

var promises = [] should obviously be outside the loop :)
You might also need to do return posts[p] from the final then.

Hmm, i just realized. Why are you calling sync for each post? Something is
way off with your flow currently.

On Wed, May 21, 2014 at 2:08 PM, jagged3dge notifications@github.comwrote:

Thanks for the tip to install 'bluebird'. Apparently, I'm just a dork.
LOL!

Modified my code to the following.

db.User.sync()
.then(function() {
  return req.User.getPosts({
    include: [
      //{ model: db.PostLikes },
      { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
      { model: db.Game, attributes: ['name'] }
    ]
  })
})
.then(function(posts) {


  for (p in posts) {

    promises = []

    promises.push(


      db.PostLikes.sync()

      .then(function() {
        return db.PostLikes.count({ where: { PostId: posts[p].id } })
      })

      .then(function(likes) {
        console.log('Blog #' + posts[p].id + ' has ' + likes + ' likes')
        posts[p].likes = likes
        console.log('posts[' + posts[p].id + '] = ', posts[p])
      })
  }

  return Promise.all(promises)

}).then(function(posts) {
  console.log(posts) // Should result in a list of posts with 'likes' attribute, but displays '[ undefined ]' instead
  res.send(404) // Temporary response while testing
})

The 'posts' parameter in the final '.then()' results in an undefined. I'm
really new to Promises and it's tough trying to wrap my head around this.
I'm sure it's something really simple that I'm missing here. Can you shed
some light on this?


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43745836
.

Mick Hansen
@mhansendev
mhansen.io

@mickhansen
Copy link
Contributor

.sync() should really only be called at the start of your app - It creates
your tables, doing it in a loop is not right :)

On Wed, May 21, 2014 at 2:08 PM, jagged3dge notifications@github.comwrote:

Thanks for the tip to install 'bluebird'. Apparently, I'm just a dork.
LOL!

Modified my code to the following.

db.User.sync()
.then(function() {
  return req.User.getPosts({
    include: [
      //{ model: db.PostLikes },
      { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
      { model: db.Game, attributes: ['name'] }
    ]
  })
})
.then(function(posts) {


  for (p in posts) {

    promises = []

    promises.push(


      db.PostLikes.sync()

      .then(function() {
        return db.PostLikes.count({ where: { PostId: posts[p].id } })
      })

      .then(function(likes) {
        console.log('Blog #' + posts[p].id + ' has ' + likes + ' likes')
        posts[p].likes = likes
        console.log('posts[' + posts[p].id + '] = ', posts[p])
      })
  }

  return Promise.all(promises)

}).then(function(posts) {
  console.log(posts) // Should result in a list of posts with 'likes' attribute, but displays '[ undefined ]' instead
  res.send(404) // Temporary response while testing
})

The 'posts' parameter in the final '.then()' results in an undefined. I'm
really new to Promises and it's tough trying to wrap my head around this.
I'm sure it's something really simple that I'm missing here. Can you shed
some light on this?


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43745836
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Haha... yes, I realise calling the .sync() at each loop call is wrong. But, like I said earlier, I'm new to async code with promises, and I had to directly lift from the documentation. I couldn't figure a way to initiate the first promise.

A copying mistake with the promises[] array. In my code, the array does get initialized outside the for loop. However, the result is like I said. The returning object in the final 'then()' is [ undefined ]. >.<

Here's the corrected code:

    db.User.sync()
    .then(function() {
      return req.User.getPosts({
        include: [
          //{ model: db.PostLikes },
          { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
          { model: db.Game, attributes: ['name'] }
        ]
      })
    })
    .then(function(posts) {

      promises = []

      for (p in posts) {

        promises.push(

          db.PostLikes.sync()

          .then(function() {
            return db.PostLikes.count({ where: { PostId: posts[p].id } })
          })

          .then(function(likes) {
            console.log('Blog #' + posts[p].id + ' has ' + likes + ' likes')
            posts[p].likes = likes
            console.log('posts[' + posts[p].id + '] = ', posts[p])
          })
        )
      }
      return Promise.all(promises)

    }).then(function(posts) {
      console.log(posts) // Should result in a list of posts with 'likes' attribute, but displays '[ undefined ]' instead
      res.send(404) // Temporary response while testing
    })

@mickhansen
Copy link
Contributor

You might need return posts[p] in the final then()

On Wed, May 21, 2014 at 2:19 PM, jagged3dge notifications@github.comwrote:

Haha... yes, I realise calling the .sync() at each loop call is wrong.
But, like I said earlier, I'm new to async code with promises, and I had to
directly lift from the documentation. I couldn't figure a way to initiate
the first promise.

A copying mistake with the promises[] array. In my code, the array does
get initialized outside the for loop. However, the result is like I said.
The returning object in the final 'then()' is [ undefined ]. >.<

Here's the corrected code:

db.User.sync()
.then(function() {
  return req.User.getPosts({
    include: [
      //{ model: db.PostLikes },
      { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
      { model: db.Game, attributes: ['name'] }
    ]
  })
})
.then(function(posts) {


  promises = []

  for (p in posts) {


    promises.push(

      db.PostLikes.sync()

      .then(function() {
        return db.PostLikes.count({ where: { PostId: posts[p].id } })
      })

      .then(function(likes) {
        console.log('Blog #' + posts[p].id + ' has ' + likes + ' likes')
        posts[p].likes = likes
        console.log('posts[' + posts[p].id + '] = ', posts[p])
      })
  }
  return Promise.all(promises)

}).then(function(posts) {
  console.log(posts) // Should result in a list of posts with 'likes' attribute, but displays '[ undefined ]' instead
  res.send(404) // Temporary response while testing
})


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43746795
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Thank you so much Mick! Finally did it!

Here's the working code:

    req.User.getPosts({
        include: [
          //{ model: db.PostLikes },
          { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
          { model: db.Game, attributes: ['name'] }
        ]
    })

    .then(function(posts) {

      promises = []

      for (p in posts) {

        promises.push(

          db.PostLikes.count({ where: { PostId: posts[p].id } })

          .then(function(likes) {
            posts[p].dataValues.likes = likes   // Gotcha! The json() string for each object only returns values from the 'dataValues' object. Adding it to the object itself doesn't return it in the JSON response
            return posts[p]   // vital for pushing the object into the result array
          })
        )
      }
      return Promise.all(promises)

    }).then(function(posts) {
      res.json(posts)
    })

@mickhansen
Copy link
Contributor

Manipulating dataValues is inadvisable.
You could do something like:

var post = posts[p].toJSON();
post.likes = likes;
return post;

On Wed, May 21, 2014 at 2:43 PM, jagged3dge notifications@github.comwrote:

Thank you so much Mick! Finally did it!

Here's the working code:

req.User.getPosts({
    include: [
      //{ model: db.PostLikes },
      { model: db.User, attributes: ['displayName', 'name', 'profilePicture'] },
      { model: db.Game, attributes: ['name'] }
    ]
})


.then(function(posts) {

  promises = []

  for (p in posts) {

    promises.push(


      db.PostLikes.count({ where: { PostId: posts[p].id } })


      .then(function(likes) {
        posts[p].dataValues.likes = likes   // Gotcha! The json() string for each object only returns values from the 'dataValues' object. Adding it to the object itself doesn't return it in the JSON response
        return posts[p]   // vital for pushing the object into the result array

      })
    )
  }
  return Promise.all(promises)

}).then(function(posts) {

  res.json(posts)
})


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43748912
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Understood. Updated the code accordingly to pre-JSONify the individual post objects and add the attribute to the JSON, instead of modifying underlying dataValues object.

Thanks for the pointer and all your help, Mick! You're awesome!

@mickhansen
Copy link
Contributor

Yeah that's the more future proof way atleast :) And should work for most
use cases (i haven't found any other use for this except API responses so
yeah).

Anytime :) We're also on irc, #sequelizejs @ freenode

On Wed, May 21, 2014 at 3:04 PM, jagged3dge notifications@github.comwrote:

Understood. Updated the code accordingly to pre-JSONify the individual
post objects and add the attribute to the JSON, instead of modifying
underlying dataValues object.

Thanks for the pointer and all your help, Mick! You're awesome!


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43751117
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Seems like my happiness was premature q_q

The resultant JSON array has the right number of objects, however the objects themselves are copies of the last object in the original array. I have a gut-feeling this is happening due to the return post in the second .then() inside the for loop. Could you please look into the logic once more to see what I'm missing this time? >.<

@mickhansen
Copy link
Contributor

Hmm, can you provide me with a full copy of your code again? (use
gist.github.com or similar rather than paste here in email)

On Wed, May 21, 2014 at 3:22 PM, jagged3dge notifications@github.comwrote:

Seems like my happiness was premature q_q

The resultant JSON array has the right number of objects, however the
objects themselves are copies of the last object in the original array. I
have a gut-feeling this is happening due to the return post in the second
.then() inside the for loop. Could you please look into the logic once
more to see what I'm missing this time? >.<


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43753070
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

@mickhansen
Copy link
Contributor

Ah yeah this is a classic case for a closure.

You either wan't posts.forEach(function (post) {}); or you want to create a
closure around your code that provides the post. something like

for (p in posts) {
(function(post) {
// promise code
})(posts[p])
}

On Wed, May 21, 2014 at 3:35 PM, jagged3dge notifications@github.comwrote:

Here's the Gist of my codehttps://gist.github.com/jagged3dge/1ae038cf050662986121


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43754630
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Awesome catch! Thanks a ton Mick :D Resolved the issue with posts.forEach(). Updated the gist as well. Here's the link again to the Gist with the final code

@mickhansen
Copy link
Contributor

You might want res.json(200, posts) rather than res.send, that sends the
appropriate content type headers (not a big deal but yeah)

On Wed, May 21, 2014 at 4:05 PM, jagged3dge notifications@github.comwrote:

Awesome catch! Thanks a ton Mick :D Resolved the issue with
posts.forEach(). Updated the gist as well. Here's the link again to the Gist
with the final codehttps://gist.github.com/jagged3dge/1ae038cf050662986121


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43758182
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Since I am converting the posts to JSON in the forEach() loop, I figured res.send() made more sense to send an array of pre-JSONified objects, instead of res.json(). Unless I'm wrong? I am testing the routes with POSTman and the responses seems to have the correct 'application/json' headers even with res.send().

One last thing before I rest this issue...

How can I go about fetching and adding additional attributes, say for eg. number of Comments on the post? Would it be possible to do that as well in the same 'forEach' closure? If so, how?

@mickhansen
Copy link
Contributor

Afaik res.json just sets the right header and does JSON.stringify on your
object. But i guess res.send might work aswell :)

Well you can just use Promise.all() again inside your then and then map
those onto.

On Wed, May 21, 2014 at 4:47 PM, jagged3dge notifications@github.comwrote:

Since I am converting the posts to JSON in the forEach() loop, I figured
res.send() made more sense to send an array of pre-JSONified objects,
instead of res.json(). Unless I'm wrong? I am testing the routes with
POSTman and the responses seems to have the correct 'application/json'
headers even with res.send().

One last thing before I rest this issue...

How can I go about adding fetching and adding additional attributes, say
for eg. number of Comments on the post? Would it be possible to do that as
well in the same 'forEach' closure? If so, how?


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43763751
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

I've been through all of bluebird's docs and tried looking around on SO as well... I'm having no luck understanding how to retrieve another attribute via Sequelize and map that onto. Most of the code snippets I've come across have one loop doing only 1 thing.

I thought of making another array of promises for Comments and utilize that to fetch and update the Comments count. But my logic flunks out at the end when it comes to return Promise.all(). Can you point me in teh right direction as to how you mean 'map those onto' ?

@mickhansen
Copy link
Contributor

Inside your closure you could do:

promises.push(Promise.all([
findCommentCount
findLikeCount
]).spread(function (commentCount, likeCount) {
post.commentCount = ..
etc

return post;
}));

On Thu, May 22, 2014 at 1:00 AM, jagged3dge notifications@github.comwrote:

I've been through all of bluebird's docs and tried looking around on SO as
well... I'm having no luck understanding how to retrieve another attribute
via Sequelize and map that onto. Most of the code snippets I've come across
have one loop doing only 1 thing.

I thought of making another array of promises for Comments and utilize
that to fetch and update the Comments count. But my logic flunks out at the
end when it comes to return Promise.all(). Can you point me in teh right
direction as to how you mean 'map those onto' ?


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43825312
.

Mick Hansen
@mhansendev
mhansen.io

@jagged3dge
Copy link

Fantastic! That works like a charm ... After this project's done, I'm gonna spend a few weeks poring over JS promises, I swear. Thanks again Mick! Updated the Gist too.

@mickhansen
Copy link
Contributor

I reckon a few hours would do the trick ;)

On Thu, May 22, 2014 at 9:23 AM, jagged3dge notifications@github.comwrote:

Fantastic! That works like a charm ... After this project's done, I'm
gonna spend a few weeks poring over JS promises, I swear. Thanks again
Mick! Updated the Gist too.https://gist.github.com/jagged3dge/1ae038cf050662986121


Reply to this email directly or view it on GitHubhttps://github.com//issues/838#issuecomment-43856177
.

Mick Hansen
@mhansendev
mhansen.io

@mota57
Copy link

mota57 commented Aug 3, 2016

Nice thanks mick and jagge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants