Skip to content

Commit

Permalink
Merge 8a0684e into 2d9356a
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Mar 27, 2019
2 parents 2d9356a + 8a0684e commit 3a90d8d
Show file tree
Hide file tree
Showing 16 changed files with 82 additions and 31 deletions.
49 changes: 45 additions & 4 deletions docs/cascading.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
---

# Cascading persist and remove
# Cascading persist, merge and remove

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
Expand All @@ -13,25 +13,33 @@ You can control this behaviour via `cascade` attribute of `@ManyToOne`, `@ManyTo
> New entities without primary key will be always persisted, regardless of `cascade` value.
```typescript
// cascade persist is default value
// cascade persist & merge is default value
@OneToMany({ entity: () => Book, fk: 'author' })
books = new Collection<Book>(this);

// same as previous definition
@OneToMany({ entity: () => Book, fk: 'author', cascade: [Cascade.PERSIST] })
@OneToMany({ entity: () => Book, fk: 'author', cascade: [Cascade.PERSIST, Cascade.MERGE] })
books = new Collection<Book>(this);

// only cascade remove
@OneToMany({ entity: () => Book, fk: 'author', cascade: [Cascade.REMOVE] })
books = new Collection<Book>(this);

// cascade persist and remove
// cascade persist and remove (but not merge)
@OneToMany({ entity: () => Book, fk: 'author', cascade: [Cascade.PERSIST, Cascade.REMOVE] })
books = new Collection<Book>(this);

// no cascade
@OneToMany({ entity: () => Book, fk: 'author', cascade: [] })
books = new Collection<Book>(this);

// cascade all (persist, merge and remove)
@OneToMany({ entity: () => Book, fk: 'author', cascade: [Cascade.ALL] })
books = new Collection<Book>(this);

