mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: support explicit resource management in QueryRunner (#11701)
* feat: support explicit resource management in QueryRunner * test: improve tests for explicit resource management * feat: commit transaction on QueryRunner dispose
This commit is contained in:
parent
ea7a99a3c3
commit
bcee921ee0
@ -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.
|
||||
|
||||
@ -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<void> {
|
||||
// there's no clean-up necessary, so simply don't do anything here
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts transaction.
|
||||
*/
|
||||
|
||||
@ -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<void>
|
||||
|
||||
async [Symbol.asyncDispose](): Promise<void> {
|
||||
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<void>
|
||||
|
||||
/**
|
||||
* Executes a given SQL query.
|
||||
*/
|
||||
|
||||
@ -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<void>
|
||||
|
||||
[Symbol.asyncDispose](): Promise<void>
|
||||
|
||||
/**
|
||||
* Removes all tables from the currently connected database.
|
||||
* Be careful with using this method and avoid using it in production or migrations
|
||||
|
||||
@ -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)
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
63
test/functional/query-runner/async-dispose.ts
Normal file
63
test/functional/query-runner/async-dispose.ts
Normal file
@ -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
|
||||
}),
|
||||
))
|
||||
})
|
||||
10
test/functional/query-runner/entity/Company.ts
Normal file
10
test/functional/query-runner/entity/Company.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src"
|
||||
|
||||
@Entity()
|
||||
export class Company {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
19
test/functional/query-runner/entity/Employee.ts
Normal file
19
test/functional/query-runner/entity/Employee.ts
Normal file
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user