mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: implement exists query method (#9303)
Adding `Exists` method to query builder and EntityManager, to check whether a row exists given the conditions Closes: #2815 Co-authored-by: mortzprk <mortz.prk@gmail.com>
This commit is contained in:
parent
53fad8f235
commit
598e26980d
@ -107,6 +107,11 @@ export interface Driver {
|
||||
|
||||
cteCapabilities: CteCapabilities
|
||||
|
||||
/**
|
||||
* Dummy table name
|
||||
*/
|
||||
dummyTableName?: string
|
||||
|
||||
/**
|
||||
* Performs connection to the database.
|
||||
* Depend on driver type it may create a connection pool.
|
||||
|
||||
@ -225,6 +225,8 @@ export class OracleDriver implements Driver {
|
||||
enabled: false, // TODO: enable
|
||||
}
|
||||
|
||||
dummyTableName = "DUAL";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -209,6 +209,8 @@ export class SapDriver implements Driver {
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
dummyTableName = `SYS.DUMMY`;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -948,6 +948,23 @@ export class EntityManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any entity exists with the given condition
|
||||
*/
|
||||
exists<Entity>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
options?: FindManyOptions<Entity>,
|
||||
): Promise<boolean> {
|
||||
const metadata = this.connection.getMetadata(entityClass)
|
||||
return this.createQueryBuilder(
|
||||
entityClass,
|
||||
FindOptionsUtils.extractFindManyOptionsAlias(options) ||
|
||||
metadata.name,
|
||||
)
|
||||
.setFindOptions(options || {})
|
||||
.getExists()
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts entities that match given options.
|
||||
* Useful for pagination.
|
||||
|
||||
@ -1178,6 +1178,21 @@ export abstract class QueryBuilder<Entity extends ObjectLiteral> {
|
||||
})
|
||||
}
|
||||
|
||||
protected getExistsCondition(subQuery: any): [string, any[]] {
|
||||
const query = subQuery
|
||||
.clone()
|
||||
.orderBy()
|
||||
.groupBy()
|
||||
.offset(undefined)
|
||||
.limit(undefined)
|
||||
.skip(undefined)
|
||||
.take(undefined)
|
||||
.select("1")
|
||||
.setOption("disable-global-order")
|
||||
|
||||
return [`EXISTS (${query.getQuery()})`, query.getParameters()]
|
||||
}
|
||||
|
||||
private findColumnsForPropertyPath(
|
||||
propertyPath: string,
|
||||
): [Alias, string[], ColumnMetadata[]] {
|
||||
|
||||
@ -249,6 +249,10 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
|
||||
return this
|
||||
}
|
||||
|
||||
fromDummy(): SelectQueryBuilder<any> {
|
||||
return this.from(this.connection.driver.dummyTableName ?? "(SELECT 1 AS dummy_column)", "dummy_table");
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies FROM which entity's table select/update/delete will be executed.
|
||||
* Also sets a main string alias of the selection data.
|
||||
@ -1190,6 +1194,27 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new where EXISTS clause
|
||||
*/
|
||||
whereExists(subQuery: SelectQueryBuilder<any>): this {
|
||||
return this.where(...this.getExistsCondition(subQuery))
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new AND where EXISTS clause
|
||||
*/
|
||||
andWhereExists(subQuery: SelectQueryBuilder<any>): this {
|
||||
return this.andWhere(...this.getExistsCondition(subQuery))
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new OR where EXISTS clause
|
||||
*/
|
||||
orWhereExists(subQuery: SelectQueryBuilder<any>): this {
|
||||
return this.orWhere(...this.getExistsCondition(subQuery))
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new AND WHERE with conditions for the given ids.
|
||||
*
|
||||
@ -1752,6 +1777,50 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets exists
|
||||
* Returns whether any rows exists matching current query.
|
||||
*/
|
||||
async getExists(): Promise<boolean> {
|
||||
if (this.expressionMap.lockMode === "optimistic")
|
||||
throw new OptimisticLockCanNotBeUsedError()
|
||||
|
||||
const queryRunner = this.obtainQueryRunner()
|
||||
let transactionStartedByUs: boolean = false
|
||||
try {
|
||||
// start transaction if it was enabled
|
||||
if (
|
||||
this.expressionMap.useTransaction === true &&
|
||||
queryRunner.isTransactionActive === false
|
||||
) {
|
||||
await queryRunner.startTransaction()
|
||||
transactionStartedByUs = true
|
||||
}
|
||||
|
||||
this.expressionMap.queryEntity = false
|
||||
const results = await this.executeExistsQuery(queryRunner)
|
||||
|
||||
// close transaction if we started it
|
||||
if (transactionStartedByUs) {
|
||||
await queryRunner.commitTransaction()
|
||||
}
|
||||
|
||||
return results
|
||||
} catch (error) {
|
||||
// rollback transaction if we started it
|
||||
if (transactionStartedByUs) {
|
||||
try {
|
||||
await queryRunner.rollbackTransaction()
|
||||
} catch (rollbackError) {}
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
if (queryRunner !== this.queryRunner)
|
||||
// means we created our own query runner
|
||||
await queryRunner.release()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes built SQL query and returns entities and overall entities count (without limitation).
|
||||
* This method is useful to build pagination.
|
||||
@ -2912,6 +2981,20 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
|
||||
return parseInt(results[0]["cnt"])
|
||||
}
|
||||
|
||||
protected async executeExistsQuery(
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<boolean> {
|
||||
const results = await this.connection
|
||||
.createQueryBuilder()
|
||||
.fromDummy()
|
||||
.select("1", "row_exists")
|
||||
.whereExists(this)
|
||||
.limit(1)
|
||||
.loadRawResults(queryRunner)
|
||||
|
||||
return results.length > 0
|
||||
}
|
||||
|
||||
protected applyFindOptions() {
|
||||
// todo: convert relations: string[] to object map to simplify code
|
||||
// todo: same with selects
|
||||
|
||||
@ -451,6 +451,13 @@ export class Repository<Entity extends ObjectLiteral> {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any entity exists that match given options.
|
||||
*/
|
||||
exist(options?: FindManyOptions<Entity>): Promise<boolean> {
|
||||
return this.manager.exists(this.metadata.target, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts entities that match given options.
|
||||
* Useful for pagination.
|
||||
|
||||
7
test/functional/query-builder/exists/entity/Test.ts
Normal file
7
test/functional/query-builder/exists/entity/Test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Entity, PrimaryColumn } from "../../../../../src"
|
||||
|
||||
@Entity("tests")
|
||||
export class Test {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
}
|
||||
45
test/functional/query-builder/exists/query-builder-exists.ts
Normal file
45
test/functional/query-builder/exists/query-builder-exists.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
import { DataSource } from "../../../../src/data-source/DataSource"
|
||||
import { expect } from "chai"
|
||||
import { Test } from "./entity/Test"
|
||||
|
||||
describe("query builder > exist", () => {
|
||||
let connections: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [Test],
|
||||
schemaCreate: true,
|
||||
dropSchema: true,
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("Exists query of empty table should be false", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(Test)
|
||||
|
||||
const exist = await repo.exist()
|
||||
expect(exist).to.be.equal(false)
|
||||
}),
|
||||
))
|
||||
|
||||
it("Exists query of non empty table should be true", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const repo = connection.getRepository(Test)
|
||||
|
||||
await repo.save({ id: "ok" })
|
||||
await repo.save({ id: "nok" })
|
||||
|
||||
const exist = await repo.exist()
|
||||
expect(exist).to.be.equal(true)
|
||||
}),
|
||||
))
|
||||
})
|
||||
@ -135,6 +135,119 @@ describe("repository > find methods", () => {
|
||||
))
|
||||
})
|
||||
|
||||
describe("exists", function () {
|
||||
it("should return a True when no criteria given", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const postRepository = connection.getRepository(Post)
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const post = new Post()
|
||||
post.id = i
|
||||
post.title = "post #" + i
|
||||
post.categoryName = "other"
|
||||
await postRepository.save(post)
|
||||
}
|
||||
|
||||
// check exist method
|
||||
const exists = await postRepository.exist({
|
||||
order: { id: "ASC" },
|
||||
})
|
||||
exists.should.be.equal(true)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return True when matches the given criteria", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const postRepository = connection.getRepository(Post)
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const post = new Post()
|
||||
post.id = i
|
||||
post.title = "post #" + i
|
||||
post.categoryName = i % 2 === 0 ? "even" : "odd"
|
||||
await postRepository.save(post)
|
||||
}
|
||||
|
||||
// check exist method
|
||||
const exists = await postRepository.exist({
|
||||
where: { categoryName: "odd" },
|
||||
order: { id: "ASC" },
|
||||
})
|
||||
exists.should.be.equal(true)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return True when matches the given multiple criteria", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const postRepository = connection.getRepository(Post)
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const post = new Post()
|
||||
post.id = i
|
||||
post.title = "post #" + i
|
||||
post.categoryName = i % 2 === 0 ? "even" : "odd"
|
||||
post.isNew = i > 90
|
||||
await postRepository.save(post)
|
||||
}
|
||||
|
||||
// check exist method
|
||||
const exists = await postRepository.exist({
|
||||
where: { categoryName: "odd", isNew: true },
|
||||
order: { id: "ASC" },
|
||||
})
|
||||
exists.should.be.equal(true)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return True when matches the given find options", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const postRepository = connection.getRepository(Post)
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const post = new Post()
|
||||
post.id = i
|
||||
post.isNew = i > 90
|
||||
post.title = post.isNew
|
||||
? "new post #" + i
|
||||
: "post #" + i
|
||||
post.categoryName = i % 2 === 0 ? "even" : "odd"
|
||||
await postRepository.save(post)
|
||||
}
|
||||
|
||||
// check exist method
|
||||
const exists = await postRepository.exist()
|
||||
exists.should.be.equal(true)
|
||||
}),
|
||||
))
|
||||
|
||||
it("should return True when matches both criteria and find options", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const postRepository = connection.getRepository(Post)
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
const post = new Post()
|
||||
post.id = i
|
||||
post.isNew = i > 90
|
||||
post.title = post.isNew
|
||||
? "new post #" + i
|
||||
: "post #" + i
|
||||
post.categoryName = i % 2 === 0 ? "even" : "odd"
|
||||
await postRepository.save(post)
|
||||
}
|
||||
|
||||
// check exist method
|
||||
const exists = await postRepository.exist({
|
||||
where: { categoryName: "even", isNew: true },
|
||||
skip: 1,
|
||||
take: 2,
|
||||
order: { id: "ASC" },
|
||||
})
|
||||
exists.should.be.equal(true)
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
describe("find and findAndCount", function () {
|
||||
it("should return everything when no criteria given", () =>
|
||||
Promise.all(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user