Skip to content

Instantly share code, notes, and snippets.

@cvburgess
Created August 28, 2018 02:13
Show Gist options
  • Save cvburgess/1f9c26df3f3d4615abf46f8d42a8ef6e to your computer and use it in GitHub Desktop.
Save cvburgess/1f9c26df3f3d4615abf46f8d42a8ef6e to your computer and use it in GitHub Desktop.
Knex - caching and batching
const { InMemoryLRUCache } = require("apollo-server-caching");
const DataLoader = require("dataloader");
class SQLCache {
constructor(cache = new InMemoryLRUCache(), knex) {
this.cache = cache;
this.loader = new DataLoader(queries =>
Promise.all(queries.map(query => knexInstance.raw(query)))
);
}
async knexPlugin(knexThen, ctx, args) {
const isSelectStatement = ctx._method === "select";
if (isSelectStatement) {
// Make a cacheKey that is sure to be unique
const normalizedQuery = ctx.toQuery().replace(/["']/g, "").toLowerCase();
const cacheKey = `sqlcache:${normalizedQuery}`;
// Serious shenanigans to support Knex not having plugins
// if you do not manually call the .then() after saving to cache
// then the code-level .then()s will fire _first_ and leave you with
// a potentially different result and then you cache the wrong thing
// :sadpanda:
const hasPromiseFn = Boolean(
args && args[0] && typeof args[0] === "function"
);
const promiseChain = hasPromiseFn ? args[0] : value => value;
const entry = await this.cache.get(cacheKey);
if (entry) {
// If there is a cache hit, return it without hitting the DB
return Promise.resolve(entry).then(promiseChain);
} else {
// If there is not a hit, query the DB and cache the result
return this.loader
.load(ctx.toQuery())
.then(({ rows }) => rows)
.then(result => {
this.cache.set(cacheKey, result);
return Promise.resolve(result);
})
.then(promiseChain);
}
} else {
return knexThen.apply(ctx, args);
}
}
}
module.exports = SQLCache;
const KnexQueryBuilder = require("knex/lib/query/builder");
const SQLCache = require("./SQLCache");
class SQLDataSource {
initialize(config) {
this.context = config.context;
const dbCache = new SQLCache(config.cache);
// This is an ugly hack to compensate for Knex not having plugins yet
const knexThen = KnexQueryBuilder.prototype.then;
KnexQueryBuilder.prototype.then = function() {
return dbCache.knexPlugin(knexThen, this, arguments);
};
this.knex.queryBuilder = () => new KnexQueryBuilder(this.knex.client);
this.db = this.knex;
}
}
module.exports = SQLDataSource;
const Knex = require("knex");
const SQLDataSource = require("../utils/SQLDataSource");
const { PG_HOST, PG_DATABASE, PG_USER, PG_PASSWORD, PG_PORT } = process.env;
class ThingsDB extends SQLDataSource {
constructor() {
super();
// Add an instance of Knex for the base class to use
this.knex = Knex({
client: "pg",
connection: {
host: PG_HOST,
user: PG_USER,
password: PG_PASSWORD,
database: PG_DATABASE,
port: PG_PORT
}
});
}
getThing({ id }) {
return this.db
.select()
.from("things")
.where({ id });
}
}
module.exports = ThingsDB;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment