Services cache documentation question

I am new to redis and caching. I am working through the services documentation for Redwood on choosing a good key. There are many examples of using updatedAt fields like below.

const post = ({ id }) => {
  return cache(['posts', id,  updatedAt.getTime()], () => {
    return db.post.findMany()
  })
}

The concept makes sense to me, but the function updatedAt is unavailable. I feel like I would have to do a query for the updatedAt first so it can be used. However if I did this I wouldn’t need the cache as I already had the result from the first query. I think there are some fundamentals I am missing here, any help would be appreciated.

const post = async ({ id }) => {
  const result = await.db.posts.findUnique({ where: { id }})

  return cache(['posts', id,  result.updatedAt.getTime()], () => {
    return db.posts.findUnique({ where: { id }})
  })
}

Thanks
Mike

Hello! That’s correct, you’d need the id and updatedAt timestamp in order to form that key. If you’re only caching a single row in the database then the cache may not provide any benefit: you need to select it anyway to get the values of the key, so you may as well just select it all in the first query anyway. The benefits come in when you’re dealing with cases like:


The contents of that single row are potentially huge (a BLOB perhaps) and you want to avoid that if it rarely changes. You can retrieve the key by adding a select: { id: true, updatedAt: true } to the first call to get the values for the key and so that query will always be quick, and you can cache and avoid the second call to get ALL of the data.

Maybe a product has an image column which is a BLOB containing the actual image (let’s not worry about whether this is a good database design!). You could do something like the following:

export const product = ({ id }) => {
  // super fast query, runs every time
  const key = await db.product.findUnique({ 
    select: { id: true, updatedAt: true }, 
    where: { id },
  })

  return cache(['product', key.id, key.updatedAt], () => {
    // slow query, but now only runs after the product has changed
    return db.product.findUnique({ where: { id })
  })
}

You’re selecting/computing more data besides just that single database row. Imagine a case where you’re building someone’s order history page: you could cache all of the data required to build their order history forever—until they place a new order, or modify an existing one. You can use the id and updatedAt stamp of the latest order (“latest” meaning whichever one has the most recent updatedAt timestamp) and use that to cache all the others: if a new order is placed (or an existing order is modified) then the first query will return a different order than last time, which would bust the cache:

export const orderHistory => () => {
  const latestOrder = await db.order.findFirst({ 
    select: { id: true, updatedAt: true }, 
    where: { userId: context.currentUser.id }, 
    orderBy: { updatedAt: { desc: true } 
  })

  return cache(['orders', latestOrder.id, latestOrder.updatedAt], () => {
    return db.order.findMany({ 
      where: { userId: context.currentUser.id },
      include: { 
        orderItems: {
          include: {
            product: true,
          }
        }
      }
    })
  })
}

I almost included currentUser.id as part of the key, but the order list itself is limited to just those placed by the current user, so in effect the key is already “scoped” to the current user anyway!


The idea is just to get the key using the simplest query possible, to avoid a more costly query/computation later on. You will always incur the overhead of the query to get the key, but we think this is worth the tradeoff over having to manually expire cache keys, or try and come up with an expiration time that’s “good enough” to give the user’s a good experience. If you’re finding that just getting the single row to create the key is too much overhead, then you can fallback to manually expiring (see the deleteCacheKey() docs) or setting expire times.

Hope that helps!

2 Likes

Rob,

I can’t thank you enough for taking the time to explain that. That completely makes sense, very clear now.

Mike

3 Likes

You’re welcome! I feel like caching is kind of a secret feature that most folks don’t even know that Redwood has, but it’s so powerful, I love talking about it. :slight_smile: And key-based caching makes things so simple. When people talk about one of the two hard things in computers being caching, I think it’s because all of the stress of worrying about manually expiring caches and being sure you catch all the weird edge cases… but with a good key, it’s automatic!

If you want to get really crazy, check out so-called Russian Doll Caching where you start nesting the cache blocks so that if one element of something deep down inside changes, you can re-use the cache of everything else and only rebuild the ones that contain the changed thing. I should add a section to our docs about it, but I learned about it from recommendations in Rails.

1 Like