diff --git a/docs/docs/query-runner.md b/docs/docs/query-runner.md index 984aa1b62..5832cebac 100644 --- a/docs/docs/query-runner.md +++ b/docs/docs/query-runner.md @@ -15,34 +15,80 @@ const queryRunner = dataSource.createQueryRunner() ## Using `QueryRunner` -After you create a new instance of `QueryRunner` use `connect` method to actually get a connection from the connection pool: +After creating a new instance of `QueryRunner`, a connection will be acquired from the pool when you issue the first query: ```typescript const queryRunner = dataSource.createQueryRunner() -await queryRunner.connect() +await queryRunner.query("SELECT 1") +await queryRunner.release() ``` -**Important**: make sure to release it when it is not necessary anymore to make it available to the connection pool again: +You can also use `connect` method to directly get a connection from the connection pool: + +```typescript +const queryRunner = dataSource.createQueryRunner() +const clientConnection = await queryRunner.connect() +await queryRunner.release() +``` + +**Important**: make sure to release the `QueryRunner` when it is no longer necessary to return the connection back to the connection pool: ```typescript await queryRunner.release() ``` -After connection is released, it is not possible to use the query runner methods. +After `QueryRunner` is released, it is no longer possible to use the query runner methods. -`QueryRunner` has a bunch of methods you can use, it also has its own `EntityManager` instance, -which you can use through `manager` property to run `EntityManager` methods on a particular database connection -used by `QueryRunner` instance: +`QueryRunner` also has its own `EntityManager` instance, which you can use through the `manager` property to run `EntityManager` queries on a particular database connection used by the `QueryRunner` instance: ```typescript -const queryRunner = dataSource.createQueryRunner() - -// take a connection from the connection pool -await queryRunner.connect() - -// use this particular connection to execute queries -const users = await queryRunner.manager.find(User) - -// remember to release connection after you are done using it -await queryRunner.release() +let queryRunner: QueryRunner +try { + queryRunner = dataSource.createQueryRunner() + // use a single database connection to execute multiple queries + await queryRunner.manager.update( + Employee, + { level: "junior" }, + { bonus: 0.2 }, + ) + await queryRunner.manager.update( + Employee, + { level: "senior" }, + { bonus: 0.1 }, + ) +} catch (error) { + console.error(error) +} finally { + // remember to release connection after you are done using it + await queryRunner.release() +} ``` + +## Explicit Resource Management + +`QueryRunner` also supports explicit resource management: + +```typescript +async function updateSalaries() { + await using queryRunner = dataSource.createQueryRunner() + await queryRunner.manager.update( + Employee, + { level: "junior" }, + { bonus: 0.2 }, + ) + await queryRunner.manager.update( + Employee, + { level: "senior" }, + { bonus: 0.1 }, + ) + // no need anymore to manually release the QueryRunner +} + +try { + await updateSalaries() +} catch (error) { + console.error(error) +} +``` + +When declaring a query runner like this, it will be automatically released after the last statement in the containing scope was executed. diff --git a/src/driver/mongodb/MongoQueryRunner.ts b/src/driver/mongodb/MongoQueryRunner.ts index 98ec2bc7d..3817a4882 100644 --- a/src/driver/mongodb/MongoQueryRunner.ts +++ b/src/driver/mongodb/MongoQueryRunner.ts @@ -544,6 +544,10 @@ export class MongoQueryRunner implements QueryRunner { // releasing connection are not supported by mongodb driver, so simply don't do anything here } + async [Symbol.asyncDispose](): Promise { + // there's no clean-up necessary, so simply don't do anything here + } + /** * Starts transaction. */ diff --git a/src/query-runner/BaseQueryRunner.ts b/src/query-runner/BaseQueryRunner.ts index ce0c48e64..9619b3158 100644 --- a/src/query-runner/BaseQueryRunner.ts +++ b/src/query-runner/BaseQueryRunner.ts @@ -18,7 +18,7 @@ import { MetadataTableType } from "../driver/types/MetadataTableType" import { InstanceChecker } from "../util/InstanceChecker" import { buildSqlTag } from "../util/SqlTagUtils" -export abstract class BaseQueryRunner { +export abstract class BaseQueryRunner implements AsyncDisposable { // ------------------------------------------------------------------------- // Public Properties // ------------------------------------------------------------------------- @@ -103,6 +103,29 @@ export abstract class BaseQueryRunner { // Public Abstract Methods // ------------------------------------------------------------------------- + /** + * Releases used database connection. + * You cannot use query runner methods after connection is released. + */ + abstract release(): Promise + + async [Symbol.asyncDispose](): Promise { + try { + if (this.isTransactionActive) { + this.transactionDepth = 1 // ignore all savepoints and commit directly + await this.commitTransaction() + } + } finally { + await this.release() + } + } + + /** + * Commits transaction. + * Error will be thrown if transaction was not started. + */ + abstract commitTransaction(): Promise + /** * Executes a given SQL query. */ diff --git a/src/query-runner/QueryRunner.ts b/src/query-runner/QueryRunner.ts index a88f4662d..911f9816d 100644 --- a/src/query-runner/QueryRunner.ts +++ b/src/query-runner/QueryRunner.ts @@ -19,7 +19,7 @@ import { ReplicationMode } from "../driver/types/ReplicationMode" /** * Runs queries on a single database connection. */ -export interface QueryRunner { +export interface QueryRunner extends AsyncDisposable { /** * Connection used by this query runner. */ @@ -88,6 +88,8 @@ export interface QueryRunner { */ release(): Promise + [Symbol.asyncDispose](): Promise + /** * Removes all tables from the currently connected database. * Be careful with using this method and avoid using it in production or migrations diff --git a/test/functional/driver/sap/connection-pool.ts b/test/functional/driver/sap/connection-pool.ts index 90527616a..ddba3b91e 100644 --- a/test/functional/driver/sap/connection-pool.ts +++ b/test/functional/driver/sap/connection-pool.ts @@ -69,4 +69,24 @@ describe("driver > sap > connection pool", () => { expect(poolClient.getPooledCount()).to.equal(3) }), )) + + it("should be managed correctly with explicit resource management ", () => + Promise.all( + dataSources.map(async (dataSource) => { + const poolClient = (dataSource.driver as SapDriver) + .master as ConnectionPool + expect(poolClient.getInUseCount()).to.equal(0) + expect(poolClient.getPooledCount()).to.be.at.most(3) + + { + await using queryRunner = dataSource.createQueryRunner() + await queryRunner.connect() + expect(poolClient.getInUseCount()).to.equal(1) + expect(poolClient.getPooledCount()).to.be.at.most(2) + } + + expect(poolClient.getInUseCount()).to.equal(0) + expect(poolClient.getPooledCount()).to.be.at.most(3) + }), + )) }) diff --git a/test/functional/query-runner/async-dispose.ts b/test/functional/query-runner/async-dispose.ts new file mode 100644 index 000000000..99b0a61af --- /dev/null +++ b/test/functional/query-runner/async-dispose.ts @@ -0,0 +1,63 @@ +import { expect } from "chai" +import "reflect-metadata" +import sinon from "sinon" +import { QueryFailedError } from "../../../src" +import { DataSource } from "../../../src/data-source/DataSource" +import { + closeTestingConnections, + createTestingConnections, + reloadTestingDatabases, +} from "../../utils/test-utils" +import { Company } from "./entity/Company" +import { Employee } from "./entity/Employee" + +describe("query runner > async dispose", () => { + let dataSources: DataSource[] + before(async () => { + dataSources = await createTestingConnections({ + entities: [Employee, Company], + enabledDrivers: ["postgres"], // this is rather a unit test, so a single driver is enough + }) + }) + beforeEach(() => reloadTestingDatabases(dataSources)) + after(() => closeTestingConnections(dataSources)) + + it("should release query runner", () => + Promise.all( + dataSources.map(async (dataSource) => { + let releaseSpy: sinon.SinonSpy + { + await using queryRunner = dataSource.createQueryRunner() + releaseSpy = sinon.spy(queryRunner, "release") + await queryRunner.connect() + } + + expect(releaseSpy).to.have.been.calledOnce + }), + )) + + it("should commit the transaction in progress", () => + Promise.all( + dataSources.map(async (dataSource) => { + let releaseSpy: sinon.SinonSpy | null = null + let error: Error | null = null + + async function insertEmployee() { + await using queryRunner = dataSource.createQueryRunner() + releaseSpy = sinon.spy(queryRunner, "release") + await queryRunner.startTransaction("READ UNCOMMITTED") + await queryRunner.sql`INSERT INTO "employee"("name", "companyId") VALUES ('John Doe', 100)` + } + + try { + await insertEmployee() + } catch (e) { + error = e + } + + expect(error).to.be.instanceOf(QueryFailedError) + expect((error as QueryFailedError).query).to.equal("COMMIT") + expect(releaseSpy).to.have.been.calledOnce + }), + )) +}) diff --git a/test/functional/query-runner/entity/Company.ts b/test/functional/query-runner/entity/Company.ts new file mode 100644 index 000000000..4a2c8df30 --- /dev/null +++ b/test/functional/query-runner/entity/Company.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src" + +@Entity() +export class Company { + @PrimaryGeneratedColumn() + id: number + + @Column() + name: string +} diff --git a/test/functional/query-runner/entity/Employee.ts b/test/functional/query-runner/entity/Employee.ts new file mode 100644 index 000000000..154822d90 --- /dev/null +++ b/test/functional/query-runner/entity/Employee.ts @@ -0,0 +1,19 @@ +import { + Column, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from "../../../../src" +import { Company } from "./Company" + +@Entity() +export class Employee { + @PrimaryGeneratedColumn() + id: number + + @Column() + name: string + + @ManyToOne(() => Company, { deferrable: "INITIALLY DEFERRED" }) + company: Company +}