moved update/delete/insert operations from persistoperationexecuter into driver specifics; made query builder to support delete and update operations

This commit is contained in:
Umed Khudoiberdiev 2016-03-13 21:33:55 +05:00
parent e530d719b0
commit a5956a5f62
6 changed files with 185 additions and 63 deletions

View File

@ -32,6 +32,7 @@ export class Connection {
constructor(name: string, driver: Driver) {
this._name = name;
this._driver = driver;
this._driver.connection = this;
}
// -------------------------------------------------------------------------

View File

@ -17,11 +17,16 @@ export interface Driver {
* Gets database name to which this connection is made.
*/
db: string;
/**
* Connection used in this driver.
*/
connection: Connection;
/**
* Creates a query builder which can be used to build an sql queries.
*/
createQueryBuilder<Entity>(connection: Connection): QueryBuilder<Entity>;
createQueryBuilder<Entity>(): QueryBuilder<Entity>;
/**
* Creates a schema builder which can be used to build database/table schemas.
@ -48,4 +53,19 @@ export interface Driver {
*/
clearDatabase(): Promise<void>;
/**
* Updates rows that match given conditions in the given table.
*/
update(tableName: string, valuesMap: Object, conditions: Object): Promise<void>;
/**
* Insert a new row into given table.
*/
insert(tableName: string, valuesMap: Object): Promise<any>;
/**
* Insert a new row into given table.
*/
delete(tableName: string, conditions: Object): Promise<void>;
}

View File

@ -3,7 +3,6 @@ import {ConnectionOptions} from "../connection/ConnectionOptions";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {MysqlSchemaBuilder} from "../schema-builder/MysqlSchemaBuilder";
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {Connection} from "../connection/Connection";
/**
@ -15,8 +14,10 @@ export class MysqlDriver implements Driver {
// Properties
// -------------------------------------------------------------------------
connection: Connection;
private mysql: any;
private connection: any;
private mysqlConnection: any;
private connectionOptions: ConnectionOptions;
// -------------------------------------------------------------------------
@ -46,8 +47,8 @@ export class MysqlDriver implements Driver {
/**
* Creates a query builder which can be used to build an sql queries.
*/
createQueryBuilder<Entity>(connection: Connection): QueryBuilder<Entity> {
return new QueryBuilder<Entity>(connection);
createQueryBuilder<Entity>(): QueryBuilder<Entity> {
return new QueryBuilder<Entity>(this.connection);
}
/**
@ -62,24 +63,24 @@ export class MysqlDriver implements Driver {
*/
connect(options: ConnectionOptions): Promise<void> {
this.connectionOptions = options;
this.connection = this.mysql.createConnection({
this.mysqlConnection = this.mysql.createConnection({
host: options.host,
user: options.username,
password: options.password,
database: options.database
});
return new Promise<void>((ok, fail) => this.connection.connect((err: any) => err ? fail(err) : ok()));
return new Promise<void>((ok, fail) => this.mysqlConnection.connect((err: any) => err ? fail(err) : ok()));
}
/**
* Closes connection with database.
*/
disconnect(): Promise<void> {
if (!this.connection)
if (!this.mysqlConnection)
throw new Error("Connection is not established, cannot disconnect.");
return new Promise<void>((ok, fail) => {
this.connection.end((err: any) => err ? fail(err) : ok());
this.mysqlConnection.end((err: any) => err ? fail(err) : ok());
});
}
@ -87,9 +88,9 @@ export class MysqlDriver implements Driver {
* Executes a given SQL query.
*/
query<T>(query: string): Promise<T> {
if (!this.connection) throw new Error("Connection is not established, cannot execute a query.");
console.info("executing:", query);
return new Promise<any>((ok, fail) => this.connection.query(query, (err: any, result: any) => {
if (!this.mysqlConnection) throw new Error("Connection is not established, cannot execute a query.");
// console.info("executing:", query);
return new Promise<any>((ok, fail) => this.mysqlConnection.query(query, (err: any, result: any) => {
if (err) {
console.error("query failed: ", query);
fail(err);
@ -103,9 +104,9 @@ export class MysqlDriver implements Driver {
* Clears all tables in the currently connected database.
*/
clearDatabase(): Promise<void> {
if (!this.connection) throw new Error("Connection is not established, cannot execute a query.");
if (!this.mysqlConnection) throw new Error("Connection is not established, cannot execute a query.");
// todo: omprize and make coder better
// todo: optrize and make coder better
const query1 = `SET FOREIGN_KEY_CHECKS = 0;`;
const query2 = `SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS q FROM information_schema.tables WHERE table_schema = '${this.connectionOptions.database}';`;
@ -121,4 +122,32 @@ export class MysqlDriver implements Driver {
.then(() => {});
}
/**
* Updates rows that match given conditions in the given table.
*/
update(tableName: string, valuesMap: Object, conditions: Object): Promise<void> {
const qb = this.createQueryBuilder().update(tableName, valuesMap).from(tableName, "t");
Object.keys(conditions).forEach(key => qb.andWhere(key + "=:" + key, { [key]: (<any> conditions)[key] }));
return qb.execute().then(() => {});
}
/**
* Insert a new row into given table.
*/
insert(tableName: string, keyValues: Object): Promise<any> {
const columns = Object.keys(keyValues).join(",");
const values = Object.keys(keyValues).map(key => (<any> keyValues)[key]).join(",");
const query = `INSERT INTO ${tableName}(${columns}) VALUES (${values})`;
return this.query(query);
}
/**
* Deletes from the given table by a given conditions.
*/
delete(tableName: string, conditions: Object): Promise<void> {
const qb = this.createQueryBuilder().delete(tableName);
Object.keys(conditions).forEach(key => qb.andWhere(key + "=:" + key, { [key]: (<any> conditions)[key] }));
return qb.execute().then(() => {});
}
}

View File

@ -7,6 +7,9 @@ import {InsertOperation} from "./operation/InsertOperation";
import {JunctionRemoveOperation} from "./operation/JunctionRemoveOperation";
import {UpdateByRelationOperation} from "./operation/UpdateByRelationOperation";
/**
* Executes PersistOperation in the given connection.
*/
export class PersistOperationExecutor {
// -------------------------------------------------------------------------
@ -20,6 +23,9 @@ export class PersistOperationExecutor {
// Public Methods
// -------------------------------------------------------------------------
/**
* Executes given persist operation.
*/
executePersistOperation(persistOperation: PersistOperation) {
return Promise.resolve()
.then(() => this.executeInsertOperations(persistOperation))
@ -130,32 +136,32 @@ export class PersistOperationExecutor {
idColumn = metadata.primaryColumn.name;
id = operation.targetEntity[metadata.primaryColumn.propertyName] || idInInserts;
}
const query = `UPDATE ${tableName} SET ${relationName}='${relationId}' WHERE ${idColumn}='${id}'`;
return this.connection.driver.query(query);
return this.connection.driver.update(tableName, { [relationName]: relationId }, { [idColumn]: id });
}
private update(updateOperation: UpdateOperation) {
const entity = updateOperation.entity;
const metadata = this.connection.getMetadata(entity.constructor);
const values = updateOperation.columns.map(column => {
return column.name + "='" + entity[column.propertyName] + "'";
});
const query = `UPDATE ${metadata.table.name} SET ${values} WHERE ${metadata.primaryColumn.name}='${metadata.getEntityId(entity)}'` ;
return this.connection.driver.query(query);
const values = updateOperation.columns.reduce((object, column) => {
(<any> object)[column.name] = entity[column.propertyName];
return object;
}, {});
return this.connection.driver.update(metadata.table.name, values, { [metadata.primaryColumn.name]: metadata.getEntityId(entity) });
}
private updateDeletedRelations(removeOperation: RemoveOperation) { // todo: check if both many-to-one deletions work too
if (removeOperation.relation.isManyToMany || removeOperation.relation.isOneToMany) return;
const value = removeOperation.relation.name + "=NULL";
const query = `UPDATE ${removeOperation.metadata.table.name} SET ${value} WHERE ${removeOperation.metadata.primaryColumn.name}='${removeOperation.fromEntityId}'` ;
return this.connection.driver.query(query);
return this.connection.driver.update(
removeOperation.metadata.table.name,
{ [removeOperation.relation.name]: null },
{ [removeOperation.metadata.primaryColumn.name]: removeOperation.fromEntityId }
);
}
private delete(entity: any) {
const metadata = this.connection.getMetadata(entity.constructor);
const query = `DELETE FROM ${metadata.table.name} WHERE ${metadata.primaryColumn.name}='${entity[metadata.primaryColumn.propertyName]}'`;
return this.connection.driver.query(query);
return this.connection.driver.delete(metadata.table.name, { [metadata.primaryColumn.name]: entity[metadata.primaryColumn.propertyName] });
}
private insert(entity: any) {
@ -180,8 +186,10 @@ export class PersistOperationExecutor {
.filter(relation => entity[relation.propertyName].hasOwnProperty(relation.relatedEntityMetadata.primaryColumn.name))
.map(relation => "'" + entity[relation.propertyName][relation.relatedEntityMetadata.primaryColumn.name] + "'");
const query = `INSERT INTO ${metadata.table.name}(${columns.concat(relationColumns).join(",")}) VALUES (${values.concat(relationValues).join(",")})`;
return this.connection.driver.query(query);
const allColumns = columns.concat(relationColumns);
const allValues = values.concat(relationValues);
return this.connection.driver.insert(metadata.table.name, this.zipObject(allColumns, allValues));
}
private insertJunctions(junctionOperation: JunctionInsertOperation, insertOperations: InsertOperation[]) {
@ -192,9 +200,7 @@ export class PersistOperationExecutor {
const id1 = junctionOperation.entity1[metadata1.primaryColumn.name] || insertOperations.find(o => o.entity === junctionOperation.entity1).entityId;
const id2 = junctionOperation.entity2[metadata2.primaryColumn.name] || insertOperations.find(o => o.entity === junctionOperation.entity2).entityId;
const values = [id1, id2]; // todo: order may differ, find solution (column.table to compare with entity metadata table?)
const query = `INSERT INTO ${junctionMetadata.table.name}(${columns.join(",")}) VALUES (${values.join(",")})`;
return this.connection.driver.query(query);
return this.connection.driver.insert(junctionMetadata.table.name, this.zipObject(columns, values));
}
private removeJunctions(junctionOperation: JunctionRemoveOperation) {
@ -204,8 +210,14 @@ export class PersistOperationExecutor {
const columns = junctionMetadata.columns.map(column => column.name);
const id1 = junctionOperation.entity1[metadata1.primaryColumn.name];
const id2 = junctionOperation.entity2[metadata2.primaryColumn.name];
const query = `DELETE FROM ${junctionMetadata.table.name} WHERE ${columns[0]}='${id1}' AND ${columns[1]}='${id2}'`;
return this.connection.driver.query(query);
return this.connection.driver.delete(junctionMetadata.table.name, { [columns[0]]: id1, [columns[1]]: id2 });
}
private zipObject(keys: any[], values: any[]): Object {
return keys.reduce((object, column, index) => {
(<any> object)[column] = values[index];
return object;
}, {});
}
}

View File

@ -19,7 +19,9 @@ export class QueryBuilder<Entity> {
private _aliasMap: AliasMap;
private type: "select"|"update"|"delete";
private selects: string[] = [];
private froms: { alias: Alias };
private fromEntity: { alias: Alias };
private fromTableName: string;
private updateQuerySet: Object;
private joins: Join[] = [];
private groupBys: string[] = [];
private wheres: { type: "simple"|"and"|"or", condition: string }[] = [];
@ -49,13 +51,43 @@ export class QueryBuilder<Entity> {
// Public Methods
// -------------------------------------------------------------------------
delete(): this {
delete(entity?: Function): this;
delete(tableName?: string): this;
delete(tableNameOrEntity?: string|Function): this {
if (tableNameOrEntity instanceof Function) {
const aliasName = (<any> tableNameOrEntity).name;
const aliasObj = new Alias(aliasName);
aliasObj.target = <Function> tableNameOrEntity;
this._aliasMap.addMainAlias(aliasObj);
this.fromEntity = { alias: aliasObj };
} else if (typeof tableNameOrEntity === "string") {
this.fromTableName = <string> tableNameOrEntity;
}
this.type = "delete";
return this;
}
update(): this {
update(updateSet: Object): this;
update(entity: Function, updateSet: Object): this;
update(tableName: string, updateSet: Object): this;
update(tableNameOrEntityOrUpdateSet?: string|Function, updateSet?: Object): this {
if (tableNameOrEntityOrUpdateSet instanceof Function) {
const aliasName = (<any> tableNameOrEntityOrUpdateSet).name;
const aliasObj = new Alias(aliasName);
aliasObj.target = <Function> tableNameOrEntityOrUpdateSet;
this._aliasMap.addMainAlias(aliasObj);
this.fromEntity = { alias: aliasObj };
} else if (typeof tableNameOrEntityOrUpdateSet === "object") {
updateSet = <Object> tableNameOrEntityOrUpdateSet;
} else if (typeof tableNameOrEntityOrUpdateSet === "string") {
this.fromTableName = <string> tableNameOrEntityOrUpdateSet;
}
this.type = "update";
this.updateQuerySet = updateSet;
return this;
}
@ -86,13 +118,17 @@ export class QueryBuilder<Entity> {
return this;
}
//from(tableName: string, alias: string): this;
from(entity: Function, alias?: string): this {
//from(entityOrTableName: Function|string, alias: string): this {
const aliasObj = new Alias(alias);
aliasObj.target = entity;
this._aliasMap.addMainAlias(aliasObj);
this.froms = { alias: aliasObj };
from(tableName: string, alias: string): this;
from(entity: Function, alias?: string): this;
from(entityOrTableName: Function|string, alias: string): this {
if (entityOrTableName instanceof Function) {
const aliasObj = new Alias(alias);
aliasObj.target = <Function> entityOrTableName;
this._aliasMap.addMainAlias(aliasObj);
this.fromEntity = { alias: aliasObj };
} else {
this.fromTableName = <string> entityOrTableName;
}
return this;
}
@ -233,8 +269,8 @@ export class QueryBuilder<Entity> {
return sql;
}
execute(): Promise<void> {
return this.connection.driver.query(this.getSql()).then(() => {});
execute(): Promise<any> {
return this.connection.driver.query(this.getSql());
}
getScalarResults<T>(): Promise<T[]> {
@ -266,16 +302,28 @@ export class QueryBuilder<Entity> {
protected createSelectExpression() {
// todo throw exception if selects or from is missing
const metadata = this._aliasMap.getEntityMetadataByAlias(this.froms.alias);
const tableName = metadata.table.name;
const alias = this.froms.alias.name;
const allSelects: string[] = [];
// add select from the main table
if (this.selects.indexOf(alias) !== -1)
metadata.columns.forEach(column => {
allSelects.push(alias + "." + column.name + " AS " + alias + "_" + column.name);
});
let alias: string, tableName: string;
const allSelects: string[] = [];
if (this.fromEntity) {
const metadata = this._aliasMap.getEntityMetadataByAlias(this.fromEntity.alias);
tableName = metadata.table.name;
alias = this.fromEntity.alias.name;
// add select from the main table
if (this.selects.indexOf(alias) !== -1) {
metadata.columns.forEach(column => {
allSelects.push(alias + "." + column.name + " AS " + alias + "_" + column.name);
});
}
} else if (this.fromTableName) {
tableName = this.fromTableName;
} else {
throw new Error("No from given");
}
// add selects from joins
this.joins
@ -292,13 +340,24 @@ export class QueryBuilder<Entity> {
return select !== alias && !this.joins.find(join => join.alias.name === select);
}).forEach(select => allSelects.push(select));
// if still selection is empty, then simply set it to all (*)
if (allSelects.length === 0)
allSelects.push("*");
// create a selection query
switch (this.type) {
case "select":
return "SELECT " + allSelects.join(", ") + " FROM " + tableName + " " + alias;
case "update":
return "UPDATE " + tableName + " " + alias;
case "delete":
return "DELETE " + tableName + " " + alias;
return "DELETE FROM " + tableName + " " + (alias ? alias : "");
case "update":
const updateSet = Object.keys(this.updateQuerySet).map(key => key + "=:updateQuerySet_" + key);
const params = Object.keys(this.updateQuerySet).reduce((object, key) => {
(<any> object)["updateQuerySet_" + key] = (<any> this.updateQuerySet)[key];
return object;
}, {});
this.addParameters(params);
return "UPDATE " + tableName + " " + (alias ? alias : "") + " SET " + updateSet;
}
return "";
}
@ -306,12 +365,12 @@ export class QueryBuilder<Entity> {
protected createWhereExpression() {
if (!this.wheres || !this.wheres.length) return "";
return " WHERE " + this.wheres.map(where => {
return " WHERE " + this.wheres.map((where, index) => {
switch (where.type) {
case "and":
return "AND " + where.condition;
return (index > 0 ? "AND " : "") + where.condition;
case "or":
return "OR " + where.condition;
return (index > 0 ? "OR " : "") + where.condition;
default:
return where.condition;
}
@ -392,7 +451,8 @@ export class QueryBuilder<Entity> {
protected replaceParameters(sql: string) {
Object.keys(this.parameters).forEach(key => {
sql = sql.replace(":" + key, '"' + this.parameters[key] + '"'); // .replace('"', '')
const value = this.parameters[key] !== null && this.parameters[key] !== undefined ? '"' + this.parameters[key] + '"' : "NULL";
sql = sql.replace(":" + key, value); // .replace('"', '')
});
return sql;
}

View File

@ -64,7 +64,7 @@ export class Repository<Entity> {
*/
createQueryBuilder(alias: string): QueryBuilder<Entity> {
return this.connection.driver
.createQueryBuilder<Entity>(this.connection)
.createQueryBuilder<Entity>()
.select(alias)
.from(this.metadata.target, alias);
}