refactoring query builder

This commit is contained in:
Umed Khudoiberdiev 2017-06-23 15:05:23 +05:00
parent 3c4a80fb49
commit 77534b4dcc
6 changed files with 204 additions and 81 deletions

View File

@ -6,6 +6,19 @@ import {ObjectLiteral} from "../common/ObjectLiteral";
*/
export class DeleteQueryBuilder<Entity> extends QueryBuilder<Entity> {
// -------------------------------------------------------------------------
// Public Implemented Methods
// -------------------------------------------------------------------------
/**
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createDeleteExpression();
sql += this.createWhereExpression();
return sql.trim();
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
@ -50,4 +63,16 @@ export class DeleteQueryBuilder<Entity> extends QueryBuilder<Entity> {
return this;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Creates DELETE express used to perform insert query.
*/
protected createDeleteExpression() {
const tableName = this.escapeTable(this.getTableName());
return `DELETE FROM ${tableName}`; // todo: how do we replace aliases in where to nothing?
}
}

View File

@ -1,11 +1,24 @@
import {QueryBuilder} from "./QueryBuilder";
import {ObjectLiteral} from "../common/ObjectLiteral";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
/**
* Allows to build complex sql queries in a fashion way and execute those queries.
*/
export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
// -------------------------------------------------------------------------
// Public Implemented Methods
// -------------------------------------------------------------------------
/**
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createInsertExpression();
return sql.trim();
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
@ -35,4 +48,52 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
return this;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Creates INSERT express used to perform insert query.
*/
protected createInsertExpression() {
const valueSets = this.getValueSets();
// get columns that participate in insertion query
const insertColumns: ColumnMetadata[] = [];
Object.keys(valueSets[0]).forEach(columnProperty => {
const column = this.expressionMap.mainAlias!.metadata.findColumnWithPropertyName(columnProperty);
if (column) insertColumns.push(column);
});
// get values needs to be inserted
const values = valueSets.map((valueSet, key) => {
const columnNames = insertColumns.map(column => {
const paramName = ":_inserted_" + key + "_" + column.databaseName;
this.setParameter(paramName, valueSet[column.propertyName]);
return paramName;
});
return "(" + columnNames.join(",") + ")";
}).join(", ");
// get a table name and all column database names
const tableName = this.escapeTable(this.getTableName());
const columnNames = insertColumns.map(column => this.escapeColumn(column.databaseName)).join(", ");
// generate sql query
return `INSERT INTO ${tableName}(${columnNames}) VALUES ${values}`;
}
/**
* Gets array of values need to be inserted into the target table.
*/
protected getValueSets(): ObjectLiteral[] {
if (this.expressionMap.valuesSet instanceof Array && this.expressionMap.valuesSet.length > 0)
return this.expressionMap.valuesSet;
if (this.expressionMap.valuesSet instanceof Object)
return [this.expressionMap.valuesSet];
throw new Error(`Cannot perform insert query because values are not defined. Call "qb.values(...)" method to specify inserted values.`);
}
}

View File

