feat: add updateAll and deleteAll methods to EntityManager and Repository APIs (#11459)

* Add updateAll() methods

* Add deleteAll() methods

* Fix softDelete/restore error messages

* Add test for Repository.delete() with criteria

* Add test for Repository.deleteAll() method

* Move/rename “Repository > delete” test files

* Tweak comments

* Add tests for Repository update methods

* Add updateAll and deleteAll to EntityManager API docs

* Add updateAll and deleteAll to Repository API docs

* Fix tests
This commit is contained in:
Simon Garner 2025-05-13 12:33:44 +12:00 committed by GitHub
parent a6b61f7645
commit 23bb1ee271
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 461 additions and 110 deletions

View File

@ -176,7 +176,7 @@ await manager.insert(User, [
])
```
- `update` - Partially updates entity by a given update options or entity id.
- `update` - Updates entities by entity id, ids or given conditions. Sets fields from supplied partial entity.
```typescript
await manager.update(User, { age: 18 }, { category: "ADULT" })
@ -186,6 +186,13 @@ await manager.update(User, 1, { firstName: "Rizzrak" })
// executes UPDATE user SET firstName = Rizzrak WHERE id = 1
```
- `updateAll` - Updates *all* entities of target type (without WHERE clause). Sets fields from supplied partial entity.
```typescript
await manager.updateAll(User, { category: "ADULT" })
// executes UPDATE user SET category = ADULT
```
- `upsert` - Inserts a new entity or array of entities unless they already exist in which case they are updated instead. Supported by AuroraDataApi, Cockroach, Mysql, Postgres, and Sqlite database drivers.
```typescript
@ -206,7 +213,7 @@ await manager.upsert(
**/
```
- `delete` - Deletes entities by entity id, ids or given conditions:
- `delete` - Deletes entities by entity id, ids or given conditions.
```typescript
await manager.delete(User, 1)
@ -214,6 +221,15 @@ await manager.delete(User, [1, 2, 3])
await manager.delete(User, { firstName: "Timber" })
```
- `deleteAll` - Deletes *all* entities of target type (without WHERE clause).
```typescript
await manager.deleteAll(User)
// executes DELETE FROM user
```
Refer also to the `clear` method, which performs database `TRUNCATE TABLE` operation instead.
- `increment` - Increments some column by provided value of entities that match given options.
```typescript

View File

@ -140,7 +140,7 @@ await repository.insert([
])
```
- `update` - Partially updates entity by a given update options or entity id.
- `update` - Updates entities by entity id, ids or given conditions. Sets fields from supplied partial entity.
```typescript
await repository.update({ age: 18 }, { category: "ADULT" })
@ -150,6 +150,13 @@ await repository.update(1, { firstName: "Rizzrak" })
// executes UPDATE user SET firstName = Rizzrak WHERE id = 1
```
- `updateAll` - Updates *all* entities of target type (without WHERE clause). Sets fields from supplied partial entity.
```typescript
await repository.updateAll({ category: "ADULT" })
// executes UPDATE user SET category = ADULT
```
- `upsert` - Inserts a new entity or array of entities unless they already exist in which case they are updated instead. Supported by AuroraDataApi, Cockroach, Mysql, Postgres, and Sqlite database drivers.
```typescript
@ -224,6 +231,15 @@ await repository.delete([1, 2, 3])
await repository.delete({ firstName: "Timber" })
```
- `deleteAll` - Deletes *all* entities of target type (without WHERE clause).
```typescript
await repository.deleteAll()
// executes DELETE FROM user
```
Refer also to the `clear` method, which performs database `TRUNCATE TABLE` operation instead.
- `softDelete` and `restore` - Soft deleting and restoring a row by id, ids, or given conditions:
```typescript
@ -399,8 +415,8 @@ const rawData = await repository.query(`SELECT * FROM USERS`)
// You can also use parameters to avoid SQL injection
// The syntax differs between the drivers
// aurora-mysql, better-sqlite3, capacitor, cordova,
// expo, mariadb, mysql, nativescript, react-native,
// aurora-mysql, better-sqlite3, capacitor, cordova,
// expo, mariadb, mysql, nativescript, react-native,
// sap, sqlite, sqljs
const rawData = await repository.query(
'SELECT * FROM USERS WHERE name = ? and age = ?',

View File

@ -806,6 +806,23 @@ export class EntityManager {
}
}
/**
* Updates all entities of target type, setting fields from supplied partial entity.
* This is a primitive operation without cascades, relations or other operations included.
* Executes fast and efficient UPDATE query without WHERE clause.
*
* WARNING! This method updates ALL rows in the target table.
*/
updateAll<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
partialEntity: QueryDeepPartialEntity<Entity>,
): Promise<UpdateResult> {
return this.createQueryBuilder()
.update(target)
.set(partialEntity)
.execute()
}
/**
* Deletes entities by a given condition(s).
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
@ -850,10 +867,23 @@ export class EntityManager {
}
}
/**
* Deletes all entities of target type.
* This is a primitive operation without cascades, relations or other operations included.
* Executes fast and efficient DELETE query without WHERE clause.
*
* WARNING! This method deletes ALL rows in the target table.
*/
deleteAll<Entity extends ObjectLiteral>(
targetOrEntity: EntityTarget<Entity>,
): Promise<DeleteResult> {
return this.createQueryBuilder().delete().from(targetOrEntity).execute()
}
/**
* Records the delete date of entities by a given condition(s).
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
* Executes fast and efficient DELETE query.
* Executes fast and efficient UPDATE query.
* Does not check if entity exist in the database.
* Condition(s) cannot be empty.
*/
@ -874,7 +904,7 @@ export class EntityManager {
if (OrmUtils.isCriteriaNullOrEmpty(criteria)) {
return Promise.reject(
new TypeORMError(
`Empty criteria(s) are not allowed for the delete method.`,
`Empty criteria(s) are not allowed for the softDelete method.`,
),
)
}
@ -918,7 +948,7 @@ export class EntityManager {
if (OrmUtils.isCriteriaNullOrEmpty(criteria)) {
return Promise.reject(
new TypeORMError(
`Empty criteria(s) are not allowed for the delete method.`,
`Empty criteria(s) are not allowed for the restore method.`,
),
)
}

View File

@ -361,12 +361,25 @@ export class Repository<Entity extends ObjectLiteral> {
partialEntity: QueryDeepPartialEntity<Entity>,
): Promise<UpdateResult> {
return this.manager.update(
this.metadata.target as any,
criteria as any,
this.metadata.target,
criteria,
partialEntity,
)
}
/**
* Updates all entities of target type, setting fields from supplied partial entity.
* This is a primitive operation without cascades, relations or other operations included.
* Executes fast and efficient UPDATE query without WHERE clause.
*
* WARNING! This method updates ALL rows in the target table.
*/
updateAll(
partialEntity: QueryDeepPartialEntity<Entity>,
): Promise<UpdateResult> {
return this.manager.updateAll(this.metadata.target, partialEntity)
}
/**
* Inserts a given entity into the database, unless a unique constraint conflicts then updates the entity
* Unlike save method executes a primitive operation without cascades, relations and other operations included.
@ -404,7 +417,18 @@ export class Repository<Entity extends ObjectLiteral> {
| FindOptionsWhere<Entity>
| FindOptionsWhere<Entity>[],
): Promise<DeleteResult> {
return this.manager.delete(this.metadata.target as any, criteria as any)
return this.manager.delete(this.metadata.target, criteria)
}
/**
* Deletes all entities of target type.
* This is a primitive operation without cascades, relations or other operations included.
* Executes fast and efficient DELETE query without WHERE clause.
*
* WARNING! This method deletes ALL rows in the target table.
*/
deleteAll(): Promise<DeleteResult> {
return this.manager.deleteAll(this.metadata.target)
}
/**

View File

@ -0,0 +1,177 @@
import "reflect-metadata"
import { expect } from "chai"
import { DataSource } from "../../../../src/data-source/DataSource"
import { Post } from "./entity/Post"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../../utils/test-utils"
describe("repository > delete methods", function () {
// -------------------------------------------------------------------------
// Configuration
// -------------------------------------------------------------------------
let connections: DataSource[]
before(
async () =>
(connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
})),
)
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
// -------------------------------------------------------------------------
// Specifications
// -------------------------------------------------------------------------
it("remove using delete method should delete successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// remove one
await postRepository.delete(1)
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(3)
expect(loadedPosts.find((p) => p.id === 1)).to.be.undefined
expect(loadedPosts.find((p) => p.id === 2)).not.to.be.undefined
expect(loadedPosts.find((p) => p.id === 3)).not.to.be.undefined
expect(loadedPosts.find((p) => p.id === 4)).not.to.be.undefined
}),
))
it("remove multiple rows using delete method should delete successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// remove multiple
await postRepository.delete([2, 3])
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(2)
expect(loadedPosts.find((p) => p.id === 1)).not.to.be.undefined
expect(loadedPosts.find((p) => p.id === 2)).to.be.undefined
expect(loadedPosts.find((p) => p.id === 3)).to.be.undefined
expect(loadedPosts.find((p) => p.id === 4)).not.to.be.undefined
}),
))
it("remove row using delete method with partial criteria should delete successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// remove with criteria
await postRepository.delete({ title: "Super post #3" })
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(3)
expect(loadedPosts.find((p) => p.title === "Super post #1")).not
.to.be.undefined
expect(loadedPosts.find((p) => p.title === "Super post #2")).not
.to.be.undefined
expect(loadedPosts.find((p) => p.title === "Super post #3")).to
.be.undefined
expect(loadedPosts.find((p) => p.title === "Super post #4")).not
.to.be.undefined
}),
))
it("removes all rows using deleteAll method", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// remove all
await postRepository.deleteAll()
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(0)
expect(loadedPosts.find((p) => p.title === "Super post #1")).to
.be.undefined
expect(loadedPosts.find((p) => p.title === "Super post #2")).to
.be.undefined
expect(loadedPosts.find((p) => p.title === "Super post #3")).to
.be.undefined
expect(loadedPosts.find((p) => p.title === "Super post #4")).to
.be.undefined
}),
))
})

View File

@ -1,99 +0,0 @@
import "reflect-metadata"
import { expect } from "chai"
import { DataSource } from "../../../../src/data-source/DataSource"
import { Post } from "./entity/Post"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../../utils/test-utils"
describe("repository > deleteById methods", function () {
// -------------------------------------------------------------------------
// Configuration
// -------------------------------------------------------------------------
let connections: DataSource[]
before(
async () =>
(connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
})),
)
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
// -------------------------------------------------------------------------
// Specifications
// -------------------------------------------------------------------------
it("remove using deleteById method should delete successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save a new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// remove one
await postRepository.delete(1)
// load to check
const loadedPosts = await postRepository.find()
// assert
loadedPosts.length.should.be.equal(3)
expect(loadedPosts.find((p) => p.id === 1)).to.be.undefined
expect(loadedPosts.find((p) => p.id === 2)).not.to.be.undefined
expect(loadedPosts.find((p) => p.id === 3)).not.to.be.undefined
expect(loadedPosts.find((p) => p.id === 4)).not.to.be.undefined
}),
))
it("remove using removeByIds method should delete successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save a new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// remove multiple
await postRepository.delete([2, 3])
// load to check
const loadedPosts = await postRepository.find()
// assert
loadedPosts.length.should.be.equal(2)
expect(loadedPosts.find((p) => p.id === 1)).not.to.be.undefined
expect(loadedPosts.find((p) => p.id === 2)).to.be.undefined
expect(loadedPosts.find((p) => p.id === 3)).to.be.undefined
expect(loadedPosts.find((p) => p.id === 4)).not.to.be.undefined
}),
))
})

View File

@ -0,0 +1,12 @@
import { Entity } from "../../../../../src/decorator/entity/Entity"
import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"
import { Column } from "../../../../../src/decorator/columns/Column"
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
}

View File

@ -0,0 +1,175 @@
import "reflect-metadata"
import { expect } from "chai"
import { DataSource } from "../../../../src/data-source/DataSource"
import { Post } from "./entity/Post"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../../utils/test-utils"
import { MoreThan } from "../../../../src"
describe("repository > update methods", function () {
// -------------------------------------------------------------------------
// Configuration
// -------------------------------------------------------------------------
let connections: DataSource[]
before(
async () =>
(connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
})),
)
beforeEach(() => reloadTestingDatabases(connections))
after(() => closeTestingConnections(connections))
// -------------------------------------------------------------------------
// Specifications
// -------------------------------------------------------------------------
it("mutate using update method should update successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// update one
await postRepository.update(1, { title: "Super duper post #1" })
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(4)
expect(
loadedPosts.filter((p) => p.title === "Super duper post #1")
.length,
).to.equal(1)
}),
))
it("mutate multiple rows using update method should update successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// update multiple
await postRepository.update([1, 2], {
title: "Updated post title",
})
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(4)
expect(
loadedPosts.filter((p) => p.title === "Updated post title")
.length,
).to.equal(2)
}),
))
it("mutate multiple rows using update method with partial criteria should update successfully", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// update multiple
await postRepository.update(
{ id: MoreThan(2) },
{ title: "Updated post title" },
)
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(4)
expect(
loadedPosts.filter((p) => p.title === "Updated post title")
.length,
).to.equal(2)
}),
))
it("mutates all rows using updateAll method", () =>
Promise.all(
connections.map(async (connection) => {
const postRepository = connection.getRepository(Post)
// save some new posts
const newPost1 = postRepository.create()
newPost1.title = "Super post #1"
const newPost2 = postRepository.create()
newPost2.title = "Super post #2"
const newPost3 = postRepository.create()
newPost3.title = "Super post #3"
const newPost4 = postRepository.create()
newPost4.title = "Super post #4"
await postRepository.save(newPost1)
await postRepository.save(newPost2)
await postRepository.save(newPost3)
await postRepository.save(newPost4)
// update all
await postRepository.updateAll({ title: "Updated post title" })
// load to check
const loadedPosts = await postRepository.find()
// assert
expect(loadedPosts.length).to.equal(4)
expect(
loadedPosts.filter((p) => p.title === "Updated post title")
.length,
).to.equal(4)
}),
))
})