import {QueryResultCache} from "./QueryResultCache"; import {QueryResultCacheOptions} from "./QueryResultCacheOptions"; import {Table} from "../schema-builder/table/Table"; import {QueryRunner} from "../query-runner/QueryRunner"; import {Connection} from "../connection/Connection"; import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver"; import {MssqlParameter} from "../driver/sqlserver/MssqlParameter"; import {ObjectLiteral} from "../common/ObjectLiteral"; import {OracleDriver} from "../driver/oracle/OracleDriver"; import {PostgresConnectionOptions} from "../driver/postgres/PostgresConnectionOptions"; import {SqlServerConnectionOptions} from "../driver/sqlserver/SqlServerConnectionOptions"; /** * Caches query result into current database, into separate table called "query-result-cache". */ export class DbQueryResultCache implements QueryResultCache { // ------------------------------------------------------------------------- // Private properties // ------------------------------------------------------------------------- private queryResultCacheTable: string; // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(protected connection: Connection) { const options = this.connection.driver.options; this.queryResultCacheTable = this.connection.driver.buildTableName("query-result-cache", options.schema, options.database); } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Creates a connection with given cache provider. */ async connect(): Promise { } /** * Disconnects with given cache provider. */ async disconnect(): Promise { } /** * Creates table for storing cache if it does not exist yet. */ async synchronize(queryRunner?: QueryRunner): Promise { queryRunner = this.getQueryRunner(queryRunner); const driver = this.connection.driver; const tableExist = await queryRunner.hasTable(this.queryResultCacheTable); // todo: table name should be configurable if (tableExist) return; await queryRunner.createTable(new Table( { name: this.queryResultCacheTable, columns: [ { name: "id", isPrimary: true, isNullable: false, type: driver.normalizeType({type: driver.mappedDataTypes.cacheId}), generationStrategy: "increment", isGenerated: true }, { name: "identifier", type: driver.normalizeType({type: driver.mappedDataTypes.cacheIdentifier}), isNullable: true }, { name: "time", type: driver.normalizeType({type: driver.mappedDataTypes.cacheTime}), isPrimary: false, isNullable: false }, { name: "duration", type: driver.normalizeType({type: driver.mappedDataTypes.cacheDuration}), isPrimary: false, isNullable: false }, { name: "query", type: driver.normalizeType({type: driver.mappedDataTypes.cacheQuery}), isPrimary: false, isNullable: false }, { 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 { queryRunner = this.getQueryRunner(queryRunner); const qb = this.connection .createQueryBuilder(queryRunner) .select() .from(this.queryResultCacheTable, "cache"); if (options.identifier) { return qb .where(`${qb.escape("cache")}.${qb.escape("identifier")} = :identifier`) .setParameters({ identifier: this.connection.driver instanceof SqlServerDriver ? new MssqlParameter(options.identifier, "nvarchar") : options.identifier }) .getRawOne(); } else if (options.query) { if (this.connection.driver instanceof OracleDriver) { return qb .where(`dbms_lob.compare(${qb.escape("cache")}.${qb.escape("query")}, :query) = 0`, { query: options.query }) .getRawOne(); } return qb .where(`${qb.escape("cache")}.${qb.escape("query")} = :query`) .setParameters({ query: this.connection.driver instanceof SqlServerDriver ? new MssqlParameter(options.query, "nvarchar") : options.query }) .getRawOne(); } return Promise.resolve(undefined); } /** * Checks if cache is expired or not. */ isExpired(savedCache: QueryResultCacheOptions): boolean { return ((typeof savedCache.time === "string" ? parseInt(savedCache.time as any) : savedCache.time)! + savedCache.duration) < new Date().getTime(); } /** * Stores given query result in the cache. */ async storeInCache(options: QueryResultCacheOptions, savedCache: QueryResultCacheOptions|undefined, queryRunner?: QueryRunner): Promise { queryRunner = this.getQueryRunner(queryRunner); let insertedValues: ObjectLiteral = options; if (this.connection.driver instanceof SqlServerDriver) { // todo: bad abstraction, re-implement this part, probably better if we create an entity metadata for cache table insertedValues = { identifier: new MssqlParameter(options.identifier, "nvarchar"), time: new MssqlParameter(options.time, "bigint"), duration: new MssqlParameter(options.duration, "int"), query: new MssqlParameter(options.query, "nvarchar"), result: new MssqlParameter(options.result, "nvarchar"), }; } if (savedCache && savedCache.identifier) { // if exist then update const qb = queryRunner.manager .createQueryBuilder() .update(this.queryResultCacheTable) .set(insertedValues); qb.where(`${qb.escape("identifier")} = :condition`, { condition: insertedValues.identifier }); await qb.execute(); } else if (savedCache && savedCache.query) { // if exist then update const qb = queryRunner.manager .createQueryBuilder() .update(this.queryResultCacheTable) .set(insertedValues); if (this.connection.driver instanceof OracleDriver) { qb.where(`dbms_lob.compare("query", :condition) = 0`, { condition: insertedValues.query }); } else { qb.where(`${qb.escape("query")} = :condition`, { condition: insertedValues.query }); } await qb.execute(); } else { // otherwise insert await queryRunner.manager .createQueryBuilder() .insert() .into(this.queryResultCacheTable) .values(insertedValues) .execute(); } } /** * Clears everything stored in the cache. */ async clear(queryRunner: QueryRunner): Promise { return this.getQueryRunner(queryRunner).clearTable(this.queryResultCacheTable); } /** * Removes all cached results by given identifiers from cache. */ async remove(identifiers: string[], queryRunner?: QueryRunner): Promise { await Promise.all(identifiers.map(identifier => { const qb = this.getQueryRunner(queryRunner).manager.createQueryBuilder(); return qb.delete() .from(this.queryResultCacheTable) .where(`${qb.escape("identifier")} = :identifier`, {identifier}) .execute(); })); } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- /** * Gets a query runner to work with. */ protected getQueryRunner(queryRunner: QueryRunner|undefined): QueryRunner { if (queryRunner) return queryRunner; return this.connection.createQueryRunner("master"); } }