@ -100,6 +100,15 @@ export abstract class QueryBuilder<Entity> {
}
}
// -------------------------------------------------------------------------
// Abstract Methods
// -------------------------------------------------------------------------
/**
* Gets generated sql query without parameters being replaced.
*/
abstract getQuery(): string;
// -------------------------------------------------------------------------
// Accessors
// -------------------------------------------------------------------------
@ -274,22 +283,6 @@ export abstract class QueryBuilder<Entity> {
return parameters;
}
/**
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createSelectExpression();
sql += this.createJoinExpression();
sql += this.createWhereExpression();
sql += this.createGroupByExpression();
sql += this.createHavingExpression();
sql += this.createOrderByExpression();
sql += this.createLimitOffsetExpression();
sql += this.createLockExpression();
sql = this.createLimitOffsetOracleSpecificExpression(sql);
return sql.trim();
}
/**
* Prints sql to stdout using console.log.
*/
@ -309,18 +302,8 @@ export abstract class QueryBuilder<Entity> {
/**
* Gets sql to be executed with all parameters used in it.
*/
getSqlAndParameters(options?: { skipOrderBy?: boolean }): [string, any[]] {
let sql = this.createSelectExpression();
sql += this.createJoinExpression();
sql += this.createWhereExpression();
sql += this.createGroupByExpression();
sql += this.createHavingExpression();
if (!options || !options.skipOrderBy)
sql += this.createOrderByExpression();
sql += this.createLimitOffsetExpression();
sql += this.createLockExpression();
sql = this.createLimitOffsetOracleSpecificExpression(sql);
return this.connection.driver.escapeQueryWithParameters(sql, this.getParameters());
getSqlAndParameters(): [string, any[]] {
return this.connection.driver.escapeQueryWithParameters(this.getQuery(), this.getParameters());
}
/**
@ -548,56 +531,11 @@ export abstract class QueryBuilder<Entity> {
}
// create a selection query
switch (this.expressionMap.queryType) {
case "select":
const selection = allSelects.map(select => select.selection + (select.aliasName ? " AS " + ea(select.aliasName) : "")).join(", ");
if ((this.expressionMap.limit || this.expressionMap.offset) && this.connection.driver instanceof OracleDriver) {
return "SELECT ROWNUM " + this.escapeAlias("RN") + "," + selection + " FROM " + this.escapeTable(tableName) + " " + ea(aliasName) + lock;
}
return "SELECT " + selection + " FROM " + this.escapeTable(tableName) + " " + ea(aliasName) + lock;
case "delete":
return "DELETE FROM " + et(tableName); // TODO: only mysql supports aliasing, so what to do with aliases in DELETE queries? right now aliases are used however we are relaying that they will always match a table names // todo: replace aliases in where to nothing?
case "update":
let valuesSet = this.expressionMap.valuesSet as ObjectLiteral;
if (!valuesSet)
throw new Error(`Cannot perform update query because updation values are not defined.`);
const updateSet: string[] = [];
Object.keys(valuesSet).forEach(columnProperty => {
const column = this.expressionMap.mainAlias!.metadata.findColumnWithPropertyName(columnProperty);
if (column) {
const paramName = "_updated_" + column.databaseName;
this.setParameter(paramName, valuesSet[column.propertyName]);
updateSet.push(ea(column.databaseName) + "=:" + paramName);
}
});
return `UPDATE ${this.escapeTable(tableName)} ${aliasName ? ea(aliasName) : ""} SET ${updateSet.join(", ")}`; // todo: replace aliases in where to nothing?
case "insert":
let valuesSets: ObjectLiteral[] = []; // todo: check if valuesSet is defined and has items if its an array
if (this.expressionMap.valuesSet instanceof Array && this.expressionMap.valuesSet.length > 0) {
valuesSets = this.expressionMap.valuesSet;
} else if (this.expressionMap.valuesSet instanceof Object) {
valuesSets = [this.expressionMap.valuesSet];
} else {
throw new Error(`Cannot perform insert query because values are not defined.`);
}
const columns: ColumnMetadata[] = [];
Object.keys(valuesSets[0]).forEach(columnProperty => {
const column = this.expressionMap.mainAlias!.metadata.findColumnWithPropertyName(columnProperty);
if (column) columns.push(column);
});
const values = valuesSets.map((valueSet, key) => {
return "(" + columns.map(column => {
const paramName = ":_inserted_" + key + "_" + column.databaseName;
this.setParameter(paramName, valueSet[column.propertyName]);
return paramName;
}).join(",") + ")";
}).join(", ");
return `INSERT INTO ${this.escapeTable(tableName)}(${columns.map(column => column.databaseName)}) VALUES ${values}`;
const selection = allSelects.map(select => select.selection + (select.aliasName ? " AS " + ea(select.aliasName) : "")).join(", ");
if ((this.expressionMap.limit || this.expressionMap.offset) && this.connection.driver instanceof OracleDriver) {
return "SELECT ROWNUM " + this.escapeAlias("RN") + "," + selection + " FROM " + this.escapeTable(tableName) + " " + ea(aliasName) + lock;
}
throw new Error("No query builder type is specified.");
return "SELECT " + selection + " FROM " + this.escapeTable(tableName) + " " + ea(aliasName) + lock;
}
protected createHavingExpression() {
@ -862,6 +800,9 @@ export abstract class QueryBuilder<Entity> {
return "";
}
/**
* Adds lock expression to a query.
*/
protected createLockExpression(): string {
switch (this.expressionMap.lockMode) {
case "pessimistic_read":
@ -932,4 +873,17 @@ export abstract class QueryBuilder<Entity> {
return [whereString, parameters];
}
/**
* Gets name of the table where insert should be performed.
*/
protected getTableName(): string {
if (!this.expressionMap.mainAlias)
throw new Error(`Entity where values should be inserted is not specified. Call "qb.into(entity)" method to specify it.`);
if (this.expressionMap.mainAlias.hasMetadata)
return this.expressionMap.mainAlias.metadata.tableName;
return this.expressionMap.mainAlias.tableName!;
}
}

View File

@ -7,6 +7,17 @@ import {QueryBuilder} from "./QueryBuilder";
*/
export class RelationQueryBuilder<Entity> extends QueryBuilder<Entity> {
// -------------------------------------------------------------------------
// Public Implemented Methods
// -------------------------------------------------------------------------
/**
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
return "";
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------

View File

@ -24,6 +24,26 @@ import {ReadStream} from "fs";
*/
export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> {
// -------------------------------------------------------------------------
// Public Implemented Methods
// -------------------------------------------------------------------------
/**
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createSelectExpression();
sql += this.createJoinExpression();
sql += this.createWhereExpression();
sql += this.createGroupByExpression();
sql += this.createHavingExpression();
sql += this.createOrderByExpression();
sql += this.createLimitOffsetExpression();
sql += this.createLockExpression();
sql = this.createLimitOffsetOracleSpecificExpression(sql);
return sql.trim();
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
@ -509,15 +529,17 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> {
* Sets ORDER BY condition in the query builder.
* If you had previously ORDER BY expression defined,
* calling this function will override previously set ORDER BY conditions.
*
* Calling order by without order set will remove all previously set order bys.
*/
orderBy(sort: string, order?: "ASC"|"DESC"): this;
orderBy(): this;
/**
* Sets ORDER BY condition in the query builder.
* If you had previously ORDER BY expression defined,
* calling this function will override previously set ORDER BY conditions.
*/
orderBy(sort: undefined): this;
orderBy(sort: string, order?: "ASC"|"DESC"): this;
/**
* Sets ORDER BY condition in the query builder.
@ -829,7 +851,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> {
}).join(", ") + ") as \"cnt\"";
const countQueryBuilder = new SelectQueryBuilder(this)
.orderBy(undefined)
.orderBy()
.offset(undefined)
.limit(undefined)
.select(countSql);
@ -880,7 +902,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> {
if (this.expressionMap.skip || this.expressionMap.take) {
// we are skipping order by here because its not working in subqueries anyway
// to make order by working we need to apply it on a distinct query
const [sql, parameters] = this.getSqlAndParameters({ skipOrderBy: true });
const [sql, parameters] = this/*.clone().orderBy()*/.getSqlAndParameters();
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
const distinctAlias = this.escapeTable("distinctAlias");

View File

@ -6,6 +6,19 @@ import {ObjectLiteral} from "../common/ObjectLiteral";
*/
export class UpdateQueryBuilder<Entity> extends QueryBuilder<Entity> {
// -------------------------------------------------------------------------
// Public Implemented Methods
// -------------------------------------------------------------------------
/**
* Gets generated sql query without parameters being replaced.
*/
getQuery(): string {
let sql = this.createUpdateExpression();
sql += this.createWhereExpression();
return sql.trim();
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
@ -49,4 +62,41 @@ export class UpdateQueryBuilder<Entity> extends QueryBuilder<Entity> {
return this;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Creates UPDATE express used to perform insert query.
*/
protected createUpdateExpression() {
const valuesSet = this.getValueSets();
const updateColumnAndValues: string[] = [];
Object.keys(valuesSet).forEach(columnProperty => {
const column = this.expressionMap.mainAlias!.metadata.findColumnWithPropertyName(columnProperty);
if (column) {
const paramName = "_updated_" + column.databaseName;
this.setParameter(paramName, valuesSet[column.propertyName]);
updateColumnAndValues.push(this.escapeAlias(column.databaseName) + "=:" + paramName);
}
});
// get a table name and all column database names
const tableName = this.escapeTable(this.getTableName());
// generate and return sql update query
return `UPDATE ${tableName} SET ${updateColumnAndValues.join(", ")}`; // todo: how do we replace aliases in where to nothing?
}
/**
* Gets array of values need to be inserted into the target table.
*/
protected getValueSets(): ObjectLiteral {
if (this.expressionMap.valuesSet instanceof Object)
return this.expressionMap.valuesSet;
throw new Error(`Cannot perform update query because update values are not defined. Call "qb.set(...)" method to specify inserted values.`);
}
}