Skip to content

Commit

Permalink
feat: Add basic support for custom cache providers (#5309)
Browse files Browse the repository at this point in the history
* Add basic support for custom cache providers

* Rename 'cacheProvider' to 'cache'

* Update caching documentation

* Add CustomQueryResultCache example in caching.md

* Add custom cache provider test
  • Loading branch information
justblender authored and pleerock committed Jan 22, 2020
1 parent dd73395 commit 6c6bde7
Show file tree
Hide file tree
Showing 7 changed files with 617 additions and 9 deletions.
21 changes: 21 additions & 0 deletions docs/caching.md
Expand Up @@ -189,6 +189,7 @@ In case you want to connect to a redis-cluster using IORedis's cluster functiona
```

Note that, you can still use options as first argument of IORedis's cluster constructor.

```typescript
{
...
Expand All @@ -213,4 +214,24 @@ Note that, you can still use options as first argument of IORedis's cluster cons
}
```

If none of the built-in cache providers satisfy your demands, then you can also specify your own cache provider by using a `provider` factory function which needs to return a new object that implements the `QueryResultCache` interface:

```typescript
class CustomQueryResultCache implements QueryResultCache {
constructor(private connection: Connection) {}
...
}
```

```typescript
{
...
cache: {
provider(connection) {
return new CustomQueryResultCache(connection);
}
}
}
```

You can use `typeorm cache:clear` to clear everything stored in the cache.
17 changes: 9 additions & 8 deletions src/cache/QueryResultCacheFactory.ts
Expand Up @@ -26,16 +26,17 @@ export class QueryResultCacheFactory {
if (!this.connection.options.cache)
throw new Error(`To use cache you need to enable it in connection options by setting cache: true or providing some caching options. Example: { host: ..., username: ..., cache: true }`);

if ((this.connection.options.cache as any).type === "redis")
return new RedisQueryResultCache(this.connection, "redis");
const cache: any = this.connection.options.cache;

if ((this.connection.options.cache as any).type === "ioredis")
return new RedisQueryResultCache(this.connection, "ioredis");
if (cache.provider && typeof cache.provider === "function") {
return cache.provider(this.connection);
}

if ((this.connection.options.cache as any).type === "ioredis/cluster")
return new RedisQueryResultCache(this.connection, "ioredis/cluster");

return new DbQueryResultCache(this.connection);
if (cache.type === "redis" || cache.type === "ioredis" || cache.type === "ioredis/cluster") {
return new RedisQueryResultCache(this.connection, cache.type);
} else {
return new DbQueryResultCache(this.connection);
}
}

}
7 changes: 7 additions & 0 deletions src/connection/BaseConnectionOptions.ts
Expand Up @@ -3,6 +3,8 @@ import {LoggerOptions} from "../logger/LoggerOptions";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {DatabaseType} from "../driver/types/DatabaseType";
import {Logger} from "../logger/Logger";
import {Connection} from "./Connection";
import {QueryResultCache} from "../cache/QueryResultCache";

/**
* BaseConnectionOptions is set of connection options shared by all database types.
Expand Down Expand Up @@ -121,6 +123,11 @@ export interface BaseConnectionOptions {
*/
readonly type?: "database" | "redis" | "ioredis" | "ioredis/cluster"; // todo: add mongodb and other cache providers as well in the future

/**
* Factory function for custom cache providers that implement QueryResultCache.
*/
readonly provider?: (connection: Connection) => QueryResultCache;

/**
* Configurable table name for "database" type cache.
* Default value is "query-result-cache"
Expand Down
318 changes: 318 additions & 0 deletions test/functional/cache/custom-cache-provider.ts
@@ -0,0 +1,318 @@
import "reflect-metadata";
import {expect} from "chai";
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
sleep
} from "../../utils/test-utils";
import {Connection} from "../../../src/connection/Connection";
import {User} from "./entity/User";
import {MockQueryResultCache} from "./provider/MockQueryResultCache";

describe("custom cache provider", () => {

let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
cache: {
provider(connection) {
return new MockQueryResultCache(connection);
}
}
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should be used instead of built-ins", () => Promise.all(connections.map(async connection => {
const queryResultCache: any = connection.queryResultCache;
expect(queryResultCache).to.have.property("queryResultCacheTable");

const queryResultCacheTable = queryResultCache.queryResultCacheTable;
expect(queryResultCacheTable).to.contain("mock");
})));

it("should cache results properly", () => Promise.all(connections.map(async connection => {

// first prepare data - insert users
const user1 = new User();
user1.firstName = "Timber";
user1.lastName = "Saw";
user1.isAdmin = false;
await connection.manager.save(user1);

const user2 = new User();
user2.firstName = "Alex";
user2.lastName = "Messer";
user2.isAdmin = false;
await connection.manager.save(user2);

const user3 = new User();
user3.firstName = "Umed";
user3.lastName = "Pleerock";
user3.isAdmin = true;
await connection.manager.save(user3);

// select for the first time with caching enabled
const users1 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.cache(true)
.getMany();
expect(users1.length).to.be.equal(1);

// insert new entity
const user4 = new User();
user4.firstName = "Bakhrom";
user4.lastName = "Brochik";
user4.isAdmin = true;
await connection.manager.save(user4);

// without cache it must return really how many there entities are
const users2 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.getMany();
expect(users2.length).to.be.equal(2);

// but with cache enabled it must not return newly inserted entity since cache is not expired yet
const users3 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.cache(true)
.getMany();
expect(users3.length).to.be.equal(1);

// give some time for cache to expire
await sleep(1000);

// now, when our cache has expired we check if we have new user inserted even with cache enabled
const users4 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.cache(true)
.getMany();
expect(users4.length).to.be.equal(2);

})));

it("should cache results with pagination enabled properly", () => Promise.all(connections.map(async connection => {

// first prepare data - insert users
const user1 = new User();
user1.firstName = "Timber";
user1.lastName = "Saw";
user1.isAdmin = false;
await connection.manager.save(user1);

const user2 = new User();
user2.firstName = "Alex";
user2.lastName = "Messer";
user2.isAdmin = false;
await connection.manager.save(user2);

const user3 = new User();
user3.firstName = "Umed";
user3.lastName = "Pleerock";
user3.isAdmin = true;
await connection.manager.save(user3);

// select for the first time with caching enabled
const users1 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.orderBy("user.id")
.cache(true)
.getMany();
expect(users1.length).to.be.equal(1);

// insert new entity
const user4 = new User();
user4.firstName = "Bakhrom";
user4.lastName = "Bro";
user4.isAdmin = false;
await connection.manager.save(user4);

// without cache it must return really how many there entities are
const users2 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.orderBy("user.id")
.getMany();
expect(users2.length).to.be.equal(2);

// but with cache enabled it must not return newly inserted entity since cache is not expired yet
const users3 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.cache(true)
.orderBy("user.id")
.getMany();
expect(users3.length).to.be.equal(1);

// give some time for cache to expire
await sleep(1000);

// now, when our cache has expired we check if we have new user inserted even with cache enabled
const users4 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.cache(true)
.orderBy("user.id")
.getMany();
expect(users4.length).to.be.equal(2);

})));

it("should cache results with custom id and duration supplied", () => Promise.all(connections.map(async connection => {

// first prepare data - insert users
const user1 = new User();
user1.firstName = "Timber";
user1.lastName = "Saw";
user1.isAdmin = false;
await connection.manager.save(user1);

const user2 = new User();
user2.firstName = "Alex";
user2.lastName = "Messer";
user2.isAdmin = false;
await connection.manager.save(user2);

const user3 = new User();
user3.firstName = "Umed";
user3.lastName = "Pleerock";
user3.isAdmin = true;
await connection.manager.save(user3);

// select for the first time with caching enabled
const users1 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.cache("user_admins", 2000)
.orderBy("user.id")
.getMany();
expect(users1.length).to.be.equal(1);

// insert new entity
const user4 = new User();
user4.firstName = "Bakhrom";
user4.lastName = "Bro";
user4.isAdmin = false;
await connection.manager.save(user4);

// without cache it must return really how many there entities are
const users2 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.orderBy("user.id")
.getMany();
expect(users2.length).to.be.equal(2);

// give some time for cache to expire
await sleep(1000);

// but with cache enabled it must not return newly inserted entity since cache is not expired yet
const users3 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.orderBy("user.id")
.cache("user_admins", 2000)
.getMany();
expect(users3.length).to.be.equal(1);

// give some time for cache to expire
await sleep(1000);

// now, when our cache has expired we check if we have new user inserted even with cache enabled
const users4 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: false })
.skip(1)
.take(5)
.orderBy("user.id")
.cache("user_admins", 2000)
.getMany();
expect(users4.length).to.be.equal(2);

})));

it("should cache results with custom id and duration supplied", () => Promise.all(connections.map(async connection => {

// first prepare data - insert users
const user1 = new User();
user1.firstName = "Timber";
user1.lastName = "Saw";
user1.isAdmin = false;
await connection.manager.save(user1);

const user2 = new User();
user2.firstName = "Alex";
user2.lastName = "Messer";
user2.isAdmin = false;
await connection.manager.save(user2);

const user3 = new User();
user3.firstName = "Umed";
user3.lastName = "Pleerock";
user3.isAdmin = true;
await connection.manager.save(user3);

// select for the first time with caching enabled
const users1 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.cache(true)
.getCount();
expect(users1).to.be.equal(1);

// insert new entity
const user4 = new User();
user4.firstName = "Bakhrom";
user4.lastName = "Brochik";
user4.isAdmin = true;
await connection.manager.save(user4);

// without cache it must return really how many there entities are
const users2 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.getCount();
expect(users2).to.be.equal(2);

// but with cache enabled it must not return newly inserted entity since cache is not expired yet
const users3 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.cache(true)
.getCount();
expect(users3).to.be.equal(1);

// give some time for cache to expire
await sleep(1000);

// now, when our cache has expired we check if we have new user inserted even with cache enabled
const users4 = await connection
.createQueryBuilder(User, "user")
.where("user.isAdmin = :isAdmin", { isAdmin: true })
.cache(true)
.getCount();
expect(users4).to.be.equal(2);

})));

});

0 comments on commit 6c6bde7

Please sign in to comment.