mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
parent
540ffffafc
commit
abf62fe753
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ node_modules/
|
||||
npm-debug.log
|
||||
ormconfig.json
|
||||
.vscode
|
||||
.idea/
|
||||
21
gulpfile.ts
21
gulpfile.ts
@ -1,3 +1,7 @@
|
||||
///<reference path="node_modules/@types/node/index.d.ts"/>
|
||||
///<reference path="node_modules/@types/chai/index.d.ts"/>
|
||||
///<reference path="node_modules/@types/mocha/index.d.ts"/>
|
||||
|
||||
import {Gulpclass, Task, SequenceTask, MergedTask} from "gulpclass";
|
||||
|
||||
const gulp = require("gulp");
|
||||
@ -301,15 +305,16 @@ export class Gulpfile {
|
||||
/**
|
||||
* Runs tests the quick way.
|
||||
*/
|
||||
@Task("ts-node-tests")
|
||||
@Task()
|
||||
quickTests() {
|
||||
chai.should();
|
||||
chai.use(require("sinon-chai"));
|
||||
chai.use(require("chai-as-promised"));
|
||||
|
||||
return gulp.src(["./test/**/*.ts"])
|
||||
return gulp.src(["./build/compiled/test/**/*.js"])
|
||||
.pipe(mocha({
|
||||
timeout: 10000
|
||||
bail: true,
|
||||
timeout: 15000
|
||||
}));
|
||||
}
|
||||
|
||||
@ -321,13 +326,21 @@ export class Gulpfile {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the code and runs tests.
|
||||
* Compiles the code and runs tests + makes coverage report.
|
||||
*/
|
||||
@SequenceTask()
|
||||
tests() {
|
||||
return ["compile", "tslint", "coveragePost", "coverageRemap"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the code and runs only mocha tests.
|
||||
*/
|
||||
@SequenceTask()
|
||||
mocha() {
|
||||
return ["compile", "quickTests"];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CI tasks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -16,6 +16,16 @@ import {PersistOptions} from "../repository/PersistOptions";
|
||||
*/
|
||||
export class EntityManager extends BaseEntityManager {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Stores temporarily user data.
|
||||
* Useful for sharing data with subscribers.
|
||||
*/
|
||||
private data: ObjectLiteral = {};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
@ -28,6 +38,20 @@ export class EntityManager extends BaseEntityManager {
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets user data by a given key.
|
||||
*/
|
||||
getData(key: string): any {
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets value for the given key in user data.
|
||||
*/
|
||||
setData(key: string, value: any) {
|
||||
this.data[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists (saves) all given entities in the database.
|
||||
* If entities do not exist in the database then inserts, otherwise updates.
|
||||
|
||||
@ -5,11 +5,9 @@ import {QueryRunner} from "../query-runner/QueryRunner";
|
||||
import {Subject, JunctionInsert, JunctionRemove} from "./Subject";
|
||||
import {OrmUtils} from "../util/OrmUtils";
|
||||
import {QueryRunnerProvider} from "../query-runner/QueryRunnerProvider";
|
||||
import {RelationMetadata} from "../metadata/RelationMetadata";
|
||||
import {EntityManager} from "../entity-manager/EntityManager";
|
||||
import {PromiseUtils} from "../util/PromiseUtils";
|
||||
import {MongoDriver} from "../driver/mongodb/MongoDriver";
|
||||
import {ColumnMetadata} from "../metadata/ColumnMetadata";
|
||||
import {EmbeddedMetadata} from "../metadata/EmbeddedMetadata";
|
||||
|
||||
/**
|
||||
@ -581,7 +579,7 @@ export class SubjectOperationExecutor {
|
||||
const value = this.connection.driver.preparePersistentValue(1, metadata.versionColumn);
|
||||
columnNames.push(metadata.versionColumn.fullName);
|
||||
columnValues.push(value);
|
||||
columnsAndValuesMap[metadata.updateDateColumn.fullName] = value;
|
||||
columnsAndValuesMap[metadata.versionColumn.fullName] = value;
|
||||
}
|
||||
|
||||
// add special column and value - discriminator value (for tables using table inheritance)
|
||||
@ -1053,7 +1051,7 @@ export class SubjectOperationExecutor {
|
||||
if (subject.metadata.hasCreateDateColumn)
|
||||
subject.entity[subject.metadata.createDateColumn.propertyName] = subject.date;
|
||||
if (subject.metadata.hasVersionColumn)
|
||||
subject.entity[subject.metadata.versionColumn.propertyName]++;
|
||||
subject.entity[subject.metadata.versionColumn.propertyName] = 1;
|
||||
if (subject.metadata.hasTreeLevelColumn) {
|
||||
// const parentEntity = insertOperation.entity[metadata.treeParentMetadata.propertyName];
|
||||
// const parentLevel = parentEntity ? (parentEntity[metadata.treeLevelColumn.propertyName] || 0) : 0;
|
||||
|
||||
@ -9,6 +9,13 @@ import {OrderByCondition} from "../find-options/OrderByCondition";
|
||||
import {Connection} from "../connection/Connection";
|
||||
import {JoinOptions} from "./JoinOptions";
|
||||
import {QueryRunnerProvider} from "../query-runner/QueryRunnerProvider";
|
||||
import {PessimisticLockTransactionRequiredError} from "./error/PessimisticLockTransactionRequiredError";
|
||||
import {NoVersionOrUpdateDateColumnError} from "./error/NoVersionOrUpdateDateColumnError";
|
||||
import {OptimisticLockVersionMismatchError} from "./error/OptimisticLockVersionMismatchError";
|
||||
import {OptimisticLockCanNotBeUsedError} from "./error/OptimisticLockCanNotBeUsedError";
|
||||
import {PostgresDriver} from "../driver/postgres/PostgresDriver";
|
||||
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
|
||||
import {LockNotSupportedOnGivenDriverError} from "./error/LockNotSupportedOnGivenDriverError";
|
||||
|
||||
/**
|
||||
*/
|
||||
@ -76,6 +83,8 @@ export class QueryBuilder<Entity> {
|
||||
protected parameters: ObjectLiteral = {};
|
||||
protected limit: number;
|
||||
protected offset: number;
|
||||
protected lockMode: "optimistic"|"pessimistic_read"|"pessimistic_write";
|
||||
protected lockVersion?: number|Date;
|
||||
protected skipNumber: number;
|
||||
protected takeNumber: number;
|
||||
protected ignoreParentTablesJoins: boolean = false;
|
||||
@ -221,6 +230,15 @@ export class QueryBuilder<Entity> {
|
||||
return this;
|
||||
}
|
||||
|
||||
setLock(lockMode: "optimistic", lockVersion: number): this;
|
||||
setLock(lockMode: "optimistic", lockVersion: Date): this;
|
||||
setLock(lockMode: "pessimistic_read"|"pessimistic_write"): this;
|
||||
setLock(lockMode: "optimistic"|"pessimistic_read"|"pessimistic_write", lockVersion?: number|Date): this {
|
||||
this.lockMode = lockMode;
|
||||
this.lockVersion = lockVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies FROM which entity's table select/update/delete will be executed.
|
||||
* Also sets a main string alias of the selection data.
|
||||
@ -841,6 +859,7 @@ export class QueryBuilder<Entity> {
|
||||
sql += this.createOrderByExpression();
|
||||
sql += this.createLimitExpression();
|
||||
sql += this.createOffsetExpression();
|
||||
sql += this.createLockExpression();
|
||||
[sql] = this.connection.driver.escapeQueryWithParameters(sql, this.parameters);
|
||||
return sql;
|
||||
}
|
||||
@ -860,6 +879,7 @@ export class QueryBuilder<Entity> {
|
||||
sql += this.createOrderByExpression();
|
||||
sql += this.createLimitExpression();
|
||||
sql += this.createOffsetExpression();
|
||||
sql += this.createLockExpression();
|
||||
return sql;
|
||||
}
|
||||
|
||||
@ -879,6 +899,7 @@ export class QueryBuilder<Entity> {
|
||||
sql += this.createOrderByExpression();
|
||||
sql += this.createLimitExpression();
|
||||
sql += this.createOffsetExpression();
|
||||
sql += this.createLockExpression();
|
||||
return this.connection.driver.escapeQueryWithParameters(sql, this.getParameters());
|
||||
}
|
||||
|
||||
@ -887,6 +908,7 @@ export class QueryBuilder<Entity> {
|
||||
*/
|
||||
async execute(): Promise<any> {
|
||||
const queryRunner = await this.getQueryRunner();
|
||||
|
||||
const [sql, parameters] = this.getSqlWithParameters();
|
||||
try {
|
||||
return await queryRunner.query(sql, parameters); // await is needed here because we are using finally
|
||||
@ -901,57 +923,66 @@ export class QueryBuilder<Entity> {
|
||||
* Executes sql generated by query builder and returns object with raw results and entities created from them.
|
||||
*/
|
||||
async getEntitiesAndRawResults(): Promise<{ entities: Entity[], rawResults: any[] }> {
|
||||
if (!this.aliasMap.hasMainAlias)
|
||||
throw new Error(`Alias is not set. Looks like nothing is selected. Use select*, delete, update method to set an alias.`);
|
||||
|
||||
const queryRunner = await this.getQueryRunner();
|
||||
|
||||
const mainAliasName = this.fromTableName ? this.fromTableName : this.aliasMap.mainAlias.name;
|
||||
let rawResults: any[];
|
||||
if (this.skipNumber || this.takeNumber) {
|
||||
// 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.getSqlWithParameters({ skipOrderBy: true });
|
||||
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
|
||||
try {
|
||||
if (!this.aliasMap.hasMainAlias)
|
||||
throw new Error(`Alias is not set. Looks like nothing is selected. Use select*, delete, update method to set an alias.`);
|
||||
|
||||
const distinctAlias = this.connection.driver.escapeTableName("distinctAlias");
|
||||
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
|
||||
let idsQuery = `SELECT `;
|
||||
idsQuery += metadata.primaryColumns.map((primaryColumn, index) => {
|
||||
const propertyName = this.connection.driver.escapeAliasName(mainAliasName + "_" + primaryColumn.fullName);
|
||||
if (index === 0) {
|
||||
return `DISTINCT(${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
|
||||
if ((this.lockMode === "pessimistic_read" || this.lockMode === "pessimistic_write") && !queryRunner.isTransactionActive())
|
||||
throw new PessimisticLockTransactionRequiredError();
|
||||
|
||||
if (this.lockMode === "optimistic") {
|
||||
const metadata = this.connection.getMetadata(this.aliasMap.mainAlias.target);
|
||||
if (!metadata.hasVersionColumn && !metadata.hasUpdateDateColumn)
|
||||
throw new NoVersionOrUpdateDateColumnError(metadata.name);
|
||||
}
|
||||
|
||||
const mainAliasName = this.fromTableName ? this.fromTableName : this.aliasMap.mainAlias.name;
|
||||
let rawResults: any[];
|
||||
if (this.skipNumber || this.takeNumber) {
|
||||
// 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.getSqlWithParameters({ skipOrderBy: true });
|
||||
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
|
||||
|
||||
const distinctAlias = this.connection.driver.escapeTableName("distinctAlias");
|
||||
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
|
||||
let idsQuery = `SELECT `;
|
||||
idsQuery += metadata.primaryColumns.map((primaryColumn, index) => {
|
||||
const propertyName = this.connection.driver.escapeAliasName(mainAliasName + "_" + primaryColumn.fullName);
|
||||
if (index === 0) {
|
||||
return `DISTINCT(${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
|
||||
} else {
|
||||
return `${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
|
||||
}
|
||||
}).join(", ");
|
||||
if (selects.length > 0)
|
||||
idsQuery += ", " + selects;
|
||||
|
||||
idsQuery += ` FROM (${sql}) ${distinctAlias}`; // TODO: WHAT TO DO WITH PARAMETERS HERE? DO THEY WORK?
|
||||
|
||||
if (orderBys.length > 0) {
|
||||
idsQuery += " ORDER BY " + orderBys;
|
||||
} else {
|
||||
return `${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
|
||||
idsQuery += ` ORDER BY "ids_${metadata.firstPrimaryColumn.fullName}"`; // this is required for mssql driver if firstResult is used. Other drivers don't care about it
|
||||
}
|
||||
}).join(", ");
|
||||
if (selects.length > 0)
|
||||
idsQuery += ", " + selects;
|
||||
|
||||
idsQuery += ` FROM (${sql}) ${distinctAlias}`; // TODO: WHAT TO DO WITH PARAMETERS HERE? DO THEY WORK?
|
||||
if (this.connection.driver instanceof SqlServerDriver) { // todo: temporary. need to refactor and make a proper abstraction
|
||||
|
||||
if (orderBys.length > 0) {
|
||||
idsQuery += " ORDER BY " + orderBys;
|
||||
} else {
|
||||
idsQuery += ` ORDER BY "ids_${metadata.firstPrimaryColumn.fullName}"`; // this is required for mssql driver if firstResult is used. Other drivers don't care about it
|
||||
}
|
||||
if (this.skipNumber || this.takeNumber) {
|
||||
idsQuery += ` OFFSET ${this.skipNumber || 0} ROWS`;
|
||||
if (this.takeNumber)
|
||||
idsQuery += " FETCH NEXT " + this.takeNumber + " ROWS ONLY";
|
||||
}
|
||||
} else {
|
||||
|
||||
if (this.connection.driver instanceof SqlServerDriver) { // todo: temporary. need to refactor and make a proper abstraction
|
||||
|
||||
if (this.skipNumber || this.takeNumber) {
|
||||
idsQuery += ` OFFSET ${this.skipNumber || 0} ROWS`;
|
||||
if (this.takeNumber)
|
||||
idsQuery += " FETCH NEXT " + this.takeNumber + " ROWS ONLY";
|
||||
idsQuery += " LIMIT " + this.takeNumber;
|
||||
if (this.skipNumber)
|
||||
idsQuery += " OFFSET " + this.skipNumber;
|
||||
}
|
||||
} else {
|
||||
|
||||
if (this.takeNumber)
|
||||
idsQuery += " LIMIT " + this.takeNumber;
|
||||
if (this.skipNumber)
|
||||
idsQuery += " OFFSET " + this.skipNumber;
|
||||
}
|
||||
|
||||
try {
|
||||
return await queryRunner.query(idsQuery, parameters)
|
||||
.then((results: any[]) => {
|
||||
rawResults = results;
|
||||
@ -1007,17 +1038,10 @@ export class QueryBuilder<Entity> {
|
||||
};
|
||||
});
|
||||
|
||||
} finally {
|
||||
if (this.hasOwnQueryRunner()) // means we created our own query runner
|
||||
await queryRunner.release();
|
||||
}
|
||||
} else {
|
||||
|
||||
} else {
|
||||
const [sql, parameters] = this.getSqlWithParameters();
|
||||
|
||||
const [sql, parameters] = this.getSqlWithParameters();
|
||||
|
||||
try {
|
||||
// console.log(sql);
|
||||
return await queryRunner.query(sql, parameters)
|
||||
.then(results => {
|
||||
rawResults = results;
|
||||
@ -1045,11 +1069,11 @@ export class QueryBuilder<Entity> {
|
||||
rawResults: rawResults
|
||||
};
|
||||
});
|
||||
|
||||
} finally {
|
||||
if (this.hasOwnQueryRunner()) // means we created our own query runner
|
||||
await queryRunner.release();
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (this.hasOwnQueryRunner()) // means we created our own query runner
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1058,6 +1082,8 @@ export class QueryBuilder<Entity> {
|
||||
* Count excludes all limitations set by setFirstResult and setMaxResults methods call.
|
||||
*/
|
||||
async getCount(): Promise<number> {
|
||||
if (this.lockMode === "optimistic")
|
||||
throw new OptimisticLockCanNotBeUsedError();
|
||||
|
||||
const queryRunner = await this.getQueryRunner();
|
||||
|
||||
@ -1102,24 +1128,34 @@ export class QueryBuilder<Entity> {
|
||||
/**
|
||||
* Gets all raw results returned by execution of generated query builder sql.
|
||||
*/
|
||||
getRawMany(): Promise<any[]> { // todo: rename to getRawMany
|
||||
async getRawMany(): Promise<any[]> {
|
||||
if (this.lockMode === "optimistic")
|
||||
throw new OptimisticLockCanNotBeUsedError();
|
||||
|
||||
return this.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first raw result returned by execution of generated query builder sql.
|
||||
*/
|
||||
getRawOne(): Promise<any> { // todo: rename to getRawOne
|
||||
return this.getRawMany().then(results => results[0]);
|
||||
async getRawOne(): Promise<any> {
|
||||
if (this.lockMode === "optimistic")
|
||||
throw new OptimisticLockCanNotBeUsedError();
|
||||
|
||||
const results = await this.execute();
|
||||
return results[0];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets entities and count returned by execution of generated query builder sql.
|
||||
*/
|
||||
getManyAndCount(): Promise<[Entity[], number]> {
|
||||
async getManyAndCount(): Promise<[Entity[], number]> {
|
||||
if (this.lockMode === "optimistic")
|
||||
throw new OptimisticLockCanNotBeUsedError();
|
||||
|
||||
// todo: share database connection and counter
|
||||
return Promise.all<any>([
|
||||
return Promise.all([
|
||||
this.getMany(),
|
||||
this.getCount()
|
||||
]);
|
||||
@ -1128,17 +1164,38 @@ export class QueryBuilder<Entity> {
|
||||
/**
|
||||
* Gets entities returned by execution of generated query builder sql.
|
||||
*/
|
||||
getMany(): Promise<Entity[]> {
|
||||
return this.getEntitiesAndRawResults().then(results => {
|
||||
return results.entities;
|
||||
});
|
||||
async getMany(): Promise<Entity[]> {
|
||||
if (this.lockMode === "optimistic")
|
||||
throw new OptimisticLockCanNotBeUsedError();
|
||||
|
||||
const results = await this.getEntitiesAndRawResults();
|
||||
return results.entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets single entity returned by execution of generated query builder sql.
|
||||
*/
|
||||
getOne(): Promise<Entity|undefined> {
|
||||
return this.getMany().then(entities => entities[0]);
|
||||
async getOne(): Promise<Entity|undefined> {
|
||||
const results = await this.getEntitiesAndRawResults();
|
||||
const result = results.entities[0] as any;
|
||||
|
||||
if (result && this.lockMode === "optimistic" && this.lockVersion) {
|
||||
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
|
||||
|
||||
if (this.lockVersion instanceof Date) {
|
||||
const actualVersion = result[metadata.updateDateColumn.propertyName];
|
||||
this.lockVersion.setMilliseconds(0);
|
||||
if (actualVersion.getTime() !== this.lockVersion.getTime())
|
||||
throw new OptimisticLockVersionMismatchError(metadata.name, this.lockVersion, actualVersion);
|
||||
|
||||
} else {
|
||||
const actualVersion = result[metadata.versionColumn.propertyName];
|
||||
if (actualVersion !== this.lockVersion)
|
||||
throw new OptimisticLockVersionMismatchError(metadata.name, this.lockVersion, actualVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1447,10 +1504,22 @@ export class QueryBuilder<Entity> {
|
||||
if (allSelects.length === 0)
|
||||
allSelects.push("*");
|
||||
|
||||
let lock: string = "";
|
||||
if (this.connection.driver instanceof SqlServerDriver) {
|
||||
switch (this.lockMode) {
|
||||
case "pessimistic_read":
|
||||
lock = " WITH (HOLDLOCK, ROWLOCK)";
|
||||
break;
|
||||
case "pessimistic_write":
|
||||
lock = " WITH (UPDLOCK, ROWLOCK)";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// create a selection query
|
||||
switch (this.type) {
|
||||
case "select":
|
||||
return "SELECT " + allSelects.join(", ") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + this.connection.driver.escapeAliasName(alias);
|
||||
return "SELECT " + allSelects.join(", ") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + this.connection.driver.escapeAliasName(alias) + lock;
|
||||
case "delete":
|
||||
return "DELETE FROM " + this.connection.driver.escapeTableName(tableName);
|
||||
// return "DELETE " + (alias ? this.connection.driver.escapeAliasName(alias) : "") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + (alias ? this.connection.driver.escapeAliasName(alias) : ""); // 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
|
||||
@ -1730,16 +1799,46 @@ export class QueryBuilder<Entity> {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected createLimitExpression() {
|
||||
protected createLimitExpression(): string {
|
||||
if (!this.limit) return "";
|
||||
return " LIMIT " + this.limit;
|
||||
}
|
||||
|
||||
protected createOffsetExpression() {
|
||||
protected createOffsetExpression(): string {
|
||||
if (!this.offset) return "";
|
||||
return " OFFSET " + this.offset;
|
||||
}
|
||||
|
||||
protected createLockExpression(): string {
|
||||
switch (this.lockMode) {
|
||||
case "pessimistic_read":
|
||||
if (this.connection.driver instanceof MysqlDriver) {
|
||||
return " LOCK IN SHARE MODE";
|
||||
|
||||
} else if (this.connection.driver instanceof PostgresDriver) {
|
||||
return " FOR SHARE";
|
||||
|
||||
} else if (this.connection.driver instanceof SqlServerDriver) {
|
||||
return "";
|
||||
|
||||
} else {
|
||||
throw new LockNotSupportedOnGivenDriverError();
|
||||
}
|
||||
case "pessimistic_write":
|
||||
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof PostgresDriver) {
|
||||
return " FOR UPDATE";
|
||||
|
||||
} else if (this.connection.driver instanceof SqlServerDriver) {
|
||||
return "";
|
||||
|
||||
} else {
|
||||
throw new LockNotSupportedOnGivenDriverError();
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private extractJoinMappings(): JoinMapping[] {
|
||||
const mappings: JoinMapping[] = [];
|
||||
this.joins
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Thrown when selected sql driver does not supports locking.
|
||||
*/
|
||||
export class LockNotSupportedOnGivenDriverError extends Error {
|
||||
name = "LockNotSupportedOnGivenDriverError";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
Object.setPrototypeOf(this, LockNotSupportedOnGivenDriverError.prototype);
|
||||
this.message = `Locking not supported on giver driver.`;
|
||||
}
|
||||
|
||||
}
|
||||
13
src/query-builder/error/NoVersionOrUpdateDateColumnError.ts
Normal file
13
src/query-builder/error/NoVersionOrUpdateDateColumnError.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Thrown when an entity does not have no version and no update date column.
|
||||
*/
|
||||
export class NoVersionOrUpdateDateColumnError extends Error {
|
||||
name = "NoVersionOrUpdateDateColumnError";
|
||||
|
||||
constructor(entity: string) {
|
||||
super();
|
||||
Object.setPrototypeOf(this, NoVersionOrUpdateDateColumnError.prototype);
|
||||
this.message = `Entity ${entity} does not have version or update date columns.`;
|
||||
}
|
||||
|
||||
}
|
||||
13
src/query-builder/error/OptimisticLockCanNotBeUsedError.ts
Normal file
13
src/query-builder/error/OptimisticLockCanNotBeUsedError.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Thrown when an optimistic lock cannot be used in query builder.
|
||||
*/
|
||||
export class OptimisticLockCanNotBeUsedError extends Error {
|
||||
name = "OptimisticLockCanNotBeUsedError";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
Object.setPrototypeOf(this, OptimisticLockCanNotBeUsedError.prototype);
|
||||
this.message = `The optimistic lock can be used only with getOne() method.`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Thrown when a version check on an object that uses optimistic locking through a version field fails.
|
||||
*/
|
||||
export class OptimisticLockVersionMismatchError extends Error {
|
||||
name = "OptimisticLockVersionMismatchError";
|
||||
|
||||
constructor(entity: string, expectedVersion: number|Date, actualVersion: number|Date) {
|
||||
super();
|
||||
Object.setPrototypeOf(this, OptimisticLockVersionMismatchError.prototype);
|
||||
this.message = `The optimistic lock on entity ${entity} failed, version ${expectedVersion} was expected, but is actually ${actualVersion}.`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Thrown when a transaction is required for the current operation, but there is none open.
|
||||
*/
|
||||
export class PessimisticLockTransactionRequiredError extends Error {
|
||||
name = "PessimisticLockTransactionRequiredError";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
Object.setPrototypeOf(this, PessimisticLockTransactionRequiredError.prototype);
|
||||
this.message = `An open transaction is required for pessimistic lock.`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -163,6 +163,7 @@ export class Repository<Entity extends ObjectLiteral> {
|
||||
const queryRunnerProvider = this.queryRunnerProvider || new QueryRunnerProvider(this.connection.driver, true);
|
||||
try {
|
||||
const transactionEntityManager = this.connection.createEntityManagerWithSingleDatabaseConnection(queryRunnerProvider);
|
||||
// transactionEntityManager.data =
|
||||
|
||||
const databaseEntityLoader = new SubjectBuilder(this.connection, queryRunnerProvider);
|
||||
await databaseEntityLoader.persist(entityOrEntities, this.metadata);
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
import {UpdateDateColumn} from "../../../../../src/decorator/columns/UpdateDateColumn";
|
||||
|
||||
@Entity()
|
||||
export class PostWithUpdateDate {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updateDate: Date;
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
|
||||
|
||||
@Entity()
|
||||
export class PostWithVersion {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@VersionColumn()
|
||||
version: number;
|
||||
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
|
||||
import {UpdateDateColumn} from "../../../../../src/decorator/columns/UpdateDateColumn";
|
||||
|
||||
@Entity()
|
||||
export class PostWithVersionAndUpdatedDate {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@VersionColumn()
|
||||
version: number;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updateDate: Date;
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
|
||||
@Entity()
|
||||
export class PostWithoutVersionAndUpdateDate {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
}
|
||||
280
test/functional/query-builder/locking/query-builder-locking.ts
Normal file
280
test/functional/query-builder/locking/query-builder-locking.ts
Normal file
@ -0,0 +1,280 @@
|
||||
import "reflect-metadata";
|
||||
import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
|
||||
import {Connection} from "../../../../src/connection/Connection";
|
||||
import {PostWithVersion} from "./entity/PostWithVersion";
|
||||
import {expect} from "chai";
|
||||
import {PostWithoutVersionAndUpdateDate} from "./entity/PostWithoutVersionAndUpdateDate";
|
||||
import {PostWithUpdateDate} from "./entity/PostWithUpdateDate";
|
||||
import {PostWithVersionAndUpdatedDate} from "./entity/PostWithVersionAndUpdatedDate";
|
||||
import {OptimisticLockVersionMismatchError} from "../../../../src/query-builder/error/OptimisticLockVersionMismatchError";
|
||||
import {OptimisticLockCanNotBeUsedError} from "../../../../src/query-builder/error/OptimisticLockCanNotBeUsedError";
|
||||
import {NoVersionOrUpdateDateColumnError} from "../../../../src/query-builder/error/NoVersionOrUpdateDateColumnError";
|
||||
import {PessimisticLockTransactionRequiredError} from "../../../../src/query-builder/error/PessimisticLockTransactionRequiredError";
|
||||
import {MysqlDriver} from "../../../../src/driver/mysql/MysqlDriver";
|
||||
import {PostgresDriver} from "../../../../src/driver/postgres/PostgresDriver";
|
||||
import {SqlServerDriver} from "../../../../src/driver/sqlserver/SqlServerDriver";
|
||||
import {SqliteDriver} from "../../../../src/driver/sqlite/SqliteDriver";
|
||||
import {OracleDriver} from "../../../../src/driver/oracle/OracleDriver";
|
||||
import {LockNotSupportedOnGivenDriverError} from "../../../../src/query-builder/error/LockNotSupportedOnGivenDriverError";
|
||||
|
||||
describe.only("query builder > locking", () => {
|
||||
|
||||
let connections: Connection[];
|
||||
before(async () => connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
schemaCreate: true,
|
||||
dropSchemaOnConnection: true,
|
||||
}));
|
||||
beforeEach(() => reloadTestingDatabases(connections));
|
||||
after(() => closeTestingConnections(connections));
|
||||
|
||||
it("should not attach pessimistic read lock statement on query if locking is not used", () => Promise.all(connections.map(async connection => {
|
||||
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
|
||||
return;
|
||||
|
||||
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getSql();
|
||||
|
||||
if (connection.driver instanceof MysqlDriver) {
|
||||
expect(sql.indexOf("LOCK IN SHARE MODE") === -1).to.be.true;
|
||||
|
||||
} else if (connection.driver instanceof PostgresDriver) {
|
||||
expect(sql.indexOf("FOR SHARE") === -1).to.be.true;
|
||||
|
||||
} else if (connection.driver instanceof SqlServerDriver) {
|
||||
expect(sql.indexOf("WITH (HOLDLOCK, ROWLOCK)") === -1).to.be.true;
|
||||
}
|
||||
})));
|
||||
|
||||
it("should throw error if pessimistic lock used without transaction", () => Promise.all(connections.map(async connection => {
|
||||
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
|
||||
return;
|
||||
|
||||
return Promise.all([
|
||||
connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_read")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError),
|
||||
|
||||
connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_write")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.be.rejectedWith(PessimisticLockTransactionRequiredError)
|
||||
]);
|
||||
})));
|
||||
|
||||
it("should not throw error if pessimistic lock used with transaction", () => Promise.all(connections.map(async connection => {
|
||||
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
|
||||
return;
|
||||
|
||||
return connection.entityManager.transaction(entityManager => {
|
||||
return Promise.all([
|
||||
entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_read")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.not.be.rejected,
|
||||
|
||||
entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_write")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.not.be.rejected
|
||||
]);
|
||||
});
|
||||
})));
|
||||
|
||||
it("should attach pessimistic read lock statement on query if locking enabled", () => Promise.all(connections.map(async connection => {
|
||||
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
|
||||
return;
|
||||
|
||||
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_read")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getSql();
|
||||
|
||||
if (connection.driver instanceof MysqlDriver) {
|
||||
expect(sql.indexOf("LOCK IN SHARE MODE") !== -1).to.be.true;
|
||||
|
||||
} else if (connection.driver instanceof PostgresDriver) {
|
||||
expect(sql.indexOf("FOR SHARE") !== -1).to.be.true;
|
||||
|
||||
} else if (connection.driver instanceof SqlServerDriver) {
|
||||
expect(sql.indexOf("WITH (HOLDLOCK, ROWLOCK)") !== -1).to.be.true;
|
||||
}
|
||||
})));
|
||||
|
||||
it("should not attach pessimistic write lock statement on query if locking is not used", () => Promise.all(connections.map(async connection => {
|
||||
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
|
||||
return;
|
||||
|
||||
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getSql();
|
||||
|
||||
if (connection.driver instanceof MysqlDriver || connection.driver instanceof PostgresDriver) {
|
||||
expect(sql.indexOf("FOR UPDATE") === -1).to.be.true;
|
||||
|
||||
} else if (connection.driver instanceof SqlServerDriver) {
|
||||
expect(sql.indexOf("WITH (UPDLOCK, ROWLOCK)") === -1).to.be.true;
|
||||
}
|
||||
})));
|
||||
|
||||
it("should attach pessimistic write lock statement on query if locking enabled", () => Promise.all(connections.map(async connection => {
|
||||
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
|
||||
return;
|
||||
|
||||
const sql = connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_write")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getSql();
|
||||
|
||||
if (connection.driver instanceof MysqlDriver || connection.driver instanceof PostgresDriver) {
|
||||
expect(sql.indexOf("FOR UPDATE") !== -1).to.be.true;
|
||||
|
||||
} else if (connection.driver instanceof SqlServerDriver) {
|
||||
expect(sql.indexOf("WITH (UPDLOCK, ROWLOCK)") !== -1).to.be.true;
|
||||
}
|
||||
|
||||
})));
|
||||
|
||||
it("should throw error if optimistic lock used with getMany method", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.getMany().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
|
||||
})));
|
||||
|
||||
it("should throw error if optimistic lock used with getCount method", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.getCount().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
|
||||
})));
|
||||
|
||||
it("should throw error if optimistic lock used with getManyAndCount method", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.getManyAndCount().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
|
||||
})));
|
||||
|
||||
it("should throw error if optimistic lock used with getRawMany method", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.getRawMany().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
|
||||
})));
|
||||
|
||||
it("should throw error if optimistic lock used with getRawOne method", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getRawOne().should.be.rejectedWith(OptimisticLockCanNotBeUsedError);
|
||||
})));
|
||||
|
||||
it("should not throw error if optimistic lock used with getOne method", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.not.be.rejected;
|
||||
})));
|
||||
|
||||
it("should throw error if entity does not have version and update date columns", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new PostWithoutVersionAndUpdateDate();
|
||||
post.title = "New post";
|
||||
await connection.entityManager.persist(post);
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithoutVersionAndUpdateDate, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.be.rejectedWith(NoVersionOrUpdateDateColumnError);
|
||||
})));
|
||||
|
||||
it("should throw error if actual version does not equal expected version", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new PostWithVersion();
|
||||
post.title = "New post";
|
||||
await connection.entityManager.persist(post);
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 2)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.be.rejectedWith(OptimisticLockVersionMismatchError);
|
||||
})));
|
||||
|
||||
it("should not throw error if actual version and expected versions are equal", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new PostWithVersion();
|
||||
post.title = "New post";
|
||||
await connection.entityManager.persist(post);
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.not.be.rejected;
|
||||
})));
|
||||
|
||||
it("should throw error if actual updated date does not equal expected updated date", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new PostWithUpdateDate();
|
||||
post.title = "New post";
|
||||
await connection.entityManager.persist(post);
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithUpdateDate, "post")
|
||||
.setLock("optimistic", new Date(2017, 1, 1))
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.be.rejectedWith(OptimisticLockVersionMismatchError);
|
||||
})));
|
||||
|
||||
it("should not throw error if actual updated date and expected updated date are equal", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new PostWithUpdateDate();
|
||||
post.title = "New post";
|
||||
await connection.entityManager.persist(post);
|
||||
|
||||
return connection.entityManager.createQueryBuilder(PostWithUpdateDate, "post")
|
||||
.setLock("optimistic", post.updateDate)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.not.be.rejected;
|
||||
})));
|
||||
|
||||
it("should work if both version and update date columns applied", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new PostWithVersionAndUpdatedDate();
|
||||
post.title = "New post";
|
||||
await connection.entityManager.persist(post);
|
||||
|
||||
return Promise.all([
|
||||
connection.entityManager.createQueryBuilder(PostWithVersionAndUpdatedDate, "post")
|
||||
.setLock("optimistic", post.updateDate)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.not.be.rejected,
|
||||
|
||||
connection.entityManager.createQueryBuilder(PostWithVersionAndUpdatedDate, "post")
|
||||
.setLock("optimistic", 1)
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.not.be.rejected
|
||||
]);
|
||||
})));
|
||||
|
||||
it("should throw error if pessimistic locking not supported by given driver", () => Promise.all(connections.map(async connection => {
|
||||
if (connection.driver instanceof SqliteDriver || connection.driver instanceof OracleDriver)
|
||||
return connection.entityManager.transaction(entityManager => {
|
||||
return Promise.all([
|
||||
entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_read")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.be.rejectedWith(LockNotSupportedOnGivenDriverError),
|
||||
|
||||
entityManager.createQueryBuilder(PostWithVersion, "post")
|
||||
.setLock("pessimistic_write")
|
||||
.where("post.id = :id", { id: 1 })
|
||||
.getOne().should.be.rejectedWith(LockNotSupportedOnGivenDriverError)
|
||||
]);
|
||||
});
|
||||
})));
|
||||
|
||||
});
|
||||
@ -42,6 +42,11 @@ export interface TestingOptions {
|
||||
*/
|
||||
entities?: string[]|Function[];
|
||||
|
||||
/**
|
||||
* Subscribers needs to be included in the connection for the given test suite.
|
||||
*/
|
||||
subscribers?: string[]|Function[];
|
||||
|
||||
/**
|
||||
* Entity schemas needs to be included in the connection for the given test suite.
|
||||
*/
|
||||
@ -73,6 +78,7 @@ export function setupSingleTestingConnection(driverType: DriverType, options: Te
|
||||
const testingConnections = setupTestingConnections({
|
||||
name: options.name ? options.name : undefined,
|
||||
entities: options.entities ? options.entities : [],
|
||||
subscribers: options.subscribers ? options.subscribers : [],
|
||||
entitySchemas: options.entitySchemas ? options.entitySchemas : [],
|
||||
dropSchemaOnConnection: options.dropSchemaOnConnection ? options.dropSchemaOnConnection : false,
|
||||
schemaCreate: options.schemaCreate ? options.schemaCreate : false,
|
||||
@ -133,6 +139,7 @@ export function setupTestingConnections(options?: TestingOptions) {
|
||||
const newConnectionOptions = Object.assign({}, connectionOptions as ConnectionOptions, {
|
||||
name: options && options.name ? options.name : connectionOptions.name,
|
||||
entities: options && options.entities ? options.entities : [],
|
||||
subscribers: options && options.subscribers ? options.subscribers : [],
|
||||
entitySchemas: options && options.entitySchemas ? options.entitySchemas : [],
|
||||
autoSchemaSync: options && options.entities ? options.schemaCreate : false,
|
||||
dropSchemaOnConnection: options && options.entities ? options.dropSchemaOnConnection : false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user