refactoring persistence

This commit is contained in:
Umed Khudoiberdiev 2017-10-27 22:47:59 +05:00
parent 1f5c6e0bf1
commit e14fce5524
22 changed files with 144 additions and 48 deletions

View File

@ -11,7 +11,7 @@ export interface RelationOptions {
* If set to true then it means that related object can be allowed to be inserted / updated / removed to the db.
* This is option a shortcut if you would like to set cascadeInsert, cascadeUpdate and cascadeRemove to true.
*/
cascadeAll?: boolean;
cascadeAll?: boolean; // todo: replace with cascade: boolean|("insert"|"update")[]
/**
* If set to true then it means that related object can be allowed to be inserted to the db.

View File

@ -17,6 +17,7 @@ import {DataTypeDefaults} from "../types/DataTypeDefaults";
import {TableColumn} from "../../schema-builder/schema/TableColumn";
import {PostgresConnectionCredentialsOptions} from "./PostgresConnectionCredentialsOptions";
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {OrmUtils} from "../../util/OrmUtils";
/**
* Organizes communication with PostgreSQL DBMS.
@ -560,7 +561,17 @@ export class PostgresDriver implements Driver {
* Creates generated map of values generated or returned by database after INSERT query.
*/
createGeneratedMap(metadata: EntityMetadata, insertResult: any) {
return insertResult[0];
if (!insertResult || !insertResult[0])
return undefined;
const result: ObjectLiteral = insertResult[0];
return Object.keys(result).reduce((map, key) => {
const column = metadata.findColumnWithDatabaseName(key);
if (column) {
OrmUtils.mergeDeep(map, column.createValueMap(result[key]));
}
return map;
}, {} as ObjectLiteral);
}
// -------------------------------------------------------------------------

View File

@ -17,6 +17,7 @@ import {MssqlParameter} from "./MssqlParameter";
import {TableColumn} from "../../schema-builder/schema/TableColumn";
import {SqlServerConnectionCredentialsOptions} from "./SqlServerConnectionCredentialsOptions";
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {OrmUtils} from "../../util/OrmUtils";
/**
* Organizes communication with SQL Server DBMS.
@ -468,8 +469,18 @@ export class SqlServerDriver implements Driver {
/**
* Creates generated map of values generated or returned by database after INSERT query.
*/
createGeneratedMap(metadata: EntityMetadata, insertionResult: any) {
return insertionResult[0];
createGeneratedMap(metadata: EntityMetadata, insertResult: any) {
if (!insertResult || !insertResult[0])
return undefined;
const result: ObjectLiteral = insertResult[0];
return Object.keys(result).reduce((map, key) => {
const column = metadata.findColumnWithDatabaseName(key);
if (column) {
OrmUtils.mergeDeep(map, column.createValueMap(result[key]));
}
return map;
}, {} as ObjectLiteral);
}
// -------------------------------------------------------------------------

View File

@ -326,6 +326,7 @@ export class EntityMetadataBuilder {
});
embeddedMetadata.embeddeds = this.createEmbeddedsRecursively(entityMetadata, this.metadataArgsStorage.filterEmbeddeds(targets));
embeddedMetadata.embeddeds.forEach(subEmbedded => subEmbedded.parentEmbeddedMetadata = embeddedMetadata);
entityMetadata.allEmbeddeds.push(embeddedMetadata);
return embeddedMetadata;
});
}

View File

