added support of embeddables

This commit is contained in:
Umed Khudoiberdiev 2016-05-27 12:01:16 +05:00
parent 249b30bc91
commit 342b560ad9
11 changed files with 239 additions and 126 deletions

View File

@ -23,41 +23,35 @@ const options: CreateConnectionOptions = {
createConnection(options).then(connection => {
let postRepository = connection.getRepository(Post);
let questionRepository = connection.getRepository(Question);
const question = new Question();
question.title = "Hello question!";
question.counters = new Counters();
question.counters.stars = 5;
question.counters.raiting = 10;
question.counters.commentCount = 3;
question.counters.metadata = "#question #question-counter";
const questionPromise = questionRepository.findOneById(1).then(question => {
if (!question) {
question = new Question();
question.title = "Umed";
return questionRepository.persist(question).then(savedAuthor => {
return questionRepository.findOneById(1);
});
}
return question;
});
const postPromise = postRepository.findOneById(1).then(post => {
if (!post) {
post = new Post();
post.title = "Hello post";
post.text = "This is post contents";
return postRepository.persist(post).then(savedPost => {
return postRepository.findOneById(1);
});
}
return post;
});
return Promise.all<any>([questionPromise, postPromise])
.then(results => {
const [question, post] = results;
question.posts = [post];
return questionRepository.persist(question);
questionRepository
.persist(question)
.then(savedQuestion => {
console.log("question has been saved: ", savedQuestion);
// lets load it now:
return questionRepository.findOneById(savedQuestion.id);
})
.then(savedAuthor => {
console.log("Question has been saved: ", savedAuthor);
.then(loadedQuestion => {
console.log("question has been loaded: ", loadedQuestion);
loadedQuestion.counters.commentCount = 7;
loadedQuestion.counters.metadata = "#updated question";
return questionRepository.persist(loadedQuestion);
})
.catch(error => console.log(error.stack));
.then(updatedQuestion => {
console.log("question has been updated: ", updatedQuestion);
})
.catch(e => console.log(e));
}, error => console.log("Cannot connect: ", error));

View File

@ -8,7 +8,6 @@ export function EmbeddableTable(): Function {
return function (target: Function) {
const args: TableMetadataArgs = {
target: target,
name: name,
type: "embeddable"
};
getMetadataArgsStorage().tables.add(args);

View File

@ -39,15 +39,15 @@ export class EntityMetadataBuilder {
const embeddableMergedArgs = getMetadataArgsStorage().getMergedEmbeddableTableMetadatas(entityClasses);
const entityMetadatas = getMetadataArgsStorage().getMergedTableMetadatas(entityClasses).map(mergedArgs => {
// find embeddable tables for embeddeds registered in this table and create EmbeddedMetadatas from them
const embeddeds: EmbeddedMetadata[] = [];
mergedArgs.embeddeds.forEach(embedded => {
const embeddableTable = embeddableMergedArgs.find(mergedArs => mergedArgs.table.target === embedded.type);
const embeddableTable = embeddableMergedArgs.find(mergedArgs => mergedArgs.table.target === embedded.type());
if (embeddableTable) {
const table = new TableMetadata(mergedArgs.table);
const columns = mergedArgs.columns.map(args => new ColumnMetadata(args));
embeddeds.push(new EmbeddedMetadata(table, columns));
const table = new TableMetadata(embeddableTable.table);
const columns = embeddableTable.columns.map(args => new ColumnMetadata(args));
embeddeds.push(new EmbeddedMetadata(embedded.type(), embedded.propertyName, table, columns));
}
});
@ -135,8 +135,7 @@ export class EntityMetadataBuilder {
nullable: relation.isNullable
}
});
relationalColumn.entityMetadata = metadata;
metadata.columns.push(relationalColumn);
metadata.addColumn(relationalColumn);
}
// create and add foreign key

View File

@ -2,6 +2,7 @@ import {PropertyMetadata} from "./PropertyMetadata";
import {ColumnMetadataArgs} from "../metadata-args/ColumnMetadataArgs";
import {ColumnType} from "./types/ColumnTypes";
import {EntityMetadata} from "./EntityMetadata";
import {EmbeddedMetadata} from "./EmbeddedMetadata";
/**
* Kinda type of the column. Not a type in the database, but locally used type to determine what kind of column
@ -23,6 +24,11 @@ export class ColumnMetadata extends PropertyMetadata {
*/
entityMetadata: EntityMetadata;
/**
* Embedded metadata where this column metadata is.
*/
embeddedMetadata: EmbeddedMetadata;
// ---------------------------------------------------------------------
// Public Readonly Properties
// ---------------------------------------------------------------------
@ -109,19 +115,8 @@ export class ColumnMetadata extends PropertyMetadata {
// Constructor
// ---------------------------------------------------------------------
constructor(args: ColumnMetadataArgs);
constructor(entityMetadata: EntityMetadata, args: ColumnMetadataArgs);
constructor(entityMetadataOrArgs: EntityMetadata|ColumnMetadataArgs, args?: ColumnMetadataArgs) {
super(
args ? args.target : (entityMetadataOrArgs as ColumnMetadataArgs).target,
args ? args.propertyName : (entityMetadataOrArgs as ColumnMetadataArgs).propertyName
);
if (entityMetadataOrArgs && args) {
this.entityMetadata = entityMetadataOrArgs as EntityMetadata;
}
args = args ? args : entityMetadataOrArgs as ColumnMetadataArgs;
constructor(args: ColumnMetadataArgs) {
super(args.target, args.propertyName);
if (args.mode)
this.mode = args.mode;
@ -163,12 +158,30 @@ export class ColumnMetadata extends PropertyMetadata {
*/
get name(): string {
// if custom column name is set implicitly then return it
if (this._name)
return this.entityMetadata.namingStrategy.columnNameCustomized(this._name);
// if this column is embedded's column then apply different entity
if (this.embeddedMetadata) {
if (this._name)
return this.embeddedMetadata.entityMetadata.namingStrategy.embeddedColumnNameCustomized(this.embeddedMetadata.propertyName, this._name);
// if there is a naming strategy then use it to normalize propertyName as column name
return this.entityMetadata.namingStrategy.columnName(this.propertyName);
return this.embeddedMetadata.entityMetadata.namingStrategy.embeddedColumnName(this.embeddedMetadata.propertyName, this.propertyName);
}
if (this.entityMetadata) {
if (this._name)
return this.entityMetadata.namingStrategy.columnNameCustomized(this._name);
// if there is a naming strategy then use it to normalize propertyName as column name
return this.entityMetadata.namingStrategy.columnName(this.propertyName);
}
throw new Error(`Column${this._name ? this._name + " " : ""} is not attached to any entity or embedded.`);
}
/**
* Indicates if this column is in embedded, not directly in the table.
*/
get isInEmbedded(): boolean {
return !!this.embeddedMetadata;
}
/**
@ -206,4 +219,39 @@ export class ColumnMetadata extends PropertyMetadata {
return this.mode === "version";
}
/**
* Gets embedded property in which column is.
*/
get embeddedProperty() {
if (!this.embeddedMetadata)
throw new Error(`This column${ this._name ? this._name + " " : "" } is not in embedded entity.`);
return this.embeddedMetadata.propertyName;
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------
hasEntityValue(entity: any) {
if (!entity)
return false;
if (this.isInEmbedded) {
return entity.hasOwnProperty(this.embeddedProperty) &&
entity[this.embeddedProperty].hasOwnProperty(this.propertyName);
} else {
return entity.hasOwnProperty(this.propertyName);
}
}
getEntityValue(entity: any) {
if (this.isInEmbedded) {
return entity[this.embeddedProperty][this.propertyName];
} else {
return entity[this.propertyName];
}
}
}

View File

@ -20,6 +20,11 @@ export class EmbeddedMetadata {
// Private Properties
// ---------------------------------------------------------------------
/**
* Property name on which this embedded is attached.
*/
readonly propertyName: string;
/**
* Embeddable table.
*/
@ -30,13 +35,34 @@ export class EmbeddedMetadata {
*/
readonly columns: ColumnMetadata[];
/**
* Embedded type.
*/
readonly type: Function;
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(table: TableMetadata, columns: ColumnMetadata[]) {
constructor(type: Function, propertyName: string, table: TableMetadata, columns: ColumnMetadata[]) {
this.type = type;
this.propertyName = propertyName;
this.table = table;
this.columns = columns;
this.columns.forEach(column => {
column.embeddedMetadata = this;
});
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------
/**
* Creates a new embedded object.
*/
create() {
return new (this.type as any);
}
}

View File

@ -36,11 +36,6 @@ export class EntityMetadata {
*/
readonly table: TableMetadata;
/**
* Entity's column metadatas.
*/
readonly columns: ColumnMetadata[];
/**
* Entity's relation metadatas.
*/
@ -61,6 +56,15 @@ export class EntityMetadata {
*/
readonly embeddeds: EmbeddedMetadata[];
// -------------------------------------------------------------------------
// Private properties
// -------------------------------------------------------------------------
/**
* Entity's column metadatas.
*/
private readonly _columns: ColumnMetadata[];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -68,14 +72,14 @@ export class EntityMetadata {
constructor(args: EntityMetadataArgs) {
this.namingStrategy = args.namingStrategy;
this.table = args.tableMetadata;
this.columns = args.columnMetadatas || [];
this._columns = args.columnMetadatas || [];
this.relations = args.relationMetadatas || [];
this.indices = args.indexMetadatas || [];
this.foreignKeys = args.foreignKeyMetadatas || [];
this.embeddeds = args.embeddedMetadatas || [];
this.table.entityMetadata = this;
this.columns.forEach(column => column.entityMetadata = this);
this._columns.forEach(column => column.entityMetadata = this);
this.relations.forEach(relation => relation.entityMetadata = this);
this.foreignKeys.forEach(foreignKey => foreignKey.entityMetadata = this);
this.indices.forEach(index => index.entityMetadata = this);
@ -99,6 +103,17 @@ export class EntityMetadata {
return this.table.name;
}
/**
* All columns of the entity, including columns that are coming from the embeddeds of this entity.
*/
get columns() {
let allColumns: ColumnMetadata[] = [].concat(this._columns);
this.embeddeds.forEach(embedded => {
allColumns = allColumns.concat(embedded.columns);
});
return allColumns;
}
/**
* Target class to which this entity metadata is bind.
*/
@ -111,14 +126,14 @@ export class EntityMetadata {
* Special entity metadatas like for junction tables and closure junction tables don't have a primary column.
*/
get hasPrimaryColumn(): boolean {
return !!this.columns.find(column => column.isPrimary);
return !!this._columns.find(column => column.isPrimary);
}
/**
* Gets the primary column.
*/
get primaryColumn(): ColumnMetadata {
const primaryKey = this.columns.find(column => column.isPrimary);
const primaryKey = this._columns.find(column => column.isPrimary);
if (!primaryKey)
throw new Error(`Primary key is not set for the ${this.name} entity.`);
@ -129,14 +144,14 @@ export class EntityMetadata {
* Checks if entity has a create date column.
*/
get hasCreateDateColumn(): boolean {
return !!this.columns.find(column => column.mode === "createDate");
return !!this._columns.find(column => column.mode === "createDate");
}
/**
* Gets entity column which contains a create date value.
*/
get createDateColumn(): ColumnMetadata {
const column = this.columns.find(column => column.mode === "createDate");
const column = this._columns.find(column => column.mode === "createDate");
if (!column)
throw new Error(`CreateDateColumn was not found in entity ${this.name}`);
@ -147,14 +162,14 @@ export class EntityMetadata {
* Checks if entity has an update date column.
*/
get hasUpdateDateColumn(): boolean {
return !!this.columns.find(column => column.mode === "updateDate");
return !!this._columns.find(column => column.mode === "updateDate");
}
/**
* Gets entity column which contains an update date value.
*/
get updateDateColumn(): ColumnMetadata {
const column = this.columns.find(column => column.mode === "updateDate");
const column = this._columns.find(column => column.mode === "updateDate");
if (!column)
throw new Error(`UpdateDateColumn was not found in entity ${this.name}`);
@ -165,14 +180,14 @@ export class EntityMetadata {
* Checks if entity has a version column.
*/
get hasVersionColumn(): boolean {
return !!this.columns.find(column => column.mode === "version");
return !!this._columns.find(column => column.mode === "version");
}
/**
* Gets entity column which contains an entity version.
*/
get versionColumn(): ColumnMetadata {
const column = this.columns.find(column => column.mode === "version");
const column = this._columns.find(column => column.mode === "version");
if (!column)
throw new Error(`VersionColumn was not found in entity ${this.name}`);
@ -183,11 +198,11 @@ export class EntityMetadata {
* Checks if entity has a tree level column.
*/
get hasTreeLevelColumn(): boolean {
return !!this.columns.find(column => column.mode === "treeLevel");
return !!this._columns.find(column => column.mode === "treeLevel");
}
get treeLevelColumn(): ColumnMetadata {
const column = this.columns.find(column => column.mode === "treeLevel");
const column = this._columns.find(column => column.mode === "treeLevel");
if (!column)
throw new Error(`TreeLevelColumn was not found in entity ${this.name}`);
@ -243,6 +258,42 @@ export class EntityMetadata {
return this.ownerOneToOneRelations.concat(this.manyToOneRelations);
}
/**
* Checks if there is a tree parent relation. Used only in tree-tables.
*/
get hasTreeParentRelation() {
return !!this.relations.find(relation => relation.isTreeParent);
}
/**
* Tree parent relation. Used only in tree-tables.
*/
get treeParentRelation() {
const relation = this.relations.find(relation => relation.isTreeParent);
if (!relation)
throw new Error(`TreeParent relation was not found in entity ${this.name}`);
return relation;
}
/**
* Checks if there is a tree children relation. Used only in tree-tables.
*/
get hasTreeChildrenRelation() {
return !!this.relations.find(relation => relation.isTreeChildren);
}
/**
* Tree children relation. Used only in tree-tables.
*/
get treeChildrenRelation() {
const relation = this.relations.find(relation => relation.isTreeChildren);
if (!relation)
throw new Error(`TreeParent relation was not found in entity ${this.name}`);
return relation;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
@ -259,7 +310,7 @@ export class EntityMetadata {
*/
createPropertiesMap(): { [name: string]: string|any } {
const entity: { [name: string]: string|any } = {};
this.columns.forEach(column => entity[column.propertyName] = column.propertyName);
this._columns.forEach(column => entity[column.propertyName] = column.propertyName);
this.relations.forEach(relation => entity[relation.propertyName] = relation.propertyName);
return entity;
}
@ -282,14 +333,14 @@ export class EntityMetadata {
* Checks if column with the given property name exist.
*/
hasColumnWithPropertyName(propertyName: string): boolean {
return !!this.columns.find(column => column.propertyName === propertyName);
return !!this._columns.find(column => column.propertyName === propertyName);
}
/**
* Checks if column with the given database name exist.
*/
hasColumnWithDbName(name: string): boolean {
return !!this.columns.find(column => column.name === name);
return !!this._columns.find(column => column.name === name);
}
/**
@ -327,41 +378,10 @@ export class EntityMetadata {
return relation;
}
/**
* Checks if there is a tree parent relation. Used only in tree-tables.
*/
get hasTreeParentRelation() {
return !!this.relations.find(relation => relation.isTreeParent);
}
/**
* Tree parent relation. Used only in tree-tables.
*/
get treeParentRelation() {
const relation = this.relations.find(relation => relation.isTreeParent);
if (!relation)
throw new Error(`TreeParent relation was not found in entity ${this.name}`);
return relation;
}
/**
* Checks if there is a tree children relation. Used only in tree-tables.
*/
get hasTreeChildrenRelation() {
return !!this.relations.find(relation => relation.isTreeChildren);
}
/**
* Tree children relation. Used only in tree-tables.
*/
get treeChildrenRelation() {
const relation = this.relations.find(relation => relation.isTreeChildren);
if (!relation)
throw new Error(`TreeParent relation was not found in entity ${this.name}`);
return relation;
addColumn(column: ColumnMetadata) {
this._columns.push(column);
column.entityMetadata = this;
}
}

View File

@ -22,6 +22,14 @@ export class DefaultNamingStrategy implements NamingStrategyInterface {
return customName;
}
embeddedColumnName(embeddedPropertyName: string, columnPropertyName: string): string {
return embeddedPropertyName + "_" + columnPropertyName;
}
embeddedColumnNameCustomized(embeddedPropertyName: string, columnCustomName: string): string {
return embeddedPropertyName + "_" + columnCustomName;
}
relationName(propertyName: string): string {
return propertyName;
}

View File

@ -24,6 +24,16 @@ export interface NamingStrategyInterface {
*/
columnNameCustomized(customName: string): string;
/**
* Gets the embedded's column name from the given property name.
*/
embeddedColumnName(embeddedPropertyName: string, columnPropertyName: string): string;
/**
* Gets the embedded's column name from the given custom column name.
*/
embeddedColumnNameCustomized(embeddedPropertyName: string, columnCustomName: string): string;
/**
* Gets the table's relation name from the given property name.
*/

View File

@ -416,10 +416,10 @@ export class EntityPersistOperationBuilder {
private diffColumns(metadata: EntityMetadata, newEntity: any, dbEntity: any) {
return metadata.columns
.filter(column => !column.isVirtual && !column.isUpdateDate && !column.isVersion && !column.isCreateDate)
.filter(column => newEntity[column.propertyName] !== dbEntity[column.propertyName])
.filter(column => column.getEntityValue(newEntity) !== column.getEntityValue(dbEntity))
.filter(column => {
// filter out "relational columns" only in the case if there is a relation object in entity
if (metadata.hasRelationWithDbName(column.propertyName)) {
if (!column.isInEmbedded && metadata.hasRelationWithDbName(column.propertyName)) {
const relation = metadata.findRelationWithDbName(column.propertyName);
if (newEntity[relation.propertyName] !== null && newEntity[relation.propertyName] !== undefined)
return false;

View File

@ -9,6 +9,7 @@ import {Broadcaster} from "../subscriber/Broadcaster";
import {EntityMetadataCollection} from "../metadata-args/collection/EntityMetadataCollection";
import {Driver} from "../driver/Driver";
import {UpdateByInverseSideOperation} from "./operation/UpdateByInverseSideOperation";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
/**
* Executes PersistOperation in the given connection.
@ -353,7 +354,7 @@ export class PersistOperationExecutor {
const values: { [key: string]: any } = {};
updateOperation.columns.forEach(column => {
values[column.name] = this.driver.preparePersistentValue(entity[column.propertyName], column);
values[column.name] = this.driver.preparePersistentValue(column.getEntityValue(entity), column);
});
updateOperation.relations.forEach(relation => {
@ -393,14 +394,13 @@ export class PersistOperationExecutor {
private insert(operation: InsertOperation) {
const entity = operation.entity;
const metadata = this.entityMetadatas.findByTarget(entity.constructor);
const columns = metadata.columns
.filter(column => !column.isVirtual)
.filter(column => entity.hasOwnProperty(column.propertyName))
.map(column => column.name);
const values = metadata.columns
.filter(column => !column.isVirtual)
.filter(column => entity.hasOwnProperty(column.propertyName))
.map(column => this.driver.preparePersistentValue(entity[column.propertyName], column));
.filter(column => !column.isVirtual && column.hasEntityValue(entity));
const columnNames = columns.map(column => column.name);
const values = columns.map(column => this.driver.preparePersistentValue(column.getEntityValue(entity), column));
const relationColumns = metadata.relations
.filter(relation => relation.isOwning && !!relation.inverseEntityMetadata)
.filter(relation => entity.hasOwnProperty(relation.propertyName))
@ -413,7 +413,7 @@ export class PersistOperationExecutor {
.filter(relation => entity[relation.propertyName].hasOwnProperty(relation.inverseEntityMetadata.primaryColumn.propertyName))
.map(relation => entity[relation.propertyName][relation.inverseEntityMetadata.primaryColumn.propertyName]);
const allColumns = columns.concat(relationColumns);
const allColumns = columnNames.concat(relationColumns);
const allValues = values.concat(relationValues);
if (metadata.hasCreateDateColumn) {

View File

@ -81,7 +81,16 @@ export class RawSqlResultsToEntityTransformer {
metadata.columns.forEach(column => {
const valueInObject = alias.getColumnValue(rawSqlResults[0], column.name); // we use zero index since its grouped data
if (valueInObject && column.propertyName && !column.isVirtual) {
entity[column.propertyName] = this.driver.prepareHydratedValue(valueInObject, column);
const value = this.driver.prepareHydratedValue(valueInObject, column);
if (column.isInEmbedded) {
if (!entity[column.embeddedProperty])
entity[column.embeddedProperty] = column.embeddedMetadata.create();
entity[column.embeddedProperty][column.propertyName] = value;
} else {
entity[column.propertyName] = value;
}
hasData = true;
}
});