-
-
Notifications
You must be signed in to change notification settings - Fork 503
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
52 changed files
with
8,271 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
--- | ||
title: Cascading persist, merge and remove | ||
sidebar_label: Cascading | ||
--- | ||
|
||
When persisting or removing entity, all your references are by default cascade persisted. | ||
This means that by persisting any entity, ORM will automatically persist all of its | ||
associations. | ||
|
||
You can control this behaviour via `cascade` attribute of `@ManyToOne`, `@ManyToMany`, | ||
`@OneToMany` and `@OneToOne` fields. | ||
|
||
> New entities without primary key will be always persisted, regardless of `cascade` value. | ||
```typescript | ||
// cascade persist & merge is default value | ||
@OneToMany({ entity: () => Book, mappedBy: 'author' }) | ||
books = new Collection<Book>(this); | ||
|
||
// same as previous definition | ||
@OneToMany({ entity: () => Book, mappedBy: 'author', cascade: [Cascade.PERSIST, Cascade.MERGE] }) | ||
books = new Collection<Book>(this); | ||
|
||
// only cascade remove | ||
@OneToMany({ entity: () => Book, mappedBy: 'author', cascade: [Cascade.REMOVE] }) | ||
books = new Collection<Book>(this); | ||
|
||
// cascade persist and remove (but not merge) | ||
@OneToMany({ entity: () => Book, mappedBy: 'author', cascade: [Cascade.PERSIST, Cascade.REMOVE] }) | ||
books = new Collection<Book>(this); | ||
|
||
// no cascade | ||
@OneToMany({ entity: () => Book, mappedBy: 'author', cascade: [] }) | ||
books = new Collection<Book>(this); | ||
|
||
// cascade all (persist, merge and remove) | ||
@OneToMany({ entity: () => Book, mappedBy: 'author', cascade: [Cascade.ALL] }) | ||
books = new Collection<Book>(this); | ||
|
||
// same as previous definition | ||
@OneToMany({ entity: () => Book, mappedBy: 'author', cascade: [Cascade.PERSIST, Cascade.MERGE, Cascade.REMOVE] }) | ||
books = new Collection<Book>(this); | ||
``` | ||
|
||
## Cascade persist | ||
|
||
Here is example of how cascade persist works: | ||
|
||
```typescript | ||
const book = await orm.em.findOne(Book, 'id', ['author', 'tags']); | ||
book.author.name = 'Foo Bar'; | ||
book.tags[0].name = 'new name 1'; | ||
book.tags[1].name = 'new name 2'; | ||
await orm.em.persistAndFlush(book); // all book tags and author will be persisted too | ||
``` | ||
|
||
> When cascade persisting collections, keep in mind only fully initialized collections | ||
> will be cascade persisted. | ||
## Cascade merge | ||
|
||
When you want to merge entity and all its associations, you can use `Cascade.MERGE`. This | ||
comes handy when you want to clear identity map (e.g. when importing large number of entities), | ||
but you also have to keep your parent entities managed (because otherwise they would be considered | ||
as new entities and insert-persisted, which would fail with non-unique identifier). | ||
|
||
In following example, without having `Author.favouriteBook` set to cascade merge, you would | ||
get an error because it would be cascade-inserted with already taken ID. | ||
|
||
```typescript | ||
const a1 = new Author(...); | ||
a1.favouriteBook = new Book('the best', ...); | ||
await orm.em.persistAndFlush(a1); // cascade persists favourite book as well | ||
|
||
for (let i = 1; i < 1000; i++) { | ||
const book = new Book('...', a1); | ||
orm.em.persist(book); | ||
|
||
// persist every 100 records | ||
if (i % 100 === 0) { | ||
await orm.em.flush(); | ||
orm.em.clear(); // this makes both a1 and his favourite book detached | ||
orm.em.merge(a1); // so we need to merge them to prevent cascade-inserts | ||
|
||
// without cascade merge, you would need to manually merge all his associations | ||
orm.em.merge(a1.favouriteBook); // not needed with Cascade.MERGE | ||
} | ||
} | ||
|
||
await orm.em.flush(); | ||
``` | ||
|
||
## Cascade remove | ||
|
||
Cascade remove works same way as cascade persist, just for removing entities. Following | ||
example assumes that `Book.publisher` is set to `Cascade.REMOVE`: | ||
|
||
> Note that cascade remove for collections can be inefficient as it will fire 1 query | ||
> for each entity in collection. | ||
```typescript | ||
await orm.em.removeEntity(book); // this will also remove book.publisher | ||
``` | ||
|
||
Keep in mind that cascade remove **can be dangerous** when used on `@ManyToOne` fields, | ||
as cascade removed entity can stay referenced in another entities that were not removed. | ||
|
||
```typescript | ||
const publisher = new Publisher(...); | ||
// all books with same publisher | ||
book1.publisher = book2.publisher = book3.publisher = publisher; | ||
await orm.em.removeEntity(book1); // this will remove book1 and its publisher | ||
|
||
// but we still have reference to removed publisher here | ||
console.log(book2.publisher, book3.publisher); | ||
``` | ||
|
||
## Orphan removal | ||
|
||
In addition to `Cascade.REMOVE`, there is also additional and more aggressive remove | ||
cascading mode which can be specified using the `orphanRemoval` flag of the `@OneToOne` | ||
and `@OneToMany` properties: | ||
|
||
```typescript | ||
@Entity() | ||
export class Author { | ||
|
||
@OneToMany({ entity: () => Book, mappedBy: 'author', orphanRemoval: true }) | ||
books = new Collection<Book>(this); | ||
|
||
} | ||
``` | ||
|
||
> `orphanRemoval` flag behaves just like `Cascade.REMOVE` for remove operation, so specifying | ||
> both is redundant. | ||
With simple `Cascade.REMOVE`, you would need to remove the `Author` entity to cascade | ||
the operation down to all loaded `Book`s. By enabling orphan removal on the collection, | ||
`Book`s will be also removed when they get disconnected from the collection (either via | ||
`remove()`, or by replacing collection items via `set()`): | ||
|
||
```typescript | ||
await author.books.set([book1, book2]); // replace whole collection | ||
await author.books.remove(book1); // remove book from collection | ||
await orm.em.persistAndFlush(author); // book1 will be removed, as well as all original items (before we called `set()`) | ||
``` | ||
|
||
In this example, no `Book` would be removed with simple `Cascade.REMOVE` as no remove operation | ||
was executed. | ||
|
||
## Declarative Referential Integrity | ||
|
||
> This is only supported in SQL drivers. | ||
As opposed to the application level cascading controlled by the `cascade` option, we can | ||
also define database level referential integrity actions: `on update` and `on delete`. | ||
|
||
Their values are automatically inferred from the `cascade` option value. You can also | ||
control the value manually via `onUpdateIntegrity` and `onDelete` options. | ||
|
||
```typescript | ||
@Entity() | ||
export class Book { | ||
|
||
@ManyToOne({ onUpdateIntegrity: 'set null', onDelete: 'cascade' }) | ||
author?: Author; | ||
|
||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
--- | ||
title: Collections | ||
--- | ||
|
||
`OneToMany` and `ManyToMany` collections are stored in a `Collection` wrapper. It implements | ||
iterator so you can use `for of` loop to iterate through it. | ||
|
||
Another way to access collection items is to use bracket syntax like when you access array items. | ||
Keep in mind that this approach will not check if the collection is initialed, while using `get` | ||
method will throw error in this case. | ||
|
||
> Note that array access in `Collection` is available only for reading already loaded items, you | ||
> cannot add new items to `Collection` this way. | ||
```typescript | ||
const author = orm.em.findOne(Author, '...', ['books']); // populating books collection | ||
|
||
// or we could lazy load books collection later via `init()` method | ||
await author.books.init(); | ||
|
||
for (const book of author.books) { | ||
console.log(book.title); // initialized | ||
console.log(book.author.isInitialized()); // true | ||
console.log(book.author.id); | ||
console.log(book.author.name); // Jon Snow | ||
console.log(book.publisher); // just reference | ||
console.log(book.publisher.isInitialized()); // false | ||
console.log(book.publisher.id); | ||
console.log(book.publisher.name); // undefined | ||
} | ||
|
||
// collection needs to be initialized before you can work with it | ||
author.books.add(book); | ||
console.log(author.books.contains(book)); // true | ||
author.books.remove(book); | ||
console.log(author.books.contains(book)); // false | ||
author.books.add(book); | ||
console.log(author.books.count()); // 1 | ||
author.books.removeAll(); | ||
console.log(author.books.contains(book)); // false | ||
console.log(author.books.count()); // 0 | ||
console.log(author.books.getItems()); // Book[] | ||
console.log(author.books.getIdentifiers()); // array of string | number | ||
console.log(author.books.getIdentifiers('_id')); // array of ObjectId | ||
|
||
// array access works as well | ||
console.log(author.books[1]); // Book | ||
console.log(author.books[12345]); // undefined, even if the collection is not initialized | ||
|
||
const author = orm.em.findOne(Author, '...'); // books collection has not been populated | ||
console.log(author.books.getItems()); // throws because the collection has not been initialized | ||
// initialize collection if not already loaded and return its items as array | ||
console.log(await author.books.loadItems()); // Book[] | ||
``` | ||
|
||
## OneToMany Collections | ||
|
||
`OneToMany` collections are inverse side of `ManyToOne` references, to which they need to point via `fk` attribute: | ||
|
||
```typescript | ||
@Entity() | ||
export class Book { | ||
|
||
@PrimaryKey() | ||
_id!: ObjectId; | ||
|
||
@ManyToOne() | ||
author!: Author; | ||
|
||
} | ||
|
||
@Entity() | ||
export class Author { | ||
|
||
@PrimaryKey() | ||
_id!: ObjectId; | ||
|
||
@OneToMany(() => Book, book => book.author) | ||
books1 = new Collection<Book>(this); | ||
|
||
// or via options object | ||
@OneToMany({ entity: () => Book, mappedBy: 'author' }) | ||
books2 = new Collection<Book>(this); | ||
|
||
} | ||
``` | ||
|
||
## ManyToMany Collections | ||
|
||
For ManyToMany, SQL drivers use pivot table that holds reference to both entities. | ||
|
||
As opposed to them, with MongoDB we do not need to have join tables for `ManyToMany` | ||
relations. All references are stored as an array of `ObjectId`s on owning entity. | ||
|
||
### Unidirectional | ||
|
||
Unidirectional `ManyToMany` relations are defined only on one side, if you define only `entity` | ||
attribute, then it will be considered the owning side: | ||
|
||
```typescript | ||
@ManyToMany(() => Book) | ||
books1 = new Collection<Book>(this); | ||
|
||
// or mark it as owner explicitly via options object | ||
@ManyToMany({ entity: () => Book, owner: true }) | ||
books2 = new Collection<Book>(this); | ||
``` | ||
|
||
### Bidirectional | ||
|
||
Bidirectional `ManyToMany` relations are defined on both sides, while one is owning side (where references are store), | ||
marked by `inversedBy` attribute pointing to the inverse side: | ||
|
||
```typescript | ||
@ManyToMany(() => BookTag, tag => tag.books, { owner: true }) | ||
tags = new Collection<BookTag>(this); | ||
|
||
// or via options object | ||
@ManyToMany({ entity: () => BookTag, inversedBy: 'books' }) | ||
tags = new Collection<BookTag>(this); | ||
``` | ||
|
||
And on the inversed side we define it with `mappedBy` attribute pointing back to the owner: | ||
|
||
```typescript | ||
@ManyToMany(() => Book, book => book.tags) | ||
books = new Collection<Book>(this); | ||
|
||
// or via options object | ||
@ManyToMany({ entity: () => Book, mappedBy: 'tags' }) | ||
books = new Collection<Book>(this); | ||
``` | ||
|
||
### Forcing fixed order of collection items | ||
|
||
> Since v3 many to many collections does not require having auto increment primary key, that | ||
> was used to ensure fixed order of collection items. | ||
To preserve fixed order of collections, you can use `fixedOrder: true` attribute, which will | ||
start ordering by `id` column. Schema generator will convert the pivot table to have auto increment | ||
primary key `id`. You can also change the order column name via `fixedOrderColumn: 'order'`. | ||
|
||
You can also specify default ordering via `orderBy: { ... }` attribute. This will be used when | ||
you fully populate the collection including its items, as it orders by the referenced entity | ||
properties instead of pivot table columns (which `fixedOrderColumn` is). On the other hand, | ||
`fixedOrder` is used to maintain the insert order of items instead of ordering by some property. | ||
|
||
## Propagation of Collection's add() and remove() operations | ||
|
||
When you use one of `Collection.add()` method, the item is added to given collection, | ||
and this action is also propagated to its counterpart. | ||
|
||
```typescript | ||
// one to many | ||
const author = new Author(...); | ||
const book = new Book(...); | ||
|
||
author.books.add(book); | ||
console.log(book.author); // author will be set thanks to the propagation | ||
``` | ||
|
||
For M:N this works in both ways, either from owning side, or from inverse side. | ||
|
||
```typescript | ||
// many to many works both from owning side and from inverse side | ||
const book = new Book(...); | ||
const tag = new BookTag(...); | ||
|
||
book.tags.add(tag); | ||
console.log(tag.books.contains(book)); // true | ||
|
||
tag.books.add(book); | ||
console.log(book.tags.contains(tag)); // true | ||
``` | ||
|
||
> Collections on both sides have to be initialized, otherwise propagation won't work. | ||
> Although this propagation works also for M:N inverse side, you should always use owning | ||
> side to manipulate the collection. | ||
Same applies for `Collection.remove()`. | ||
|
||
## Filtering and ordering of collection items | ||
|
||
When initializing collection items via `collection.init()`, you can filter the collection | ||
as well as order its items: | ||
|
||
```typescript | ||
await book.tags.init({ where: { active: true }, orderBy: { name: QueryOrder.DESC } }); | ||
``` | ||
|
||
> You should never modify partially loaded collection. |
Oops, something went wrong.