@ -364,7 +364,7 @@ export class ColumnMetadata {
* Examples what this method can return depend if this column is in embeds.
* { id: 1 } or { title: "hello" }, { counters: { code: 1 } }, { data: { information: { counters: { code: 1 } } } }
*/
getEntityValueMap(entity: ObjectLiteral, returnIfEmpty: boolean = false): ObjectLiteral {
getEntityValueMap(entity: ObjectLiteral): ObjectLiteral|undefined {
// extract column value from embeds of entity if column is in embedded
if (this.embeddedMetadata) {
@ -393,16 +393,21 @@ export class ColumnMetadata {
};
const map: ObjectLiteral = {};
extractEmbeddedColumnValue(propertyNames, entity, map);
return map;
return Object.keys(map).length > 0 ? map : undefined;
} else { // no embeds - no problems. Simply return column property name and its value of the entity
if (this.relationMetadata && entity[this.propertyName] && entity[this.propertyName] instanceof Object) {
const map = this.relationMetadata.joinColumns.reduce((map, joinColumn) => {
return OrmUtils.mergeDeep(map, joinColumn.referencedColumn!.getEntityValueMap(entity[this.propertyName]));
const value = joinColumn.referencedColumn!.getEntityValueMap(entity[this.propertyName]);
if (!value) return map;
return OrmUtils.mergeDeep(map, value);
}, {});
return { [this.propertyName]: map };
return { [this.propertyName]: Object.keys(map).length > 0 ? map : undefined };
} else {
return { [this.propertyName]: entity[this.propertyName] };
if (entity[this.propertyName] !== undefined)
return { [this.propertyName]: entity[this.propertyName] };
return undefined;
}
}
}
@ -443,7 +448,7 @@ export class ColumnMetadata {
return undefined;
} else { // no embeds - no problems. Simply return column name by property name of the entity
if (this.relationMetadata && this.referencedColumn && this.isVirtual) { // todo: do we really need isVirtual?
if (this.relationMetadata && this.referencedColumn/* && this.isVirtual*/) { // todo: do we really need isVirtual?
const relatedEntity = this.relationMetadata.getEntityValue(entity);
if (relatedEntity && relatedEntity instanceof Object)
return this.referencedColumn.getEntityValue(relatedEntity);

View File

@ -159,7 +159,7 @@ export class EmbeddedMetadata {
this.prefix = this.buildPrefix(connection);
this.parentPropertyNames = this.buildParentPropertyNames();
this.parentPrefixes = this.buildParentPrefixes();
this.propertyPath = this.parentPrefixes.join(".");
this.propertyPath = this.parentPropertyNames.join(".");
this.embeddedMetadataTree = this.buildEmbeddedMetadataTree();
this.columnsFromTree = this.buildColumnsFromTree();
this.relationsFromTree = this.buildRelationsFromTree();

View File

@ -210,6 +210,11 @@ export class EntityMetadata {
*/
embeddeds: EmbeddedMetadata[] = [];
/**
* All embeddeds - embeddeds from this entity metadata and from all child embeddeds, etc.
*/
allEmbeddeds: EmbeddedMetadata[] = [];
/**
* Entity listener metadatas.
*/
@ -495,7 +500,7 @@ export class EntityMetadata {
* Finds embedded with a given property path.
*/
findEmbeddedWithPropertyPath(propertyPath: string): EmbeddedMetadata|undefined {
return this.embeddeds.find(embedded => {
return this.allEmbeddeds.find(embedded => {
return embedded.propertyPath === propertyPath;
});
}

View File

@ -18,7 +18,7 @@ export class EntityMetadataUtils {
// example: .update().set({ name: () => `SUBSTR('', 1, 2)` })
const parentPath = prefix ? prefix + "." + key : key;
if (metadata.hasEmbeddedWithPropertyPath(parentPath)) {
const subPaths = this.createPropertyPath(metadata, entity[key], key);
const subPaths = this.createPropertyPath(metadata, entity[key], parentPath);
paths.push(...subPaths);
} else {
const path = prefix ? prefix + "." + key : key;

View File

@ -289,6 +289,8 @@ export class RelationMetadata {
getRelationIdMap(entity: ObjectLiteral): ObjectLiteral|undefined {
const joinColumns = this.isOwning ? this.joinColumns : this.inverseRelation!.joinColumns;
const referencedColumns = joinColumns.map(joinColumn => joinColumn.referencedColumn!);
// console.log("entity", entity);
// console.log("referencedColumns", referencedColumns);
return this.inverseEntityMetadata.getValueMap(entity, referencedColumns);
}

View File

@ -129,16 +129,23 @@ export class Subject {
}
}
if (changeMap.column) {
// value = changeMap.valueFactory ? changeMap.valueFactory(value) : changeMap.column.createValueMap(value);
if (this.metadata.isJunction && changeMap.column) {
OrmUtils.mergeDeep(updateMap, changeMap.column.createValueMap(changeMap.column.referencedColumn!.getEntityValue(value)));
} else if (changeMap.column) {
OrmUtils.mergeDeep(updateMap, changeMap.column.createValueMap(value));
} else if (changeMap.relation) {
changeMap.relation!.joinColumns.forEach(column => {
OrmUtils.mergeDeep(updateMap, column.createValueMap(value));
});
OrmUtils.mergeDeep(updateMap, changeMap.relation!.createValueMap(value));
// changeMap.relation!.joinColumns.forEach(column => {
// OrmUtils.mergeDeep(updateMap, column.createValueMap(value));
// });
}
return updateMap;
}, {} as ObjectLiteral);
// console.log(changeSet);
this.changeMaps = changeMapsWithoutValues;
return changeSet;
}
@ -146,7 +153,7 @@ export class Subject {
buildIdentifier() {
return this.metadata.primaryColumns.reduce((identifier, column) => {
if (column.isGenerated && this.generatedMap) {
return OrmUtils.mergeDeep(identifier, column.createValueMap(this.generatedMap[column.databaseName]));
return OrmUtils.mergeDeep(identifier, column.getEntityValueMap(this.generatedMap));
} else {
return OrmUtils.mergeDeep(identifier, column.getEntityValueMap(this.entity!));
}

View File

@ -25,4 +25,9 @@ export interface SubjectChangeMap {
*/
value: Subject|any;
/**
* Callback used to produce a final value.
*/
valueFactory?: (value: any) => any;
}

View File

@ -113,10 +113,26 @@ export class SubjectExecutor {
newInsertedSubjects.push(...entityTargetSubjects);
entityTargetSubjects.forEach(entityTargetSubject => this.insertSubjects.splice(this.insertSubjects.indexOf(entityTargetSubject), 1));
});
const dependencies2: string[][] = [];
metadatas.forEach(metadata => {
metadata.relationsWithJoinColumns.forEach(relation => {
dependencies2.push([metadata.targetName, relation.inverseEntityMetadata.targetName]);
});
});
const sortedEntityTargets2 = OrmUtils.toposort(dependencies2).reverse();
const newInsertedSubjects2: Subject[] = [];
sortedEntityTargets2.forEach(sortedEntityTarget => {
const entityTargetSubjects = this.insertSubjects.filter(subject => subject.metadata.targetName === sortedEntityTarget);
newInsertedSubjects2.push(...entityTargetSubjects);
entityTargetSubjects.forEach(entityTargetSubject => this.insertSubjects.splice(this.insertSubjects.indexOf(entityTargetSubject), 1));
});
newInsertedSubjects.push(...newInsertedSubjects2);
newInsertedSubjects.push(...this.insertSubjects);
this.insertSubjects = newInsertedSubjects;
// console.log("dependencies", dependencies);
// console.log("toposort", );
}
await this.executeInsertOperations();
@ -203,6 +219,10 @@ export class SubjectExecutor {
return false;
}
// if (column.referencedColumn) {
//
// }
return true;
});
diffColumns.forEach(column => {
@ -281,6 +301,8 @@ export class SubjectExecutor {
*/
protected async executeInsertOperations(): Promise<void> {
// console.log(this.insertSubjects.map(subject => subject.entity));
// then we run insertion in the sequential order which is important since we have an ordered subjects
await PromiseUtils.runInSequence(this.insertSubjects, async subject => {
@ -308,7 +330,6 @@ export class SubjectExecutor {
// const valueSets = this.getValueSets();
// if (valueSets.length > 1)
// throw Error(`Returning / output can be used only when a single value / entity is inserted.`);
// const alias = this.expressionMap.mainAlias!.name;
// const returningResult = await this.createQueryBuilder()
// .select(this.expressionMap.returning as string[])
@ -323,12 +344,12 @@ export class SubjectExecutor {
if (subject.entity) {
subject.identifier = subject.buildIdentifier();
// console.log(subject.identifier);
}
// if there are changes left mark it for updation
if (subject.hasChanges()) {
subject.canBeUpdated = true;
// console.log("can be updated!", subject.mustBeUpdated);
}
});
}

View File

@ -116,9 +116,7 @@ export class ManyToManySubjectBuilder {
// extract only relation id from the related entities, since we only need it for comparision
// by example: extract from category only relation id (category id, or let's say category title, depend on join column options)
const relatedEntityRelationIdMap = relation.getRelationIdMap(relatedEntity);
console.log("relatedEntityRelationIdMap", relatedEntityRelationIdMap);
const relatedEntityRelationIdMap = relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity);
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
const relatedEntitySubject = this.subjects.find(subject => {
@ -131,10 +129,10 @@ export class ManyToManySubjectBuilder {
// if related entity does not have a subject then it means user tries to bind entity which wasn't saved
// in this persistence because he didn't pass this entity for save or he did not set cascades
// but without entity being inserted we cannot bind it in the relation operation, so we throw an exception here
if (!relatedEntitySubject || true === true)
throw new Error(`Many-to-many relation ${relation.entityMetadata.name}.${relation.propertyPath} contains ` +
if (!relatedEntitySubject)
throw new Error(`Many-to-many relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
`entities which do not exist in the database yet, thus they cannot be bind in the database. ` +
`Please setup cascade insertion or save entity before binding it.`);
`Please setup cascade insertion or save entities before binding it.`);
}
// try to find related entity in the database
@ -147,8 +145,8 @@ export class ManyToManySubjectBuilder {
if (relatedEntityExistInDatabase)
return;
const ownerEntityMap = relation.isOwning ? subject.entity! : relatedEntity; // by example: ownerEntityMap is post from subject here
const inverseEntityMap = relation.isOwning ? relatedEntity : subject.entity!; // by example: inverseEntityMap is category from categories array here
const ownerValue = relation.isOwning ? subject : (relatedEntitySubject || relatedEntity); // by example: ownerEntityMap is post from subject here
const inverseValue = relation.isOwning ? (relatedEntitySubject || relatedEntity) : subject; // by example: inverseEntityMap is category from categories array here
// create a new subject for insert operation of junction rows
const junctionSubject = new Subject(relation.junctionEntityMetadata!);
@ -158,21 +156,23 @@ export class ManyToManySubjectBuilder {
relation.junctionEntityMetadata!.ownerColumns.forEach(column => {
junctionSubject.changeMaps.push({
column: column,
value: column.referencedColumn!.getEntityValue(ownerEntityMap),
value: ownerValue,
// valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(ownerEntityMap),
});
});
relation.junctionEntityMetadata!.inverseColumns.forEach(column => {
junctionSubject.changeMaps.push({
column: column,
value: column.referencedColumn!.getEntityValue(inverseEntityMap),
value: inverseValue,
// valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(inverseEntityMap),
});
});
});
// get all inverse entities relation ids that are "bind" to the currently persisted entity
const changedInverseEntityRelationIds = relatedEntities
.map(relatedEntity => relation.getRelationIdMap(relatedEntity))
.map(relatedEntity => relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity))
.filter(relatedEntityRelationIdMap => relatedEntityRelationIdMap !== undefined && relatedEntityRelationIdMap !== null);
// now from all entities in the persisted entity find only those which aren't found in the db

View File

@ -76,7 +76,7 @@ export class OneToManySubjectBuilder {
// by example: extract from categories only relation ids (category id, or let's say category title, depend on join column options)
const relatedPersistedEntityRelationIds: ObjectLiteral[] = [];
relatedEntities.forEach(relatedEntity => { // by example: relatedEntity is a category here
const relationIdMap = relation.getRelationIdMap(relatedEntity); // by example: relationIdMap is category.id map here, e.g. { id: ... }
const relationIdMap = relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity); // by example: relationIdMap is category.id map here, e.g. { id: ... }
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
let relatedEntitySubject = this.subjects.find(subject => {
@ -93,9 +93,9 @@ export class OneToManySubjectBuilder {
// in this persistence because he didn't pass this entity for save or he did not set cascades
// but without entity being inserted we cannot bind it in the relation operation, so we throw an exception here
if (!relatedEntitySubject)
throw new Error(`One-to-many relation ${relation.entityMetadata.name}.${relation.propertyPath} contains ` +
throw new Error(`One-to-many relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
`entities which do not exist in the database yet, thus they cannot be bind in the database. ` +
`Please setup cascade insertion or save entity before binding it.`);
`Please setup cascade insertion or save entities before binding it.`);
// okay, so related subject exist and its marked for insertion, then add a new change map
// by example: this will tell category to insert into its post relation our post we are working with

View File

@ -94,7 +94,7 @@ export class OneToOneInverseSideSubjectBuilder {
// extract only relation id from the related entities, since we only need it for comparision
// by example: extract from category only relation id (category id, or let's say category title, depend on join column options)
const relationIdMap = relation.getRelationIdMap(relatedEntity); // by example: relationIdMap is category.id map here, e.g. { id: ... }
const relationIdMap = relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity); // by example: relationIdMap is category.id map here, e.g. { id: ... }
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
let relatedEntitySubject = this.subjects.find(operateSubject => {
@ -111,7 +111,7 @@ export class OneToOneInverseSideSubjectBuilder {
// in this persistence because he didn't pass this entity for save or he did not set cascades
// but without entity being inserted we cannot bind it in the relation operation, so we throw an exception here
if (!relatedEntitySubject)
throw new Error(`One-to-one inverse relation ${relation.entityMetadata.name}.${relation.propertyPath} contains ` +
throw new Error(`One-to-one inverse relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
`entity which does not exist in the database yet, thus cannot be bind in the database. ` +
`Please setup cascade insertion or save entity before binding it.`);

View File

@ -131,7 +131,12 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
values = valueSets.map((valueSet, insertionIndex) => {
const columnValues = columns.map(column => {
const paramName = "_inserted_" + insertionIndex + "_" + column.databaseName;
const value = this.connection.driver.preparePersistentValue(column.getEntityValue(valueSet), column);
let value = column.getEntityValue(valueSet);
if (column.referencedColumn && value instanceof Object) {
value = column.referencedColumn.getEntityValue(value);
}
value = this.connection.driver.preparePersistentValue(value, column);
if (value instanceof Function) { // support for SQL expressions in update query
return value();

View File

@ -563,7 +563,14 @@ export abstract class QueryBuilder<Entity> {
protected createReturningExpression(): string {
const columns = this.getReturningColumns();
if (columns.length) {
return columns.map(column => "INSERTED." + this.escape(column.databaseName)).join(", ");
return columns.map(column => {
const name = this.escape(column.databaseName);
if (this.connection.driver instanceof SqlServerDriver) {
return "INSERTED." + name;
} else {
return name;
}
}).join(", ");
} else if (typeof this.expressionMap.returning === "string") {
return this.expressionMap.returning;

View File

@ -147,11 +147,17 @@ export class UpdateQueryBuilder<Entity> extends QueryBuilder<Entity> implements
// todo: make this and other query builder to work with properly with tables without metadata
const column = metadata.findColumnWithPropertyPath(propertyPath);
// we update an entity and entity can contain property which aren't columns, so we just skip them
// we update an entity and entity can contain properties which aren't columns, so we just skip them
if (!column) return;
const paramName = "_updated_" + column.databaseName;
const value = this.connection.driver.preparePersistentValue(column.getEntityValue(valuesSet), column);
//
let value = column.getEntityValue(valuesSet);
if (column.referencedColumn && value instanceof Object) {
value = column.referencedColumn.getEntityValue(value);
}
value = this.connection.driver.preparePersistentValue(value, column);
// todo: duplication zone
if (value instanceof Function) { // support for SQL expressions in update query

View File

@ -60,11 +60,19 @@ export class OrmUtils {
if (this.isObject(target) && this.isObject(source)) {
for (const key in source) {
if (this.isObject(source[key])) {
let propertyKey = key;
if (source[key] instanceof Promise)
continue;
// if (source[key] instanceof Promise) {
// propertyKey = "__" + key + "__";
// }
if (this.isObject(source[propertyKey]) && !(source[propertyKey] instanceof Date)) {
if (!target[key]) Object.assign(target, { [key]: {} });
this.mergeDeep(target[key], source[key]);
this.mergeDeep(target[key], source[propertyKey]);
} else {
Object.assign(target, { [key]: source[key] });
Object.assign(target, { [key]: source[propertyKey] });
}
}
}

View File

@ -3,7 +3,8 @@ import {closeTestingConnections, createTestingConnections, reloadTestingDatabase
import {Connection} from "../../../src/connection/Connection";
import {Student} from "./entity/Student";
describe("github issues > #144 Class Table Inheritance doesn't seem to work", () => {
// todo fix this test once class table inheritance support is back
describe.skip("github issues > #144 Class Table Inheritance doesn't seem to work", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({

View File

@ -3,7 +3,8 @@ import {createTestingConnections, closeTestingConnections, reloadTestingDatabase
import {Connection} from "../../../src/connection/Connection";
import {Category} from "./entity/Category";
describe("github issues > #904 Using closure tables without @TreeLevelColumn will always fail on insert", () => {
// todo: uncomment test once closure tables functionality is back
describe.skip("github issues > #904 Using closure tables without @TreeLevelColumn will always fail on insert", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({

View File

@ -48,7 +48,7 @@ describe("many-to-many", function() {
// Specifications
// -------------------------------------------------------------------------
describe.only("insert post and details (has inverse relation + full cascade options)", function() {
describe("insert post and details (has inverse relation + full cascade options)", function() {
let newPost: Post, details: PostDetails, savedPost: Post;
before(reloadDatabase);