mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
added query builder result cache functionality
This commit is contained in:
parent
b51e199a4a
commit
838b336c4e
@ -51,4 +51,11 @@ services:
|
||||
image: "mongo:3.4.1"
|
||||
container_name: "typeorm-mongodb"
|
||||
ports:
|
||||
- "27017:27017"
|
||||
- "27017:27017"
|
||||
|
||||
# redis
|
||||
# redis:
|
||||
# image: "redis:3.0.3"
|
||||
# container_name: "typeorm-redis"
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
@ -24,6 +24,7 @@
|
||||
* Set locking
|
||||
* Partial selection
|
||||
* Using subqueries
|
||||
* Caching queries
|
||||
* Building `INSERT` query
|
||||
* Building `UPDATE` query
|
||||
* Building `DELETE` query
|
||||
@ -934,6 +935,108 @@ const posts = await connection
|
||||
.getRawMany();
|
||||
```
|
||||
|
||||
### Caching queries
|
||||
|
||||
You can cache results of `getMany`, `getOne`, `getRawMany`, `getRawOne` and `getCount` methods.
|
||||
To enable caching you need to explicitly enable it in connection options:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "mysql",
|
||||
host: "localhost",
|
||||
username: "test",
|
||||
...
|
||||
cache: true
|
||||
}
|
||||
```
|
||||
|
||||
When you enable cache for the first time,
|
||||
you must synchronize your database schema (using cli, migrations or simply option in connection).
|
||||
|
||||
Then in `QueryBuilder` you can enable query cache for any query:
|
||||
|
||||
```typescript
|
||||
const users = await connection
|
||||
.createQueryBuilder(User, "user")
|
||||
.where("user.isAdmin = :isAdmin", { isAdmin: true })
|
||||
.cache(true)
|
||||
.getMany();
|
||||
```
|
||||
|
||||
This will execute query to fetch all admin users and cache its result.
|
||||
Next time when you execute same code it will get admin users from cache.
|
||||
Default cache time is equal to `1000 ms`, e.g. 1 second.
|
||||
It means cache will be invalid in 1 second after you first time call query builder code.
|
||||
In practice it means if users open user page 150 times within 3 seconds only three queries will be executed during this period.
|
||||
All other inserted users during 1 second of caching won't be returned to user.
|
||||
|
||||
You can change cache time manually:
|
||||
|
||||
```typescript
|
||||
const users = await connection
|
||||
.createQueryBuilder(User, "user")
|
||||
.where("user.isAdmin = :isAdmin", { isAdmin: true })
|
||||
.cache(60000) // 1 minute
|
||||
.getMany();
|
||||
```
|
||||
|
||||
Or globally in connection options:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "mysql",
|
||||
host: "localhost",
|
||||
username: "test",
|
||||
...
|
||||
cache: {
|
||||
duration: 30000 // 30 seconds
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also you can set a "cache id":
|
||||
|
||||
```typescript
|
||||
const users = await connection
|
||||
.createQueryBuilder(User, "user")
|
||||
.where("user.isAdmin = :isAdmin", { isAdmin: true })
|
||||
.cache("users_admins", 25000)
|
||||
.getMany();
|
||||
```
|
||||
|
||||
It will allow you to granular control your cache,
|
||||
for example clear cached results when you insert a new user:
|
||||
|
||||
```typescript
|
||||
await connection.queryResultCache.remove(["users_admins"]);
|
||||
```
|
||||
|
||||
|
||||
By default, TypeORM uses separate table called `query-result-cache` and stores all queries and results there.
|
||||
If storing cache in a single database table is not effective for you,
|
||||
you can change cache type to "redis" and TypeORM will store all cache records in redis instead.
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "mysql",
|
||||
host: "localhost",
|
||||
username: "test",
|
||||
...
|
||||
cache: {
|
||||
type: "redis",
|
||||
options: {
|
||||
host: "localhost",
|
||||
port: 6379
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
"options" are [redis specific options](https://github.com/NodeRedis/node_redis#options-object-properties).
|
||||
|
||||
You can use `typeorm cache:clear` command to clear everything stored in cache.
|
||||
|
||||
## Building `INSERT` query
|
||||
|
||||
You can create `INSERT` queries using `QueryBuilder`.
|
||||
|
||||
2061
package-lock.json
generated
2061
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -62,10 +62,11 @@
|
||||
"mysql": "^2.14.1",
|
||||
"mysql2": "^1.4.1",
|
||||
"pg": "^7.3.0",
|
||||
"redis": "^2.8.0",
|
||||
"remap-istanbul": "^0.9.5",
|
||||
"sinon": "^2.4.1",
|
||||
"sinon-chai": "^2.13.0",
|
||||
"sqlite3": "^3.1.8",
|
||||
"sqlite3": "^3.1.10",
|
||||
"ts-node": "^3.3.0",
|
||||
"tslint": "^5.6.0",
|
||||
"tslint-stylish": "^2.1.0",
|
||||
|
||||
169
src/cache/DbQueryResultCache.ts
vendored
Normal file
169
src/cache/DbQueryResultCache.ts
vendored
Normal file
@ -0,0 +1,169 @@
|
||||
import {QueryResultCache} from "./QueryResultCache";
|
||||
import {QueryResultCacheOptions} from "./QueryResultCacheOptions";
|
||||
import {TableSchema} from "../schema-builder/schema/TableSchema";
|
||||
import {ColumnSchema} from "../schema-builder/schema/ColumnSchema";
|
||||
import {QueryRunner} from "../query-runner/QueryRunner";
|
||||
import {Connection} from "../connection/Connection";
|
||||
|
||||
/**
|
||||
* Caches query result into current database, into separate table called "query-result-cache".
|
||||
*/
|
||||
export class DbQueryResultCache implements QueryResultCache {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(protected connection: Connection) {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a connection with given cache provider.
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects with given cache provider.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates table for storing cache if it does not exist yet.
|
||||
*/
|
||||
async synchronize(queryRunner?: QueryRunner): Promise<void> {
|
||||
queryRunner = this.getQueryRunner(queryRunner);
|
||||
const driver = this.connection.driver;
|
||||
const tableExist = await queryRunner.hasTable("query-result-cache"); // todo: table name should be configurable
|
||||
if (tableExist)
|
||||
return;
|
||||
|
||||
await queryRunner.createTable(new TableSchema("query-result-cache", [ // createTableIfNotExist
|
||||
new ColumnSchema({
|
||||
name: "id",
|
||||
isNullable: true,
|
||||
isPrimary: true,
|
||||
type: driver.normalizeType({ type: driver.mappedDataTypes.cacheId }),
|
||||
generationStrategy: "increment",
|
||||
isGenerated: true
|
||||
}),
|
||||
new ColumnSchema({
|
||||
name: "identifier",
|
||||
type: driver.normalizeType({ type: driver.mappedDataTypes.cacheIdentifier }),
|
||||
isNullable: true,
|
||||
isUnique: true
|
||||
}),
|
||||
new ColumnSchema({
|
||||
name: "time",
|
||||
type: driver.normalizeType({ type: driver.mappedDataTypes.cacheTime }),
|
||||
isPrimary: false,
|
||||
isNullable: false
|
||||
}),
|
||||
new ColumnSchema({
|
||||
name: "duration",
|
||||
type: driver.normalizeType({ type: driver.mappedDataTypes.cacheDuration }),
|
||||
isPrimary: false,
|
||||
isNullable: false
|
||||
}),
|
||||
new ColumnSchema({
|
||||
name: "query",
|
||||
type: driver.normalizeType({ type: driver.mappedDataTypes.cacheQuery }),
|
||||
isPrimary: false,
|
||||
isNullable: false
|
||||
}),
|
||||
new ColumnSchema({
|
||||
name: "result",
|
||||
type: driver.normalizeType({ type: driver.mappedDataTypes.cacheResult }),
|
||||
isNullable: false
|
||||
}),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches given query result.
|
||||
* Returns cache result if found.
|
||||
* Returns undefined if result is not cached.
|
||||
*/
|
||||
getFromCache(options: QueryResultCacheOptions, queryRunner?: QueryRunner): Promise<QueryResultCacheOptions|undefined> {
|
||||
queryRunner = this.getQueryRunner(queryRunner);
|
||||
const qb = this.connection
|
||||
.createQueryBuilder(queryRunner)
|
||||
.select()
|
||||
.from("query-result-cache", "cache");
|
||||
|
||||
if (options.identifier) {
|
||||
return qb
|
||||
.where(`${qb.escape("cache")}.${qb.escape("identifier")} = :identifier`)
|
||||
.setParameters({ identifier: options.identifier })
|
||||
.getRawOne();
|
||||
|
||||
} else if (options.query) {
|
||||
return qb
|
||||
.where(`${qb.escape("cache")}.${qb.escape("query")} = :query`)
|
||||
.setParameters({ query: options.query })
|
||||
.getRawOne();
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if cache is expired or not.
|
||||
*/
|
||||
isExpired(savedCache: QueryResultCacheOptions): boolean {
|
||||
return (savedCache.time! + savedCache.duration) < new Date().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores given query result in the cache.
|
||||
*/
|
||||
async storeInCache(options: QueryResultCacheOptions, savedCache: QueryResultCacheOptions|undefined, queryRunner?: QueryRunner): Promise<void> {
|
||||
queryRunner = this.getQueryRunner(queryRunner);
|
||||
|
||||
if (savedCache && savedCache.identifier) { // if exist then update
|
||||
await queryRunner.update("query-result-cache", options, { identifier: options.identifier });
|
||||
|
||||
} else if (savedCache && savedCache.query) { // if exist then update
|
||||
await queryRunner.update("query-result-cache", options, { query: options.query });
|
||||
|
||||
} else { // otherwise insert
|
||||
await queryRunner.insert("query-result-cache", options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears everything stored in the cache.
|
||||
*/
|
||||
async clear(queryRunner: QueryRunner): Promise<void> {
|
||||
return this.getQueryRunner(queryRunner).truncate("query-result-cache");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cached results by given identifiers from cache.
|
||||
*/
|
||||
async remove(identifiers: string[], queryRunner?: QueryRunner): Promise<void> {
|
||||
await Promise.all(identifiers.map(identifier => {
|
||||
return this.getQueryRunner(queryRunner).delete("query-result-cache", { identifier });
|
||||
}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets a query runner to work with.
|
||||
*/
|
||||
protected getQueryRunner(queryRunner: QueryRunner|undefined): QueryRunner {
|
||||
if (queryRunner)
|
||||
return queryRunner;
|
||||
|
||||
return this.connection.createQueryRunner("master");
|
||||
}
|
||||
|
||||
}
|
||||
49
src/cache/QueryResultCache.ts
vendored
Normal file
49
src/cache/QueryResultCache.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
import {QueryResultCacheOptions} from "./QueryResultCacheOptions";
|
||||
import {QueryRunner} from "../query-runner/QueryRunner";
|
||||
|
||||
/**
|
||||
* Implementations of this interface provide different strategies to cache query builder results.
|
||||
*/
|
||||
export interface QueryResultCache {
|
||||
|
||||
/**
|
||||
* Creates a connection with given cache provider.
|
||||
*/
|
||||
connect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Closes a connection with given cache provider.
|
||||
*/
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Performs operations needs to be created during schema synchronization.
|
||||
*/
|
||||
synchronize(queryRunner?: QueryRunner): Promise<void>;
|
||||
|
||||
/**
|
||||
* Caches given query result.
|
||||
*/
|
||||
getFromCache(options: QueryResultCacheOptions, queryRunner?: QueryRunner): Promise<QueryResultCacheOptions|undefined>;
|
||||
|
||||
/**
|
||||
* Stores given query result in the cache.
|
||||
*/
|
||||
storeInCache(options: QueryResultCacheOptions, savedCache: QueryResultCacheOptions|undefined, queryRunner?: QueryRunner): Promise<void>;
|
||||
|
||||
/**
|
||||
* Checks if cache is expired or not.
|
||||
*/
|
||||
isExpired(savedCache: QueryResultCacheOptions): boolean;
|
||||
|
||||
/**
|
||||
* Clears everything stored in the cache.
|
||||
*/
|
||||
clear(queryRunner?: QueryRunner): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes all cached results by given identifiers from cache.
|
||||
*/
|
||||
remove(identifiers: string[], queryRunner?: QueryRunner): Promise<void>;
|
||||
|
||||
}
|
||||
36
src/cache/QueryResultCacheFactory.ts
vendored
Normal file
36
src/cache/QueryResultCacheFactory.ts
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
import {RedisQueryResultCache} from "./RedisQueryResultCache";
|
||||
import {DbQueryResultCache} from "./DbQueryResultCache";
|
||||
import {QueryResultCache} from "./QueryResultCache";
|
||||
import {Connection} from "../connection/Connection";
|
||||
|
||||
/**
|
||||
* Caches query result into Redis database.
|
||||
*/
|
||||
export class QueryResultCacheFactory {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(protected connection: Connection) {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new query result cache based on connection options.
|
||||
*/
|
||||
create(): QueryResultCache {
|
||||
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);
|
||||
|
||||
return new DbQueryResultCache(this.connection);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
32
src/cache/QueryResultCacheOptions.ts
vendored
Normal file
32
src/cache/QueryResultCacheOptions.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Options passed to QueryResultCache class.
|
||||
*/
|
||||
export interface QueryResultCacheOptions {
|
||||
|
||||
/**
|
||||
* Cache identifier set by user.
|
||||
* Can be empty.
|
||||
*/
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* Time, when cache was created.
|
||||
*/
|
||||
time?: number;
|
||||
|
||||
/**
|
||||
* Duration in milliseconds during which results will be returned from cache.
|
||||
*/
|
||||
duration: number;
|
||||
|
||||
/**
|
||||
* Cached query.
|
||||
*/
|
||||
query: string;
|
||||
|
||||
/**
|
||||
* Query result that will be cached.
|
||||
*/
|
||||
result?: any;
|
||||
|
||||
}
|
||||
172
src/cache/RedisQueryResultCache.ts
vendored
Normal file
172
src/cache/RedisQueryResultCache.ts
vendored
Normal file
@ -0,0 +1,172 @@
|
||||
import {QueryResultCache} from "./QueryResultCache";
|
||||
import {QueryResultCacheOptions} from "./QueryResultCacheOptions";
|
||||
import {PlatformTools} from "../platform/PlatformTools";
|
||||
import {Connection} from "../connection/Connection";
|
||||
import {QueryRunner} from "../query-runner/QueryRunner";
|
||||
|
||||
/**
|
||||
* Caches query result into Redis database.
|
||||
*/
|
||||
export class RedisQueryResultCache implements QueryResultCache {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Redis module instance loaded dynamically.
|
||||
*/
|
||||
protected redis: any;
|
||||
|
||||
/**
|
||||
* Connected redis client.
|
||||
*/
|
||||
protected client: any;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(protected connection: Connection) {
|
||||
this.redis = this.loadRedis();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a connection with given cache provider.
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
const cacheOptions: any = this.connection.options.cache;
|
||||
if (cacheOptions && cacheOptions.options) {
|
||||
this.client = this.redis.createClient(cacheOptions.options);
|
||||
} else {
|
||||
this.client = this.redis.createClient();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection with given cache provider.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
this.client.quit((err: any, result: any) => {
|
||||
if (err) return fail(err);
|
||||
ok();
|
||||
this.client = undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates table for storing cache if it does not exist yet.
|
||||
*/
|
||||
async synchronize(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches given query result.
|
||||
* Returns cache result if found.
|
||||
* Returns undefined if result is not cached.
|
||||
*/
|
||||
getFromCache(options: QueryResultCacheOptions, queryRunner?: QueryRunner): Promise<QueryResultCacheOptions|undefined> {
|
||||
return new Promise((ok, fail) => {
|
||||
|
||||
if (options.identifier) {
|
||||
this.client.get(options.identifier, (err: any, result: any) => {
|
||||
if (err) return fail(err);
|
||||
ok(JSON.parse(result));
|
||||
});
|
||||
|
||||
} else if (options.query) {
|
||||
this.client.get(options.query, (err: any, result: any) => {
|
||||
if (err) return fail(err);
|
||||
ok(JSON.parse(result));
|
||||
});
|
||||
|
||||
} else {
|
||||
ok(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if cache is expired or not.
|
||||
*/
|
||||
isExpired(savedCache: QueryResultCacheOptions): boolean {
|
||||
return (savedCache.time! + savedCache.duration) < new Date().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores given query result in the cache.
|
||||
*/
|
||||
async storeInCache(options: QueryResultCacheOptions, savedCache: QueryResultCacheOptions, queryRunner?: QueryRunner): Promise<void> {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
if (options.identifier) {
|
||||
this.client.set(options.identifier, JSON.stringify(options), (err: any, result: any) => {
|
||||
if (err) return fail(err);
|
||||
ok();
|
||||
});
|
||||
|
||||
} else if (options.query) {
|
||||
this.client.set(options.query, JSON.stringify(options), (err: any, result: any) => {
|
||||
if (err) return fail(err);
|
||||
ok();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears everything stored in the cache.
|
||||
*/
|
||||
async clear(queryRunner?: QueryRunner): Promise<void> {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
this.client.flushdb((err: any, result: any) => {
|
||||
if (err) return fail(err);
|
||||
ok();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cached results by given identifiers from cache.
|
||||
*/
|
||||
async remove(identifiers: string[], queryRunner?: QueryRunner): Promise<void> {
|
||||
await Promise.all(identifiers.map(identifier => {
|
||||
return this.deleteKey(identifier);
|
||||
}));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Removes a single key from redis database.
|
||||
*/
|
||||
protected deleteKey(key: string): Promise<void> {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
this.client.del(key, (err: any, result: any) => {
|
||||
if (err) return fail(err);
|
||||
ok();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads redis dependency.
|
||||
*/
|
||||
protected loadRedis(): any {
|
||||
try {
|
||||
return PlatformTools.load("redis");
|
||||
|
||||
} catch (e) {
|
||||
throw new Error(`Cannot use cache because redis is not installed. Please run "npm i redis --save".`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
60
src/commands/CacheClearCommand.ts
Normal file
60
src/commands/CacheClearCommand.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {createConnection} from "../index";
|
||||
import {ConnectionOptionsReader} from "../connection/ConnectionOptionsReader";
|
||||
import {Connection} from "../connection/Connection";
|
||||
const chalk = require("chalk");
|
||||
|
||||
/**
|
||||
* Clear cache command.
|
||||
*/
|
||||
export class CacheClearCommand {
|
||||
|
||||
command = "cache:clear";
|
||||
describe = "Clears all data stored in query runner cache.";
|
||||
|
||||
builder(yargs: any) {
|
||||
return yargs
|
||||
.option("c", {
|
||||
alias: "connection",
|
||||
default: "default",
|
||||
describe: "Name of the connection on which run a query."
|
||||
})
|
||||
.option("cf", {
|
||||
alias: "config",
|
||||
default: "ormconfig",
|
||||
describe: "Name of the file with connection configuration."
|
||||
});
|
||||
}
|
||||
|
||||
async handler(argv: any) {
|
||||
|
||||
let connection: Connection|undefined = undefined;
|
||||
try {
|
||||
const connectionOptionsReader = new ConnectionOptionsReader({ root: process.cwd(), configName: argv.config });
|
||||
const connectionOptions = await connectionOptionsReader.get(argv.connection);
|
||||
Object.assign(connectionOptions, {
|
||||
subscribers: [],
|
||||
dropSchemaOnConnection: false,
|
||||
autoSchemaSync: false,
|
||||
autoMigrationsRun: false,
|
||||
logging: { logQueries: false, logFailedQueryError: false, logSchemaCreation: true }
|
||||
});
|
||||
connection = await createConnection(connectionOptions);
|
||||
|
||||
if (!connection.queryResultCache)
|
||||
return console.log(chalk.black.bgRed("Cache is not enabled. To use cache enable it in connection configuration."));
|
||||
|
||||
await connection.queryResultCache.clear();
|
||||
console.log(chalk.green("Cache was successfully cleared"));
|
||||
|
||||
} catch (err) {
|
||||
console.log(chalk.black.bgRed("Error during cache clear:"));
|
||||
console.error(err);
|
||||
// throw err;
|
||||
|
||||
} finally {
|
||||
if (connection)
|
||||
await connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -104,6 +104,38 @@ export interface BaseConnectionOptions {
|
||||
*/
|
||||
readonly extra?: any;
|
||||
|
||||
/**
|
||||
* Allows to setup cache options.
|
||||
*/
|
||||
readonly cache?: boolean|{
|
||||
|
||||
/**
|
||||
* Type of caching.
|
||||
*
|
||||
* - "database" means cached values will be stored in the separate table in database. This is default value.
|
||||
* - "redis" means cached values will be stored inside redis. You must provide redis connection options.
|
||||
*/
|
||||
readonly type?: "database"|"redis"; // todo: add mongodb and other cache providers as well in the future
|
||||
|
||||
/**
|
||||
* Used to provide redis connection options.
|
||||
*/
|
||||
readonly options?: any;
|
||||
|
||||
/**
|
||||
* If set to true then queries (using find methods and QueryBuilder's methods) will always be cached.
|
||||
*/
|
||||
readonly alwaysEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Time in milliseconds in which cache will expire.
|
||||
* This can be setup per-query.
|
||||
* Default value is 1000 which is equivalent to 1 second.
|
||||
*/
|
||||
readonly duration?: number;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* CLI settings.
|
||||
*/
|
||||
|
||||
@ -25,6 +25,8 @@ import {ConnectionMetadataBuilder} from "./ConnectionMetadataBuilder";
|
||||
import {QueryRunner} from "../query-runner/QueryRunner";
|
||||
import {SelectQueryBuilder} from "../query-builder/SelectQueryBuilder";
|
||||
import {LoggerFactory} from "../logger/LoggerFactory";
|
||||
import {QueryResultCacheFactory} from "../cache/QueryResultCacheFactory";
|
||||
import {QueryResultCache} from "../cache/QueryResultCache";
|
||||
|
||||
/**
|
||||
* Connection is a single database ORM connection to a specific database.
|
||||
@ -87,6 +89,11 @@ export class Connection {
|
||||
*/
|
||||
readonly entityMetadatas: EntityMetadata[] = [];
|
||||
|
||||
/**
|
||||
* Used to work with query result cache.
|
||||
*/
|
||||
readonly queryResultCache?: QueryResultCache;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
@ -98,6 +105,7 @@ export class Connection {
|
||||
this.driver = new DriverFactory().create(this);
|
||||
this.manager = new EntityManagerFactory().create(this);
|
||||
this.namingStrategy = options.namingStrategy || new DefaultNamingStrategy();
|
||||
this.queryResultCache = options.cache ? new QueryResultCacheFactory(this).create() : undefined;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@ -134,6 +142,10 @@ export class Connection {
|
||||
// connect to the database via its driver
|
||||
await this.driver.connect();
|
||||
|
||||
// connect to the cache-specific database if cache is enabled
|
||||
if (this.queryResultCache)
|
||||
await this.queryResultCache.connect();
|
||||
|
||||
// set connected status for the current connection
|
||||
Object.assign(this, { isConnected: true });
|
||||
|
||||
@ -176,6 +188,11 @@ export class Connection {
|
||||
throw new CannotExecuteNotConnectedError(this.name);
|
||||
|
||||
await this.driver.disconnect();
|
||||
|
||||
// disconnect from the cache-specific database if cache was enabled
|
||||
if (this.queryResultCache)
|
||||
await this.queryResultCache.disconnect();
|
||||
|
||||
Object.assign(this, { isConnected: false });
|
||||
}
|
||||
|
||||
|
||||
@ -70,6 +70,12 @@ export class MongoDriver implements Driver {
|
||||
treeLevel: "int",
|
||||
migrationName: "int",
|
||||
migrationTimestamp: "int",
|
||||
cacheId: "int",
|
||||
cacheIdentifier: "int",
|
||||
cacheTime: "int",
|
||||
cacheDuration: "int",
|
||||
cacheQuery: "int",
|
||||
cacheResult: "int",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -119,7 +119,13 @@ export class MysqlDriver implements Driver {
|
||||
version: "int",
|
||||
treeLevel: "int",
|
||||
migrationName: "varchar",
|
||||
migrationTimestamp: "bigint"
|
||||
migrationTimestamp: "bigint",
|
||||
cacheId: "int",
|
||||
cacheIdentifier: "varchar",
|
||||
cacheTime: "bigint",
|
||||
cacheDuration: "int",
|
||||
cacheQuery: "text",
|
||||
cacheResult: "text",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -120,8 +120,14 @@ export class OracleDriver implements Driver {
|
||||
updateDateDefault: "CURRENT_TIMESTAMP",
|
||||
version: "number",
|
||||
treeLevel: "number",
|
||||
migrationName: "varchar",
|
||||
migrationName: "nvarchar",
|
||||
migrationTimestamp: "timestamp",
|
||||
cacheId: "int",
|
||||
cacheIdentifier: "nvarchar",
|
||||
cacheTime: "timestamp",
|
||||
cacheDuration: "int",
|
||||
cacheQuery: "text",
|
||||
cacheResult: "text",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -140,6 +140,12 @@ export class PostgresDriver implements Driver {
|
||||
treeLevel: "int",
|
||||
migrationName: "varchar",
|
||||
migrationTimestamp: "bigint",
|
||||
cacheId: "int",
|
||||
cacheIdentifier: "varchar",
|
||||
cacheTime: "bigint",
|
||||
cacheDuration: "int",
|
||||
cacheQuery: "text",
|
||||
cacheResult: "text",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -119,6 +119,12 @@ export class AbstractSqliteDriver implements Driver {
|
||||
treeLevel: "integer",
|
||||
migrationName: "varchar",
|
||||
migrationTimestamp: "bigint",
|
||||
cacheId: "int",
|
||||
cacheIdentifier: "varchar",
|
||||
cacheTime: "bigint",
|
||||
cacheDuration: "int",
|
||||
cacheQuery: "text",
|
||||
cacheResult: "text",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -126,6 +126,12 @@ export class SqlServerDriver implements Driver {
|
||||
treeLevel: "int",
|
||||
migrationName: "varchar",
|
||||
migrationTimestamp: "bigint",
|
||||
cacheId: "int",
|
||||
cacheIdentifier: "varchar",
|
||||
cacheTime: "bigint",
|
||||
cacheDuration: "int",
|
||||
cacheQuery: "text",
|
||||
cacheResult: "text",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -56,4 +56,34 @@ export interface MappedColumnTypes {
|
||||
*/
|
||||
migrationName: ColumnType;
|
||||
|
||||
/**
|
||||
* Column type for identifier column in query result cache table.
|
||||
*/
|
||||
cacheId: ColumnType;
|
||||
|
||||
/**
|
||||
* Column type for identifier column in query result cache table.
|
||||
*/
|
||||
cacheIdentifier: ColumnType;
|
||||
|
||||
/**
|
||||
* Column type for time column in query result cache table.
|
||||
*/
|
||||
cacheTime: ColumnType;
|
||||
|
||||
/**
|
||||
* Column type for duration column in query result cache table.
|
||||
*/
|
||||
cacheDuration: ColumnType;
|
||||
|
||||
/**
|
||||
* Column type for query column in query result cache table.
|
||||
*/
|
||||
cacheQuery: ColumnType;
|
||||
|
||||
/**
|
||||
* Column type for result column in query result cache table.
|
||||
*/
|
||||
cacheResult: ColumnType;
|
||||
|
||||
}
|
||||
@ -293,9 +293,9 @@ export abstract class QueryBuilder<Entity> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets sql to be executed with all parameters used in it.
|
||||
* Gets query to be executed with all parameters used in it.
|
||||
*/
|
||||
getSqlAndParameters(): [string, any[]] {
|
||||
getQueryAndParameters(): [string, any[]] {
|
||||
return this.connection.driver.escapeQueryWithParameters(this.getQuery(), this.getParameters());
|
||||
}
|
||||
|
||||
@ -303,7 +303,7 @@ export abstract class QueryBuilder<Entity> {
|
||||
* Executes sql generated by query builder and returns raw database results.
|
||||
*/
|
||||
async execute(): Promise<any> {
|
||||
const [sql, parameters] = this.getSqlAndParameters();
|
||||
const [sql, parameters] = this.getQueryAndParameters();
|
||||
const queryRunner = this.obtainQueryRunner();
|
||||
try {
|
||||
return await queryRunner.query(sql, parameters); // await is needed here because we are using finally
|
||||
|
||||
@ -166,6 +166,23 @@ export class QueryExpressionMap {
|
||||
*/
|
||||
aliasNamePrefixingEnabled: boolean = true;
|
||||
|
||||
/**
|
||||
* Indicates if query result cache is enabled or not.
|
||||
*/
|
||||
cache: boolean = false;
|
||||
|
||||
/**
|
||||
* Time in milliseconds in which cache will expire.
|
||||
* If not set then global caching time will be used.
|
||||
*/
|
||||
cacheDuration: number;
|
||||
|
||||
/**
|
||||
* Cache id.
|
||||
* Used to identifier your cache queries.
|
||||
*/
|
||||
cacheId: string;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
@ -287,6 +304,12 @@ export class QueryExpressionMap {
|
||||
map.disableEscaping = this.disableEscaping;
|
||||
map.ignoreParentTablesJoins = this.ignoreParentTablesJoins;
|
||||
map.enableRelationIdValues = this.enableRelationIdValues;
|
||||
map.extraAppendedAndWhereCondition = this.extraAppendedAndWhereCondition;
|
||||
map.subQuery = this.subQuery;
|
||||
map.aliasNamePrefixingEnabled = this.aliasNamePrefixingEnabled;
|
||||
map.cache = this.cache;
|
||||
map.cacheId = this.cacheId;
|
||||
map.cacheDuration = this.cacheDuration;
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import {QueryRunner} from "../query-runner/QueryRunner";
|
||||
import {WhereExpression} from "./WhereExpression";
|
||||
import {Brackets} from "./Brackets";
|
||||
import {SqliteDriver} from "../driver/sqlite/SqliteDriver";
|
||||
import {QueryResultCacheOptions} from "../cache/QueryResultCacheOptions";
|
||||
|
||||
/**
|
||||
* Allows to build complex sql queries in a fashion way and execute those queries.
|
||||
@ -892,13 +893,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
* Gets first raw result returned by execution of generated query builder sql.
|
||||
*/
|
||||
async getRawOne(): Promise<any> {
|
||||
if (this.expressionMap.lockMode === "optimistic")
|
||||
throw new OptimisticLockCanNotBeUsedError();
|
||||
|
||||
this.expressionMap.queryEntity = false;
|
||||
const results = await this.execute();
|
||||
return results[0];
|
||||
|
||||
return (await this.getRawMany())[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -909,7 +904,15 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
throw new OptimisticLockCanNotBeUsedError();
|
||||
|
||||
this.expressionMap.queryEntity = false;
|
||||
return this.execute();
|
||||
const queryRunner = this.obtainQueryRunner();
|
||||
try {
|
||||
return await this.loadRawResults(queryRunner);
|
||||
|
||||
} finally {
|
||||
if (queryRunner !== this.queryRunner) { // means we created our own query runner
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1006,7 +1009,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
*/
|
||||
async stream(): Promise<ReadStream> {
|
||||
this.expressionMap.queryEntity = false;
|
||||
const [sql, parameters] = this.getSqlAndParameters();
|
||||
const [sql, parameters] = this.getQueryAndParameters();
|
||||
const queryRunner = this.obtainQueryRunner();
|
||||
try {
|
||||
const releaseFn = () => {
|
||||
@ -1022,6 +1025,46 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables query result caching.
|
||||
*/
|
||||
cache(enabled: boolean): this;
|
||||
|
||||
/**
|
||||
* Enables query result caching and sets in milliseconds in which cache will expire.
|
||||
* If not set then global caching time will be used.
|
||||
*/
|
||||
cache(milliseconds: number): this;
|
||||
|
||||
/**
|
||||
* Enables query result caching and sets cache id and milliseconds in which cache will expire.
|
||||
*/
|
||||
cache(id: any, milliseconds?: number): this;
|
||||
|
||||
/**
|
||||
* Enables or disables query result caching.
|
||||
*/
|
||||
cache(enabledOrMillisecondsOrId: boolean|number|string, maybeMilliseconds?: number): this {
|
||||
|
||||
if (typeof enabledOrMillisecondsOrId === "boolean") {
|
||||
this.expressionMap.cache = enabledOrMillisecondsOrId;
|
||||
|
||||
} else if (typeof enabledOrMillisecondsOrId === "number") {
|
||||
this.expressionMap.cache = true;
|
||||
this.expressionMap.cacheDuration = enabledOrMillisecondsOrId;
|
||||
|
||||
} else if (typeof enabledOrMillisecondsOrId === "string" || typeof enabledOrMillisecondsOrId === "number") {
|
||||
this.expressionMap.cache = true;
|
||||
this.expressionMap.cacheId = enabledOrMillisecondsOrId;
|
||||
}
|
||||
|
||||
if (maybeMilliseconds) {
|
||||
this.expressionMap.cacheDuration = maybeMilliseconds;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
@ -1442,16 +1485,15 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
}).join(", ") + ")) as \"cnt\"";
|
||||
}
|
||||
|
||||
const [countQuerySql, countQueryParameters] = this.clone()
|
||||
const results = await this.clone()
|
||||
.mergeExpressionMap({ ignoreParentTablesJoins: true })
|
||||
.orderBy()
|
||||
.groupBy()
|
||||
.offset(undefined)
|
||||
.limit(undefined)
|
||||
.select(countSql)
|
||||
.getSqlAndParameters();
|
||||
.loadRawResults(queryRunner);
|
||||
|
||||
const results = await queryRunner.query(countQuerySql, countQueryParameters);
|
||||
if (!results || !results[0] || !results[0]["cnt"])
|
||||
return 0;
|
||||
|
||||
@ -1508,10 +1550,11 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
rawResults = await new SelectQueryBuilder(this.connection, queryRunner)
|
||||
.select(`DISTINCT ${querySelects.join(", ")} `)
|
||||
.addSelect(selects)
|
||||
.from(`(${this.clone().orderBy().groupBy().orderBy().getQuery()})`, "distinctAlias")
|
||||
.from(`(${this.clone().orderBy().groupBy().getQuery()})`, "distinctAlias")
|
||||
.offset(this.expressionMap.skip)
|
||||
.limit(this.expressionMap.take)
|
||||
.orderBy(orderBys)
|
||||
.cache(this.expressionMap.cache ? this.expressionMap.cache : this.expressionMap.cacheId, this.expressionMap.cacheDuration)
|
||||
.setParameters(this.getParameters())
|
||||
.getRawMany();
|
||||
|
||||
@ -1539,12 +1582,11 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
rawResults = await this.clone()
|
||||
.mergeExpressionMap({ extraAppendedAndWhereCondition: condition })
|
||||
.setParameters(parameters)
|
||||
.getRawMany();
|
||||
.loadRawResults(queryRunner);
|
||||
}
|
||||
|
||||
} else {
|
||||
const [sql, parameters] = this.getSqlAndParameters();
|
||||
rawResults = await queryRunner.query(sql, parameters);
|
||||
rawResults = await this.loadRawResults(queryRunner);
|
||||
}
|
||||
|
||||
if (rawResults.length > 0) {
|
||||
@ -1595,6 +1637,38 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
|
||||
return [selectString, orderByObject];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads raw results from the database.
|
||||
*/
|
||||
protected async loadRawResults(queryRunner: QueryRunner) {
|
||||
const [sql, parameters] = this.getQueryAndParameters();
|
||||
const cacheOptions = typeof this.connection.options.cache === "object" ? this.connection.options.cache : {};
|
||||
let savedQueryResultCacheOptions: QueryResultCacheOptions|undefined = undefined;
|
||||
if (this.connection.queryResultCache && (this.expressionMap.cache || cacheOptions.alwaysEnabled)) {
|
||||
savedQueryResultCacheOptions = await this.connection.queryResultCache.getFromCache({
|
||||
identifier: this.expressionMap.cacheId,
|
||||
query: this.getSql(),
|
||||
duration: this.expressionMap.cacheDuration || cacheOptions.duration || 1000
|
||||
}, queryRunner);
|
||||
if (savedQueryResultCacheOptions && !this.connection.queryResultCache.isExpired(savedQueryResultCacheOptions))
|
||||
return JSON.parse(savedQueryResultCacheOptions.result);
|
||||
}
|
||||
|
||||
const results = await queryRunner.query(sql, parameters);
|
||||
|
||||
if (this.connection.queryResultCache && (this.expressionMap.cache || cacheOptions.alwaysEnabled)) {
|
||||
await this.connection.queryResultCache.storeInCache({
|
||||
identifier: this.expressionMap.cacheId,
|
||||
query: this.getSql(),
|
||||
time: new Date().getTime(),
|
||||
duration: this.expressionMap.cacheDuration || cacheOptions.duration || 1000,
|
||||
result: JSON.stringify(results)
|
||||
}, savedQueryResultCacheOptions, queryRunner);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges into expression map given expression map properties.
|
||||
*/
|
||||
|
||||
@ -139,6 +139,8 @@ export interface QueryRunner {
|
||||
*/
|
||||
createTable(table: TableSchema): Promise<void>;
|
||||
|
||||
// todo: create createTableIfNotExist method
|
||||
|
||||
/**
|
||||
* Drops the table.
|
||||
*/
|
||||
|
||||
@ -63,6 +63,11 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
|
||||
await this.queryRunner.startTransaction();
|
||||
try {
|
||||
await this.executeSchemaSyncOperationsInProperOrder();
|
||||
|
||||
// if cache is enabled then perform cache-synchronization as well
|
||||
if (this.connection.queryResultCache)
|
||||
await this.connection.queryResultCache.synchronize(this.queryRunner);
|
||||
|
||||
await this.queryRunner.commitTransaction();
|
||||
|
||||
} catch (error) {
|
||||
@ -86,6 +91,11 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
|
||||
this.tableSchemas = await this.loadTableSchemas();
|
||||
this.queryRunner.enableSqlMemory();
|
||||
await this.executeSchemaSyncOperationsInProperOrder();
|
||||
|
||||
// if cache is enabled then perform cache-synchronization as well
|
||||
if (this.connection.queryResultCache) // todo: check this functionality
|
||||
await this.connection.queryResultCache.synchronize(this.queryRunner);
|
||||
|
||||
return this.queryRunner.getMemorySql();
|
||||
|
||||
} finally {
|
||||
|
||||
20
test/functional/query-builder/cache/entity/User.ts
vendored
Normal file
20
test/functional/query-builder/cache/entity/User.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
firstName: string;
|
||||
|
||||
@Column()
|
||||
lastName: string;
|
||||
|
||||
@Column()
|
||||
isAdmin: boolean;
|
||||
|
||||
}
|
||||
304
test/functional/query-builder/cache/query-builder-cache.ts
vendored
Normal file
304
test/functional/query-builder/cache/query-builder-cache.ts
vendored
Normal file
@ -0,0 +1,304 @@
|
||||
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";
|
||||
|
||||
describe("query builder > cache", () => {
|
||||
|
||||
let connections: Connection[];
|
||||
before(async () => connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
dropSchema: true,
|
||||
// cache: true,
|
||||
// cache: {
|
||||
// type: "redis",
|
||||
// options: {
|
||||
// host: "localhost",
|
||||
// }
|
||||
// }
|
||||
}));
|
||||
beforeEach(() => reloadTestingDatabases(connections));
|
||||
after(() => closeTestingConnections(connections));
|
||||
|
||||
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)
|
||||
.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)
|
||||
.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)
|
||||
.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)
|
||||
.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)
|
||||
.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)
|
||||
.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)
|
||||
.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)
|
||||
.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);
|
||||
|
||||
})));
|
||||
|
||||
});
|
||||
@ -24,7 +24,7 @@ describe("github issues > #521 Attributes in UPDATE in QB arent getting replaced
|
||||
.where("name = :name", {
|
||||
name: "Toyota",
|
||||
})
|
||||
.getSqlAndParameters();
|
||||
.getQueryAndParameters();
|
||||
query.should.not.be.empty;
|
||||
return parameters.length.should.eql(2);
|
||||
})));
|
||||
|
||||
@ -67,6 +67,39 @@ export interface TestingOptions {
|
||||
*/
|
||||
schema?: string;
|
||||
|
||||
/**
|
||||
* Schema name used for postgres driver.
|
||||
*/
|
||||
cache?: boolean|{
|
||||
|
||||
/**
|
||||
* Type of caching.
|
||||
*
|
||||
* - "database" means cached values will be stored in the separate table in database. This is default value.
|
||||
* - "mongodb" means cached values will be stored in mongodb database. You must provide mongodb connection options.
|
||||
* - "redis" means cached values will be stored inside redis. You must provide redis connection options.
|
||||
*/
|
||||
type?: "database"|"redis";
|
||||
|
||||
/**
|
||||
* Used to provide mongodb / redis connection options.
|
||||
*/
|
||||
options?: any;
|
||||
|
||||
/**
|
||||
* If set to true then queries (using find methods and QueryBuilder's methods) will always be cached.
|
||||
*/
|
||||
alwaysEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Time in milliseconds in which cache will expire.
|
||||
* This can be setup per-query.
|
||||
* Default value is 1000 which is equivalent to 1 second.
|
||||
*/
|
||||
duration?: number;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,6 +116,7 @@ export function setupSingleTestingConnection(driverType: DatabaseType, options:
|
||||
dropSchema: options.dropSchema ? options.dropSchema : false,
|
||||
schemaCreate: options.schemaCreate ? options.schemaCreate : false,
|
||||
enabledDrivers: [driverType],
|
||||
cache: options.cache,
|
||||
schema: options.schema ? options.schema : undefined
|
||||
});
|
||||
if (!testingConnections.length)
|
||||
@ -144,6 +178,7 @@ export function setupTestingConnections(options?: TestingOptions): ConnectionOpt
|
||||
autoSchemaSync: options && (options.entities || options.entitySchemas) ? options.schemaCreate : false,
|
||||
dropSchema: options && (options.entities || options.entitySchemas) ? options.dropSchema : false,
|
||||
schema: options && options.schema ? options.schema : undefined,
|
||||
cache: options ? options.cache : undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user