Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy relation promise persistence #2902

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/metadata/EntityMetadata.ts
Expand Up @@ -664,16 +664,20 @@ export class EntityMetadata {
* Iterates through entity and finds and extracts all values from relations in the entity.
* If relation value is an array its being flattened.
*/
extractRelationValuesFromEntity(entity: ObjectLiteral, relations: RelationMetadata[]): [RelationMetadata, any, EntityMetadata][] {
async extractRelationValuesFromEntity(entity: ObjectLiteral, relations: RelationMetadata[]): Promise<[RelationMetadata, any, EntityMetadata][]> {
const relationsAndValues: [RelationMetadata, any, EntityMetadata][] = [];
relations.forEach(relation => {
const value = relation.getEntityValue(entity);
await Promise.all(relations.map(async relation => {
let value = relation.getEntityValue(entity);
if (value instanceof Promise) {
// Promise will only be returned if it is user-supplied (ie. value is dirty)
value = await value;
}
if (value instanceof Array) {
value.forEach(subValue => relationsAndValues.push([relation, subValue, relation.inverseEntityMetadata]));
} else if (value) {
relationsAndValues.push([relation, value, relation.inverseEntityMetadata]);
}
});
}));
return relationsAndValues;
}

Expand Down Expand Up @@ -830,4 +834,4 @@ export class EntityMetadata {
return this.database && !(this.connection.driver instanceof PostgresDriver) ? this.database + "." + this.schema : this.schema;
}

}
}
8 changes: 4 additions & 4 deletions src/metadata/RelationMetadata.ts
Expand Up @@ -348,7 +348,7 @@ export class RelationMetadata {
if (embeddedObject["__" + this.propertyName + "__"] !== undefined)
return embeddedObject["__" + this.propertyName + "__"];

if (getLazyRelationsPromiseValue === true)
if (entity["__dirty_" + this.propertyName + "__"] || getLazyRelationsPromiseValue === true)
return embeddedObject[this.propertyName];

return undefined;
Expand All @@ -360,7 +360,7 @@ export class RelationMetadata {
if (entity["__" + this.propertyName + "__"] !== undefined)
return entity["__" + this.propertyName + "__"];

if (getLazyRelationsPromiseValue === true)
if (entity["__dirty_" + this.propertyName + "__"] || getLazyRelationsPromiseValue === true)
return entity[this.propertyName];

return undefined;
Expand All @@ -376,7 +376,7 @@ export class RelationMetadata {
* If merge is set to true, it merges given value into currently
*/
setEntityValue(entity: ObjectLiteral, value: any): void {
const propertyName = this.isLazy ? "__" + this.propertyName + "__" : this.propertyName;
const propertyName = (this.isLazy && !(value instanceof Promise)) ? "__" + this.propertyName + "__" : this.propertyName;

if (this.embeddedMetadata) {

Expand Down Expand Up @@ -515,4 +515,4 @@ export class RelationMetadata {
return this.embeddedMetadata.parentPropertyNames.join(".") + "." + this.propertyName;
}

}
}
8 changes: 4 additions & 4 deletions src/persistence/EntityPersistExecutor.ts
Expand Up @@ -85,11 +85,11 @@ export class EntityPersistExecutor {
// console.time("building cascades...");
// go through each entity with metadata and create subjects and subjects by cascades for them
const cascadesSubjectBuilder = new CascadesSubjectBuilder(subjects);
subjects.forEach(subject => {
await Promise.all(subjects.map(async subject => {
// next step we build list of subjects we will operate with
// these subjects are subjects that we need to insert or update alongside with main persisted entity
cascadesSubjectBuilder.build(subject);
});
await cascadesSubjectBuilder.build(subject);
}));
// console.timeEnd("building cascades...");

// load database entities for all subjects we have
Expand Down Expand Up @@ -171,4 +171,4 @@ export class EntityPersistExecutor {
});
}

}
}
14 changes: 7 additions & 7 deletions src/persistence/subject-builder/CascadesSubjectBuilder.ts
Expand Up @@ -21,11 +21,11 @@ export class CascadesSubjectBuilder {
/**
* Builds a cascade subjects tree and pushes them in into the given array of subjects.
*/
build(subject: Subject) {
async build(subject: Subject) {

subject.metadata
.extractRelationValuesFromEntity(subject.entity!, subject.metadata.relations) // todo: we can create EntityMetadata.cascadeRelations
.forEach(([relation, relationEntity, relationEntityMetadata]) => {
const relationValues = await subject.metadata
.extractRelationValuesFromEntity(subject.entity!, subject.metadata.relations); // todo: we can create EntityMetadata.cascadeRelations
await Promise.all(relationValues.map(async ([relation, relationEntity, relationEntityMetadata]) => {

// we need only defined values and insert or update cascades of the relation should be set
if (relationEntity === undefined ||
Expand Down Expand Up @@ -60,8 +60,8 @@ export class CascadesSubjectBuilder {
this.allSubjects.push(relationEntitySubject);

// go recursively and find other entities we need to insert/update
this.build(relationEntitySubject);
});
await this.build(relationEntitySubject);
}));
}

// ---------------------------------------------------------------------
Expand All @@ -84,4 +84,4 @@ export class CascadesSubjectBuilder {
});
}

}
}
10 changes: 7 additions & 3 deletions src/query-builder/RelationLoader.ts
Expand Up @@ -188,6 +188,7 @@ export class RelationLoader {
const dataIndex = "__" + relation.propertyName + "__"; // in what property of the entity loaded data will be stored
const promiseIndex = "__promise_" + relation.propertyName + "__"; // in what property of the entity loading promise will be stored
const resolveIndex = "__has_" + relation.propertyName + "__"; // indicates if relation data already was loaded or not, we need this flag if loaded data is empty
const dirtyIndex = "__dirty_" + relation.propertyName + "__"; // indicates if relation Promise is "dirty" - ie a user-supplied promise set via the property setter

Object.defineProperty(entity, relation.propertyName, {
get: function() {
Expand All @@ -209,11 +210,14 @@ export class RelationLoader {
},
set: function(value: any|Promise<any>) {
if (value instanceof Promise) { // if set data is a promise then wait for its resolve and save in the object
value.then(result => {
this[dirtyIndex] = true;
this[promiseIndex] = value.then(result => {
this[dataIndex] = result;
this[resolveIndex] = true;
delete this[promiseIndex];
delete this[dirtyIndex];
return this[dataIndex];
});

} else { // if its direct data set (non promise, probably not safe-typed)
this[dataIndex] = value;
this[resolveIndex] = true;
Expand All @@ -223,4 +227,4 @@ export class RelationLoader {
});
}

}
}
Expand Up @@ -94,16 +94,17 @@ export class PlainObjectToDatabaseEntityTransformer {

// create a special load map that will hold all metadata that will be used to operate with entities easily
const loadMap = new LoadMap();
const fillLoadMap = (entity: ObjectLiteral, entityMetadata: EntityMetadata, parentLoadMapItem?: LoadMapItem, relation?: RelationMetadata) => {
const fillLoadMap = async (entity: ObjectLiteral, entityMetadata: EntityMetadata, parentLoadMapItem?: LoadMapItem, relation?: RelationMetadata) => {
const item = new LoadMapItem(entity, entityMetadata, parentLoadMapItem, relation);
loadMap.addLoadMap(item);

entityMetadata
.extractRelationValuesFromEntity(entity, metadata.relations)
const relationValues = await entityMetadata
.extractRelationValuesFromEntity(entity, metadata.relations);
await Promise.all(relationValues
.filter(value => value !== null && value !== undefined)
.forEach(([relation, value, inverseEntityMetadata]) => fillLoadMap(value, inverseEntityMetadata, item, relation));
.map(async ([relation, value, inverseEntityMetadata]) => await fillLoadMap(value, inverseEntityMetadata, item, relation)));
};
fillLoadMap(plainObject, metadata);
await fillLoadMap(plainObject, metadata);
// load all entities and store them in the load map
await Promise.all(loadMap.groupByTargetIds().map(targetWithIds => { // todo: fix type hinting
return this.manager
Expand Down Expand Up @@ -132,4 +133,4 @@ export class PlainObjectToDatabaseEntityTransformer {
return loadMap.mainLoadMapItem ? loadMap.mainLoadMapItem.entity : undefined;
}

}
}
94 changes: 55 additions & 39 deletions src/query-builder/transformer/PlainObjectToNewEntityTransformer.ts
@@ -1,5 +1,6 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {RelationMetadata} from "../../metadata/RelationMetadata";

/**
* Transforms plain old javascript object
Expand Down Expand Up @@ -48,52 +49,67 @@ export class PlainObjectToNewEntityTransformer {
if (objectRelatedValue === undefined)
return;

if (relation.isOneToMany || relation.isManyToMany) {
if (!(objectRelatedValue instanceof Array))
return;

if (!entityRelatedValue) {
entityRelatedValue = [];
relation.setEntityValue(entity, entityRelatedValue);
}
if (relation.isLazy && objectRelatedValue instanceof Promise) {
// Set lazy entity value to a Promise which resolves to the transformed value
relation.setEntityValue(
entity,
objectRelatedValue.then(value => this.transformRelatedValue(entityRelatedValue, value, relation, getLazyRelationsPromiseValue))
);
} else {
relation.setEntityValue(entity, this.transformRelatedValue(entityRelatedValue, objectRelatedValue, relation, getLazyRelationsPromiseValue));
}
});
}
}

objectRelatedValue.forEach(objectRelatedValueItem => {
/**
* Transform and return an Entity for the provided relation value
*/
private transformRelatedValue(entityRelatedValue: ObjectLiteral, objectRelatedValue: ObjectLiteral, relation: RelationMetadata, getLazyRelationsPromiseValue: boolean) {

// check if we have this item from the merging object in the original entity we merge into
let objectRelatedValueEntity = (entityRelatedValue as any[]).find(entityRelatedValueItem => {
return relation.inverseEntityMetadata.compareEntities(objectRelatedValueItem, entityRelatedValueItem);
});
if (relation.isOneToMany || relation.isManyToMany) {
if (!(objectRelatedValue instanceof Array))
return;

// if such item already exist then merge new data into it, if its not we create a new entity and merge it into the array
if (!objectRelatedValueEntity) {
objectRelatedValueEntity = relation.inverseEntityMetadata.create();
entityRelatedValue.push(objectRelatedValueEntity);
}
if (!entityRelatedValue) {
entityRelatedValue = [];
}

this.groupAndTransform(objectRelatedValueEntity, objectRelatedValueItem, relation.inverseEntityMetadata, getLazyRelationsPromiseValue);
});
objectRelatedValue.forEach(objectRelatedValueItem => {

} else {
// check if we have this item from the merging object in the original entity we merge into
let objectRelatedValueEntity = (entityRelatedValue as any[]).find(entityRelatedValueItem => {
return relation.inverseEntityMetadata.compareEntities(objectRelatedValueItem, entityRelatedValueItem);
});

// if related object isn't an object (direct relation id for example)
// we just set it to the entity relation, we don't need anything more from it
// however we do it only if original entity does not have this relation set to object
// to prevent full overriding of objects
if (!(objectRelatedValue instanceof Object)) {
if (!(entityRelatedValue instanceof Object))
relation.setEntityValue(entity, objectRelatedValue);
return;
}

if (!entityRelatedValue) {
entityRelatedValue = relation.inverseEntityMetadata.create();
relation.setEntityValue(entity, entityRelatedValue);
}

this.groupAndTransform(entityRelatedValue, objectRelatedValue, relation.inverseEntityMetadata, getLazyRelationsPromiseValue);
// if such item already exist then merge new data into it, if its not we create a new entity and merge it into the array
if (!objectRelatedValueEntity) {
objectRelatedValueEntity = relation.inverseEntityMetadata.create();
entityRelatedValue.push(objectRelatedValueEntity);
}

this.groupAndTransform(objectRelatedValueEntity, objectRelatedValueItem, relation.inverseEntityMetadata, getLazyRelationsPromiseValue);
});

} else {

// if related object isn't an object (direct relation id for example)
// we just set it to the entity relation, we don't need anything more from it
// however we do it only if original entity does not have this relation set to object
// to prevent full overriding of objects
if (!(objectRelatedValue instanceof Object)) {
if (!(entityRelatedValue instanceof Object))
entityRelatedValue = objectRelatedValue;
return;
}

if (!entityRelatedValue) {
entityRelatedValue = relation.inverseEntityMetadata.create();
}

this.groupAndTransform(entityRelatedValue, objectRelatedValue, relation.inverseEntityMetadata, getLazyRelationsPromiseValue);
}
}

}
return entityRelatedValue;
}
}
15 changes: 15 additions & 0 deletions test/github-issues/2729/entity/Bar.ts
@@ -0,0 +1,15 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "../../../../src";
import { Foo } from "./Foo";

@Entity()
export class Bar {

@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@OneToMany(() => Foo, foo => foo.bar, { lazy: true })
foos: Promise<Foo[]>;
}
14 changes: 14 additions & 0 deletions test/github-issues/2729/entity/Baz.ts
@@ -0,0 +1,14 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "../../../../src";
import { Foo } from "./Foo";

@Entity()
export class Baz {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@OneToMany(() => Foo, foo => foo.baz, { lazy: true })
foos: Promise<Foo[]>;
}
18 changes: 18 additions & 0 deletions test/github-issues/2729/entity/Foo.ts
@@ -0,0 +1,18 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "../../../../src";
import { Bar } from "./Bar";
import { Baz } from "./Baz";

@Entity()
export class Foo {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@ManyToOne(() => Bar, bar => bar.foos, { lazy: true })
bar: Promise<Bar>;

@ManyToOne(() => Baz, baz => baz.foos, { lazy: true })
baz: Promise<Baz>;
}