// same as previous definition
@OneToMany({ entity: () => Book, fk: 'author', cascade: [Cascade.PERSIST, Cascade.MERGE, Cascade.REMOVE] })
books = new Collection<Book>(this);
```

## Cascade persist
Expand All @@ -49,6 +57,39 @@ await orm.em.persist(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.persistLater(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
Expand Down
20 changes: 14 additions & 6 deletions lib/EntityManager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Configuration, RequestContext, Utils } from './utils';
import { EntityRepository, EntityAssigner, EntityFactory, EntityLoader, EntityValidator, ReferenceType } from './entity';
import { UnitOfWork } from './unit-of-work';
import { FilterQuery, IDatabaseDriver } from './drivers/IDatabaseDriver';
import { FilterQuery, IDatabaseDriver } from './drivers';
import { EntityData, EntityName, IEntity, IEntityType, IPrimaryKey } from './decorators';
import { QueryBuilder, QueryOrder, SmartQueryHelper } from './query';
import { MetadataStorage } from './metadata';
import { Connection } from './connections/Connection';
import { Connection } from './connections';

export class EntityManager {

Expand Down Expand Up @@ -158,7 +158,13 @@ export class EntityManager {
return this.driver.aggregate(entityName, pipeline);
}

merge<T extends IEntityType<T>>(entityName: EntityName<T>, data: EntityData<T>): T {
merge<T extends IEntityType<T>>(entity: T): T;
merge<T extends IEntityType<T>>(entityName: EntityName<T>, data: EntityData<T>): T;
merge<T extends IEntityType<T>>(entityName: EntityName<T> | T, data?: EntityData<T>): T {
if (Utils.isEntity(entityName)) {
return this.merge(entityName.constructor.name, entityName as EntityData<T>);
}

entityName = Utils.className(entityName);
const meta = this.metadata[entityName];

Expand All @@ -167,9 +173,11 @@ export class EntityManager {
}

const entity = Utils.isEntity<T>(data) ? data : this.getEntityFactory().create<T>(entityName, data, true);
this.getUnitOfWork().addToIdentityMap(entity); // add to IM immediately - needed for self-references that can be part of `data`

// add to IM immediately - needed for self-references that can be part of `data` (and do not trigger cascade merge)
this.getUnitOfWork().merge(entity, [entity]);
EntityAssigner.assign(entity, data, true);
this.getUnitOfWork().addToIdentityMap(entity); // add to IM again so we have correct payload saved to change set computation
this.getUnitOfWork().merge(entity); // add to IM again so we have correct payload saved to change set computation

return entity;
}
Expand All @@ -192,7 +200,7 @@ export class EntityManager {
}

const entity = this.getEntityFactory().createReference<T>(entityName, id);
this.getUnitOfWork().addToIdentityMap(entity);
this.getUnitOfWork().merge(entity);

return entity;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/MikroORM.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EntityManager } from './EntityManager';
import { IDatabaseDriver } from './drivers/IDatabaseDriver';
import { IDatabaseDriver } from './drivers';
import { MetadataDiscovery } from './metadata';
import { Configuration, Logger, Options } from './utils';
import { EntityMetadata } from './decorators';
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/ManyToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function ManyToMany(options: ManyToManyOptions): Function {
throw new Error(`'@ManyToMany({ entity: string | Function })' is required in '${target.constructor.name}.${propertyName}'`);
}

const property = { name: propertyName, reference: ReferenceType.MANY_TO_MANY, owner: !!options.inversedBy, cascade: [Cascade.PERSIST] };
const property = { name: propertyName, reference: ReferenceType.MANY_TO_MANY, owner: !!options.inversedBy, cascade: [Cascade.PERSIST, Cascade.MERGE] };
meta.properties[propertyName] = Object.assign(property, options) as EntityProperty;
};
}
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/ManyToOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function ManyToOne(options: ManyToOneOptions = {}): Function {
return function (target: IEntity, propertyName: string) {
const meta = MetadataStorage.getMetadata(target.constructor.name);
Utils.lookupPathFromDecorator(meta);
const property = { name: propertyName, reference: ReferenceType.MANY_TO_ONE, cascade: [Cascade.PERSIST] };
const property = { name: propertyName, reference: ReferenceType.MANY_TO_ONE, cascade: [Cascade.PERSIST, Cascade.MERGE] };
meta.properties[propertyName] = Object.assign(property, options) as EntityProperty;
};
}
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/OneToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function OneToMany(options: OneToManyOptions): Function {
throw new Error(`'@OneToMany({ entity: string | Function })' is required in '${target.constructor.name}.${propertyName}'`);
}

const property = { name: propertyName, reference: ReferenceType.ONE_TO_MANY, cascade: [Cascade.PERSIST] };
const property = { name: propertyName, reference: ReferenceType.ONE_TO_MANY, cascade: [Cascade.PERSIST, Cascade.MERGE] };
meta.properties[propertyName] = Object.assign(property, options) as EntityProperty;
};
}
Expand Down
2 changes: 1 addition & 1 deletion lib/drivers/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EntityData, IEntityType, IPrimaryKey } from '../decorators';
import { DatabaseDriver } from './DatabaseDriver';
import { Connection, QueryResult } from '../connections/Connection';
import { Connection, QueryResult } from '../connections';
import { ReferenceType } from '../entity';
import { FilterQuery } from './IDatabaseDriver';
import { QueryBuilder, QueryOrder } from '../query';
Expand Down
2 changes: 1 addition & 1 deletion lib/drivers/PostgreSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AbstractSqlDriver } from './AbstractSqlDriver';
import { EntityData, IEntityType } from '../decorators';
import { QueryType } from '../query';
import { PostgreSqlPlatform } from '../platforms/PostgreSqlPlatform';
import { QueryResult } from '../connections/Connection';
import { QueryResult } from '../connections';

export class PostgreSqlDriver extends AbstractSqlDriver<PostgreSqlConnection> {

Expand Down
2 changes: 2 additions & 0 deletions lib/entity/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export enum ReferenceType {

export enum Cascade {
PERSIST = 'persist',
MERGE = 'merge',
REMOVE = 'remove',
ALL = 'all',
}
2 changes: 1 addition & 1 deletion lib/metadata/JavaScriptMetadataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class JavaScriptMetadataProvider extends MetadataProvider {
}

if (prop.reference !== ReferenceType.SCALAR && typeof prop.cascade === 'undefined') {
prop.cascade = [Cascade.PERSIST];
prop.cascade = [Cascade.PERSIST, Cascade.MERGE];
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/metadata/MetadataDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export class MetadataDiscovery {
}

private definePivotProperty(prop: EntityProperty, name: string): EntityProperty {
const ret = { name, type: name, reference: ReferenceType.MANY_TO_ONE, cascade: [Cascade.PERSIST, Cascade.REMOVE] } as EntityProperty;
const ret = { name, type: name, reference: ReferenceType.MANY_TO_ONE, cascade: [Cascade.ALL] } as EntityProperty;

if (name === prop.type) {
const meta = this.metadata[name];
Expand Down
5 changes: 3 additions & 2 deletions lib/schema/SchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ export class SchemaGenerator {
const meta2 = this.metadata[prop.type];
const pk2 = meta2.properties[meta2.primaryKey].fieldName;
let ret = `REFERENCES ${this.helper.quoteIdentifier(meta2.collection)} (${this.helper.quoteIdentifier(pk2)})`;
ret += ` ON DELETE ${prop.cascade.includes(Cascade.REMOVE) ? 'CASCADE' : 'SET NULL'}`;
const cascade = prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL);
ret += ` ON DELETE ${cascade ? 'CASCADE' : 'SET NULL'}`;

if (prop.cascade.includes(Cascade.PERSIST)) {
if (prop.cascade.includes(Cascade.PERSIST) || prop.cascade.includes(Cascade.ALL)) {
ret += ' ON UPDATE CASCADE';
}

Expand Down
2 changes: 1 addition & 1 deletion lib/unit-of-work/ChangeSetPersister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { EntityMetadata, EntityProperty, IEntityType } from '../decorators';
import { EntityIdentifier } from '../entity';
import { ChangeSet, ChangeSetType } from './ChangeSet';
import { IDatabaseDriver } from '..';
import { QueryResult } from '../connections/Connection';
import { QueryResult } from '../connections';

export class ChangeSetPersister {

Expand Down
15 changes: 7 additions & 8 deletions lib/unit-of-work/UnitOfWork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@ export class UnitOfWork {

constructor(private readonly em: EntityManager) { }

addToIdentityMap(entity: IEntity, initialized = true): void {
merge<T extends IEntityType<T>>(entity: T, visited: IEntity[] = []): void {
this.identityMap[`${entity.constructor.name}-${entity.__serializedPrimaryKey}`] = entity;

if (initialized) {
this.originalEntityData[entity.__uuid] = Utils.copy(entity);
}
this.originalEntityData[entity.__uuid] = Utils.copy(entity);
this.cascade(entity, Cascade.MERGE, visited);
}

getById<T extends IEntityType<T>>(entityName: string, id: IPrimaryKey): T {
Expand Down Expand Up @@ -179,7 +177,7 @@ export class UnitOfWork {
await this.changeSetPersister.persistToDatabase(changeSet);

if (changeSet.type !== ChangeSetType.DELETE) {
this.em.merge(changeSet.name, changeSet.entity);
this.em.merge(changeSet.entity);
}

await this.runHooks(`after${type}`, changeSet.entity);
Expand Down Expand Up @@ -234,7 +232,8 @@ export class UnitOfWork {
visited.push(entity);

switch (type) {
case Cascade.PERSIST: this.persist<T>(entity, visited); break;
case Cascade.PERSIST: this.persist(entity, visited); break;
case Cascade.MERGE: this.merge(entity, visited); break;
case Cascade.REMOVE: this.remove(entity, visited); break;
}

Expand All @@ -246,7 +245,7 @@ export class UnitOfWork {
}

private cascadeReference<T extends IEntityType<T>>(entity: T, prop: EntityProperty, type: Cascade, visited: IEntity[]): void {
if (!prop.cascade || !prop.cascade.includes(type)) {
if (!prop.cascade || !(prop.cascade.includes(type) || prop.cascade.includes(Cascade.ALL))) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion tests/EntityManager.mongo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ describe('EntityManagerMongo', () => {

test('should throw when trying to merge entity without id', async () => {
const author = new Author('test', 'test');
expect(() => orm.em.merge(Author, author)).toThrowError('You cannot merge entity without identifier!');
expect(() => orm.em.merge(author)).toThrowError('You cannot merge entity without identifier!');
});

test('fork', async () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/UnitOfWork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('UnitOfWork', () => {
test('changeSet is null for empty payload', async () => {
const author = new Author('test', 'test');
author.id = '00000001885f0a3cc37dc9f0';
uow.addToIdentityMap(author); // add entity to IM first
uow.merge(author); // add entity to IM first
const changeSet = await computer.computeChangeSet(author); // then try to persist it again
expect(changeSet).toBeNull();
expect(uow.getIdentityMap()).not.toEqual({});
Expand Down

0 comments on commit 3a90d8d

Please sign in to comment.