{"id":198,"date":"2019-08-29T23:36:46","date_gmt":"2019-08-29T23:36:46","guid":{"rendered":"https:\/\/www.instapainting.com\/blog\/?p=198"},"modified":"2020-02-14T16:22:59","modified_gmt":"2020-02-14T16:22:59","slug":"use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries-in-graphql","status":"publish","type":"post","link":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries","title":{"rendered":"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers"},"content":{"rendered":"\r\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">We implement arbitrary query caching and batching using DataLoader to allow greedy data fetching with automatic DB query optimization.\r\n\r\n<cite>You may see some minimal Typescript syntax to provide some context in the example code. Do not be alarmed.<\/cite><\/blockquote>\r\n\r\n\r\n\r\n\r\nGraphQL promises a lot of cool features like the ability to batch and cache queries, save on data transmission, query for data anywhere and only request exactly what you need from the server, and all sorts of optimizations. But it merely\u00a0<em>enables<\/em> these features through its use of strong typing of the API schema\u2014they do not come for free. It&#8217;s up to us to implement the optimizations by utilizing the metadata.\r\n\r\n\r\n\r\n\r\n\r\n<!--more-->\r\n\r\n\r\n\r\n\r\n<h2 class=\"wp-block-heading\">Why DataLoader?<\/h2>\r\n\r\n\r\n\r\n\r\nGraphQL servers require you to write resolver functions for every field, query, and schema type (implementations such as ApolloServer provide a set of default resolvers).\u00a0This forces the logic for resolving data to be distributed across various resolver functions.\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\" title=\"User schema\"><code class=\"language-graphql\" lang=\"graphql\">type User {\r\n  id: ID!\r\n  messages: [Message] # In the DB this is stored as a list of message IDs\r\n  friends: [User] # In the DB this is stored as a list of User IDs\r\n}<\/code><\/pre>\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\" title=\"User Resolver\"><code class=\"language-typescript\" lang=\"typescript\">const userResolvers = {\r\n  Mutation: {\r\n    createUser(root, arg, context, info) {\r\n      ...\r\n    }\r\n  },\r\n  Query: {\r\n    allUsers(root, arg, context, info) {\r\n      \/\/ Fetch all users\r\n      return MongooseUserModel.find({}).exec()\r\n    }\r\n  },\r\n  User: {\r\n    messages(user, arg, context, info) {\r\n      \/\/ This function is called for every User to determine what the User.messages field value should be\r\n      return MongooseMessageModel.find({ _id: { $in: user.messages } }).exec()\r\n    },\r\n    friends(user, arg, context, info) {\r\n      \/\/ This function is called for every User to determine what the User.friends field value should be\r\n      return MongooseFriendModel.find({ _id: { $in: user.messages } }).exec()\r\n    }\r\n  }\r\n}<\/code><\/pre>\r\n\r\n\r\n\r\n\r\nIf we were to call the <code>allUsers<\/code>\u00a0GraphQL query, we&#8217;d get a list of <code>Users<\/code>, and for each user, the server will call the <code>messages<\/code>\u00a0and <code>friends<\/code>\u00a0resolver functions for\u00a0<em>each<\/em> of the returned users. For every <code>n<\/code>\u00a0users returned, there will be <code>1 + (n*2)<\/code> queries! And this is for a relatively simple GraphQL schema. In a traditional REST API server you may be writing logic to handle a specific API call, and so the parameters are constrained enough for you to optimize the queries. GraphQL uses distributed resolver logic: optimizing the queries becomes almost impossible because the resolver structure is designed to handle logic for arbitrary queries and fields.\r\n\r\n\r\n\r\n\r\n<h2 class=\"wp-block-heading\">Brief Intro to DataLoader<\/h2>\r\n\r\n\r\n\r\n\r\nFacebook&#8217;s DataLoader project is one of those necessary implementations to optimize your GraphQL server. DataLoader doesn&#8217;t depend on GraphQL and can be used independently to batch and cache any sort of server-side data fetching, but because of the way GraphQL servers resolve data fields it&#8217;s almost required to have a production-ready server implementation. It caches queries by memoizing the primary key you are using to fetch the data. In this article I&#8217;ll demonstrate how we (at <a href=\"https:\/\/www.instapainting.com\">Instapainting.com<\/a>) extended this to support batching any arbitrary MongoDB query\u2014not just queries by IDs\u2014while also preserving query caching.\r\n\r\n\r\n\r\n\r\n\r\nIf you&#8217;ve looked into the DataLoader docs you may notice that its examples revolve around optimizing queries where you find and return just one result:\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">\/\/ old query that we can get rid of\r\n\/\/ const user1 = await MongooseUserModel.findOne({ _id: 1 }).exec()\r\n\/\/ With a DataLoader you can swap out the above with this equivalent call\r\nconst user1 = await userByIDLoader.load(1)<\/code><\/pre>\r\n\r\n\r\n\r\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">Here the loader takes the ID &#8220;1&#8221;, makes a query with that ID implemented by the loader, and returns a single &#8220;user&#8221;.<\/blockquote>\r\n\r\n\r\n\r\n\r\nAnd loading many results would require specifying a list of IDs:\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">const [ user1, user2 ] = await userByIDLoader.loadMany([ 1, 2 ]);<\/code><\/pre>\r\n\r\n\r\n\r\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">This is the same as above but fires multiple &#8220;loads&#8221; all at once, returning multiple users (in the same order).<\/blockquote>\r\n\r\n\r\n\r\n\r\nYou can now call that anywhere in your code where you need to fetch a user by their ID, and as many times as you&#8217;d like. Regardless of if you call <code>load<\/code>\u00a0or <code>loadMany<\/code>, all those userIDs will get sent to your batch load function. The batch load function supplied when creating the DataLoader instance is where you make your actual query with all the data at one time.\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">function userByIdBatchLoadFn(userIDs) {\r\n  \/\/ Remember these are batched now, so instead of \"findOne\" we have to do \"find\".\r\n  const users = await MongooseUserModel.find({'_id': { '$in': userIDs } }).exec();\r\n\r\n  \/\/ The results must be returned in the same order of the keys passed to this function\r\n  return userIDs.map((userID) =&gt; users.find((user) =&gt; user._id.toString() === userID));\r\n}<\/code><\/pre>\r\n\r\n\r\n\r\n\r\nThis is a simple overview of how DataLoader works:\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">loader(userID) =&gt; memoize(userID) =&gt; if userID in cache, return user from cache =&gt; runQuery(listOfAllUserIDsNotInCache)\u00a0=&gt;\u00a0listOfUsers\u00a0=&gt; loader(userID)\u00a0returns\u00a0a\u00a0user<\/code><\/pre>\r\n\r\n\r\n\r\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">Memoization is the process of caching the userID so that it can pull it from cache if it exists.<\/blockquote>\r\n\r\n\r\n\r\n\r\nDataLoader coalesces all the <code>userIDs<\/code>\u00a0from a single tick of execution into a list, and you implement the actual query using that list of <code>userIDs<\/code>\u00a0to return a list of users from your query (in the same order). It then handles returning the correct data to where the <code>loader.load(userID)<\/code> function was originally called. Cached lookups are returned to the correct <code>load<\/code>\u00a0call, and omitted from the <code>listOfAllUserIDs<\/code>\u00a0sent to your <code>batchLoadFn<\/code> when you make the actual query.\r\n\r\n\r\n\r\n\r\n<h2 class=\"wp-block-heading\">What if I want to batch a non-ID query?<\/h2>\r\n\r\n\r\n\r\n\r\nThis is the elephant in the room, as most queries are not just by primary keys or by IDs, and it&#8217;s not really addressed by the DataLoader docs. DataLoader requires the data key to be hashable. Thankfully, it&#8217;s not too hard to modify the default DataLoader implementation to start batching and caching arbitrary objects\u2014in our case MongoDB queries. We just convert the object into a string!\r\n\r\n\r\n\r\n\r\n<h3 class=\"wp-block-heading\">Override the default cache function<\/h3>\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">\/\/ We use the modified stringify to normalize the query object key ordering so that {a: 1, b: 2} == {b: 2, a:1}\r\nimport stringify from \"json-stable-stringify\";\r\nconst dataLoaderOpts = {\r\n  cacheKeyFn: (query) =&gt; {\r\n    return stringify(query) \r\n  }\r\n}\r\nconst usersByQueryLoader = new DataLoader(usersByQueryBatchLoadFn, dataLoaderOpts)\r\n<\/code><\/pre>\r\n\r\n\r\n\r\n\r\nNow you can supply an arbitrary object to the <code>load<\/code>\u00a0method.\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">const users = await usersByQueryLoader.load({age: {$gte: 18}})<\/code><\/pre>\r\n\r\n\r\n\r\n<h3 class=\"wp-block-heading\">Write your batch load function to support a list of <em>queries<\/em><\/h3>\r\n\r\n\r\n\r\n\r\nHere&#8217;s where it gets a little tricky. DataLoader expects that you return the response data in the same order in which it provides the input data. This is how it maps the responses to the input data.\r\n\r\n\r\n\r\n\r\n\r\nWhen we batch a bunch of data for a MongoDB query either in the form of an <code>$in<\/code>\u00a0query or by merging multiple queries with <code>$or<\/code>, the results are returned in undefined order and thus may have nothing to do with the order by which you supplied the input data in the query. It&#8217;s like everyone pooling together their food delivery order and then trying to sift through the pile of food to see who ordered what.\r\n\r\n\r\n\r\n\r\n<div class=\"wp-block-image\">\r\n<figure class=\"aligncenter\"><a href=\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries-graphql-resolvers\/dataloader-illustration1\/\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"720\" class=\"wp-image-218\" src=\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png\" alt=\"How DataLoader fetches multiple queries.\" srcset=\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png 1200w, https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1-300x180.png 300w, https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1-768x461.png 768w, https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1-1024x614.png 1024w\" sizes=\"(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><\/a><figcaption>Don&#8217;t worry you&#8217;ll get a real artist when you order something through Instapainting.com.<\/figcaption><\/figure>\r\n<\/div>\r\n\r\n\r\n\r\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><code>db.users.query({ _id: { $in: [ 1, 2, 3 ] } })<\/code>\u00a0may return data in a different order like\u00a0<code>[{ _id: 2 }, { _id: 1 }, { _id: 3 }]<\/code>.<\/blockquote>\r\n\r\n\r\n\r\n\r\nIn order to guarantee the data returned is in the correct order, we have to go through and match the returned data back to the original query somehow.\r\n\r\n\r\n\r\n\r\n\r\nFor simple primary key queries we just iterated the original query data and assigned the results to the right spots based on matching the primary keys:\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">async function userByIdBatchLoadFn(listOfUserIDs) {\r\n   const users: object[] = await ...fetch your data here...\r\n   return listOfUserIDs.map((userID) =&gt; users.find((user) =&gt; user._id.toString() === userID))\r\n }<\/code><\/pre>\r\n\r\n\r\n\r\n\r\nSince now we have a <code>listOfQueries<\/code> instead of IDs, this becomes more complicated. In the former case we start with an ID, and we end up with data which contains that same ID. In the current case we start with a\u00a0<em>query object<\/em> and we end up with a <em>list of data<\/em> that has <em>many IDs<\/em>. We can longer match up query and response data using the IDs.\r\n\r\n\r\n\r\n\r\n<div class=\"wp-block-image\">\r\n<figure class=\"aligncenter\"><a href=\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries-graphql-resolvers\/dataloader-illustration2\/\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"720\" class=\"wp-image-219\" src=\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration2.png\" alt=\"Which query wanted what data?\" srcset=\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration2.png 1200w, https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration2-300x180.png 300w, https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration2-768x461.png 768w, https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration2-1024x614.png 1024w\" sizes=\"(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><\/a><figcaption>Which query wanted what data again?<\/figcaption><\/figure>\r\n<\/div>\r\n\r\n\r\n\r\n\r\nLike a black hole emitting Hawking radiation, or mixing sperm in a test tube from multiple fathers, it&#8217;s difficult to infer the inputs from the output data. By batching the queries into one we&#8217;ve destroyed some info and it comes back to us in a disorganized pile.\r\n\r\n\r\n\r\n\r\n\r\nThe only way to associate the data to back to the caller would be:\r\n\r\n\r\n\r\n\r\n<ol>\r\n \t<li>Run the individual queries to see what data is returned for what query; compare notes.<\/li>\r\n \t<li>Have some sort of one-way function that works like the ID comparison that takes a Query and checks if the data belongs to that Query.<\/li>\r\n<\/ol>\r\n\r\n\r\n\r\n\r\nObviously option #1 is a no-go. The whole point of this is to avoid running all those queries!\u00a0Luckily someone made option #2 already, and it&#8217;s called <a href=\"https:\/\/github.com\/crcn\/sift.js\">Sift.js<\/a>.\r\n\r\n\r\n\r\n\r\n\r\nSift.js allows you to use the MongoDB query syntax on a regular javascript array. This is essentially option #1, but done in memory within the just the result set that was returned. To continue the food delivery analogy, this would be like simulating the job of the restaurant (MongoDB) and asking for everyone&#8217;s orders again to try to match it to the actual food in the pile on the table.\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">function usersByQueryBatchLoadFn(queries) {\r\n  \/\/ The '$or' operator lets you combine multiple queries so that any record matching any of the queries gets returned\r\n  const users = await MongooseUserModel.find({ '$or': queries }).exec();\r\n\r\n  \/\/ You can prime other loaders as well\r\n  \/\/ Priming inserts the key into the cache of another loader\r\n  for (const user of users) {\r\n    userByIdLoader.prime(user.id.toString(), user);\r\n  }\r\n\r\n  \/\/ Sift.js applies the MongoDB query to the data to determine if it matches the query. We use this to assign the right users to the right query that requested it.\r\n  return queries.map(query =&gt; users.filter(sift(query)));\r\n};<\/code><\/pre>\r\n\r\n\r\n\r\n\r\nInstead of returning <em>one user<\/em> per <em>ID<\/em>, the loader now returns <em>many users<\/em> per\u00a0<em>user query<\/em>.\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">\/\/ Get back one user or undefined if no match\r\nconst user1: UserDocument | undefined = await userByIDLoader.load(1)\r\n\r\n\/\/\u00a0Now\u00a0we\u00a0get\u00a0back\u00a0a\u00a0list\u00a0of\u00a0potential\u00a0users\u00a0that\u00a0matched\u00a0the\u00a0query, or an empty array if no matches\r\nconst users: UserDocument[] = await usersByQueryLoader.load(query)\r\n\r\n\/\/\u00a0loadMany still works too, except you supply a list of queries for users, and get back an array of many users.\r\nconst [users1, users2]: (UserDocument[])[] = await usersByQueryLoader.loadMany([query1, query2])<\/code><\/pre>\r\n\r\n\r\n\r\n\r\n<strong>There are some caveats<\/strong>.\r\n\r\n\r\n\r\n\r\n\r\nSift.js has to keep up with the official MongoDB query operator specs, and even at the time of this writing there are some MongoDB queries that\u00a0<em><a href=\"https:\/\/github.com\/crcn\/sift.js\/issues\/160\">don&#8217;t work<\/a>.<\/em> Furthermore, we&#8217;re depending on the maintainer to keep it up-to-date with spec. This would be like trying to keep up to date with the restaurant&#8217;s menu, and being able to identify someone&#8217;s order of &#8220;Lion King Roll&#8221; (actual menu item) with the correct dish sitting in front of you.\r\n\r\n\r\n\r\n\r\n\r\nThis method does not support <code>sort<\/code> or <code>limit<\/code> queries. This is because any sort params or limit params would apply to the whole batched query, rather than the individual constituent queries.\r\n\r\n\r\n\r\n\r\n\r\nBecause it doesn&#8217;t support <code>limit<\/code>\u00a0queries, you should not use DataLoader for large unbounded queries (such as those where you would normally paginate).\r\n\r\n\r\n\r\n\r\n\r\nTherefore some queries cannot be run through the DataLoader, but since DataLoading is mostly an optimization, you work with what you can get.\r\n\r\n\r\n\r\n\r\n<h2 class=\"wp-block-heading\">Putting It All Together<\/h2>\r\n\r\n\r\n\r\n\r\nI&#8217;ve gone over the constituent parts of how DataLoader works and how we got it to work with arbitrary queries. Now I&#8217;ll explain how to actually tie it all together with real code.\r\n\r\n\r\n\r\n\r\n<pre class=\"wp-block-code\"><code class=\"language-typescript\" lang=\"typescript\">\/\/ Assume referenced functions and symbols from earlier in this article are either defined in this file or imported\r\n\r\nimport express from \"express\"\r\nconst app = express()\r\n\r\napp.get('\/', async function (req, res) {\r\n  \/\/ You'll want to instantiate a new instance of DataLoader for *every HTTP request*. This is to prevent leaking data cached for one user to another user of your app.\r\n  \/\/ It doesn't have to be directly in the get() call, but must be within the call stack of this request handler\r\n  \/\/ The main goal is to *not* instantiate in the global scope for your app server\r\n  const dataLoaders = {};\r\n  dataLoaders.userByIdLoader = new DataLoader(userByIdBatchLoadFn, dataLoaderOpts)\r\n  dataLoaders.userByQueryLoader = new DataLoader(userByQueryBatchLoadFn, dataLoaderOpts)\r\n\r\n  \/\/ You can then handle your request with whatever framework you're using, and use the passed dataLoader instances to make queries\r\n  handleRequest(req, res, dataLoaders)\r\n\r\n  \/\/ Make queries like so\r\n  const oldUsers = await usersByQueryLoader.load({age: {$gte: 18}})\r\n  const youngUsers = await usersByQueryLoader.load({age: {$lt: 18}})\r\n  const specificUser = await userByIDLoader.load(12)\r\n  \/\/ Despite querying 3 times, you'll only get two queries sent to MongoDB: one for old and young users, and one for a specific user.\r\n  \/\/ And if the specificUser is one of the old or young users returned, we'd actually only get 1 query because it can be skipped due to us priming the userByIdLoader cache!\r\n}<\/code><\/pre>\r\n\r\n\r\n\r\n\r\nI&#8217;d also like to add that the technologies used here: MongoDB, Node.js, GraphQL\u2014are not required to adopt this paradigm or put to practice these concepts. With a little modification you can make this work with SQL, <a href=\"https:\/\/github.com\/sheerun\/dataloader\">Ruby<\/a>, and even a REST API. Plus there are already many <a href=\"https:\/\/github.com\/graphql\/dataloader#other-implementations\">non-JS implementations of DataLoader<\/a>\u00a0available.\r\n\r\n<!-- AddThis Advanced Settings generic via filter on the_content --><!-- AddThis Share Buttons generic via filter on the_content -->","protected":false},"excerpt":{"rendered":"<p>We implement arbitrary query caching and batching using DataLoader to allow greedy data fetching with automatic DB query optimization. You may see some minimal Typescript syntax to provide some context in the example code. Do not be alarmed. GraphQL promises a lot of cool features like the ability to batch and cache queries, save on &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers&#8221;<\/span><\/a><\/p>\n<p><!-- AddThis Advanced Settings generic via filter on get_the_excerpt --><!-- AddThis Share Buttons generic via filter on get_the_excerpt --><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[11],"tags":[13,12],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v22.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers - Instapainting Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers - Instapainting Blog\" \/>\n<meta property=\"og:description\" content=\"We implement arbitrary query caching and batching using DataLoader to allow greedy data fetching with automatic DB query optimization. You may see some minimal Typescript syntax to provide some context in the example code. Do not be alarmed. GraphQL promises a lot of cool features like the ability to batch and cache queries, save on &hellip; Continue reading &quot;Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers&quot;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/\" \/>\n<meta property=\"og:site_name\" content=\"Instapainting Blog\" \/>\n<meta property=\"article:published_time\" content=\"2019-08-29T23:36:46+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2020-02-14T16:22:59+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png\" \/>\n<meta name=\"author\" content=\"chris\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"chris\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/\",\"url\":\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/\",\"name\":\"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers - Instapainting Blog\",\"isPartOf\":{\"@id\":\"https:\/\/www.instapainting.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png\",\"datePublished\":\"2019-08-29T23:36:46+00:00\",\"dateModified\":\"2020-02-14T16:22:59+00:00\",\"author\":{\"@id\":\"https:\/\/www.instapainting.com\/blog\/#\/schema\/person\/f0008166544f365e1de42677b6ec07a2\"},\"breadcrumb\":{\"@id\":\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#primaryimage\",\"url\":\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png\",\"contentUrl\":\"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png\",\"width\":1200,\"height\":720,\"caption\":\"How DataLoader fetches multiple queries.\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.instapainting.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.instapainting.com\/blog\/#website\",\"url\":\"https:\/\/www.instapainting.com\/blog\/\",\"name\":\"Instapainting Blog\",\"description\":\"Leveraging Art &amp; Technology\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.instapainting.com\/blog\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.instapainting.com\/blog\/#\/schema\/person\/f0008166544f365e1de42677b6ec07a2\",\"name\":\"chris\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.instapainting.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/4836857b5056601222e6aca8804d8348?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/4836857b5056601222e6aca8804d8348?s=96&d=mm&r=g\",\"caption\":\"chris\"},\"url\":\"https:\/\/www.instapainting.com\/blog\/author\/chris\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers - Instapainting Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/","og_locale":"en_US","og_type":"article","og_title":"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers - Instapainting Blog","og_description":"We implement arbitrary query caching and batching using DataLoader to allow greedy data fetching with automatic DB query optimization. You may see some minimal Typescript syntax to provide some context in the example code. Do not be alarmed. GraphQL promises a lot of cool features like the ability to batch and cache queries, save on &hellip; Continue reading \"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers\"","og_url":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/","og_site_name":"Instapainting Blog","article_published_time":"2019-08-29T23:36:46+00:00","article_modified_time":"2020-02-14T16:22:59+00:00","og_image":[{"url":"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png"}],"author":"chris","twitter_card":"summary_large_image","twitter_misc":{"Written by":"chris","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/","url":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/","name":"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers - Instapainting Blog","isPartOf":{"@id":"https:\/\/www.instapainting.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#primaryimage"},"image":{"@id":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#primaryimage"},"thumbnailUrl":"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png","datePublished":"2019-08-29T23:36:46+00:00","dateModified":"2020-02-14T16:22:59+00:00","author":{"@id":"https:\/\/www.instapainting.com\/blog\/#\/schema\/person\/f0008166544f365e1de42677b6ec07a2"},"breadcrumb":{"@id":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#primaryimage","url":"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png","contentUrl":"https:\/\/www.instapainting.com\/blog\/wp-content\/uploads\/2019\/08\/dataloader-illustration1.png","width":1200,"height":720,"caption":"How DataLoader fetches multiple queries."},{"@type":"BreadcrumbList","@id":"https:\/\/www.instapainting.com\/blog\/use-dataloader-to-batch-and-cache-arbitrary-mongodb-queries\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.instapainting.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Use DataLoader to Batch And Cache Arbitrary MongoDB Queries in GraphQL Resolvers"}]},{"@type":"WebSite","@id":"https:\/\/www.instapainting.com\/blog\/#website","url":"https:\/\/www.instapainting.com\/blog\/","name":"Instapainting Blog","description":"Leveraging Art &amp; Technology","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.instapainting.com\/blog\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/www.instapainting.com\/blog\/#\/schema\/person\/f0008166544f365e1de42677b6ec07a2","name":"chris","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.instapainting.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/4836857b5056601222e6aca8804d8348?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/4836857b5056601222e6aca8804d8348?s=96&d=mm&r=g","caption":"chris"},"url":"https:\/\/www.instapainting.com\/blog\/author\/chris\/"}]}},"_links":{"self":[{"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/posts\/198"}],"collection":[{"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/comments?post=198"}],"version-history":[{"count":26,"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/posts\/198\/revisions"}],"predecessor-version":[{"id":287,"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/posts\/198\/revisions\/287"}],"wp:attachment":[{"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/media?parent=198"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/categories?post=198"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.instapainting.com\/blog\/wp-json\/wp\/v2\/tags?post=198"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}