Skip to content

Commit

Permalink
feat: add WITH (lock) clause for MSSQL select with join queries (#8507)
Browse files Browse the repository at this point in the history
* fix: add lock clause for MSSQL select with join clause

typeorm didn't supported LOCK clause in SELECT + JOIN query. For example, we cannot buld SQL such as "SELECT * FROM USER U WITH(NOLOCK) INNER JOIN ORDER WITH(NOLOCK) O ON U.ID=O.UserID". This pull request enables LOCK with SELECT + JOIN sql query.

Closes: #4764

* chore: add test cases

* chore: refactor method name, miscellaneous changes on createTableLockExpression
  • Loading branch information
icecreamparlor committed Feb 12, 2022
1 parent c490319 commit 3284808
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 21 deletions.
50 changes: 29 additions & 21 deletions src/query-builder/SelectQueryBuilder.ts
Expand Up @@ -1437,21 +1437,6 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
if (allSelects.length === 0)
allSelects.push({ selection: "*" });

let lock: string = "";
if (this.connection.driver instanceof SqlServerDriver) {
switch (this.expressionMap.lockMode) {
case "pessimistic_read":
lock = " WITH (HOLDLOCK, ROWLOCK)";
break;
case "pessimistic_write":
lock = " WITH (UPDLOCK, ROWLOCK)";
break;
case "dirty_read":
lock = " WITH (NOLOCK)";
break;
}
}

// Use certain index
let useIndex: string = "";
if (this.expressionMap.useIndex) {
Expand All @@ -1473,7 +1458,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
const select = this.createSelectDistinctExpression();
const selection = allSelects.map(select => select.selection + (select.aliasName ? " AS " + this.escape(select.aliasName) : "")).join(", ");

return select + selection + " FROM " + froms.join(", ") + lock + useIndex;
return select + selection + " FROM " + froms.join(", ") + this.createTableLockExpression() + useIndex;
}

/**
Expand Down Expand Up @@ -1528,7 +1513,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
// table to join, without junction table involved. This means we simply join direct table.
if (!parentAlias || !relation) {
const destinationJoin = joinAttr.alias.subQuery ? joinAttr.alias.subQuery : this.getTableName(destinationTableName);
return " " + joinAttr.direction + " JOIN " + destinationJoin + " " + this.escape(destinationTableAlias) +
return " " + joinAttr.direction + " JOIN " + destinationJoin + " " + this.escape(destinationTableAlias) + this.createTableLockExpression() +
(joinAttr.condition ? " ON " + this.replacePropertyNames(joinAttr.condition) : "");
}

Expand All @@ -1541,7 +1526,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
parentAlias + "." + relation.propertyPath + "." + joinColumn.referencedColumn!.propertyPath;
}).join(" AND ");

return " " + joinAttr.direction + " JOIN " + this.getTableName(destinationTableName) + " " + this.escape(destinationTableAlias) + " ON " + this.replacePropertyNames(condition + appendedCondition);
return " " + joinAttr.direction + " JOIN " + this.getTableName(destinationTableName) + " " + this.escape(destinationTableAlias) + this.createTableLockExpression() + " ON " + this.replacePropertyNames(condition + appendedCondition);

} else if (relation.isOneToMany || relation.isOneToOneNotOwner) {

Expand All @@ -1555,7 +1540,7 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
parentAlias + "." + joinColumn.referencedColumn!.propertyPath;
}).join(" AND ");

return " " + joinAttr.direction + " JOIN " + this.getTableName(destinationTableName) + " " + this.escape(destinationTableAlias) + " ON " + this.replacePropertyNames(condition + appendedCondition);
return " " + joinAttr.direction + " JOIN " + this.getTableName(destinationTableName) + " " + this.escape(destinationTableAlias) + this.createTableLockExpression() + " ON " + this.replacePropertyNames(condition + appendedCondition);

} else { // means many-to-many
const junctionTableName = relation.junctionEntityMetadata!.tablePath;
Expand Down Expand Up @@ -1587,8 +1572,8 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
}).join(" AND ");
}

return " " + joinAttr.direction + " JOIN " + this.getTableName(junctionTableName) + " " + this.escape(junctionAlias) + " ON " + this.replacePropertyNames(junctionCondition) +
" " + joinAttr.direction + " JOIN " + this.getTableName(destinationTableName) + " " + this.escape(destinationTableAlias) + " ON " + this.replacePropertyNames(destinationCondition + appendedCondition);
return " " + joinAttr.direction + " JOIN " + this.getTableName(junctionTableName) + " " + this.escape(junctionAlias) + this.createTableLockExpression() + " ON " + this.replacePropertyNames(junctionCondition) +
" " + joinAttr.direction + " JOIN " + this.getTableName(destinationTableName) + " " + this.escape(destinationTableAlias) + this.createTableLockExpression() + " ON " + this.replacePropertyNames(destinationCondition + appendedCondition);

}
});
Expand Down Expand Up @@ -1693,6 +1678,29 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> implements
return "";
}

/**
* Creates "LOCK" part of SELECT Query after table Clause
* ex.
* SELECT 1
* FROM USER U WITH (NOLOCK)
* JOIN ORDER O WITH (NOLOCK)
* ON U.ID=O.OrderID
*/
private createTableLockExpression(): string {
if(this.connection.driver instanceof SqlServerDriver) {
switch (this.expressionMap.lockMode) {
case "pessimistic_read":
return " WITH (HOLDLOCK, ROWLOCK)";
case "pessimistic_write":
return " WITH (UPDLOCK, ROWLOCK)";
case "dirty_read":
return " WITH (NOLOCK)";
}
}

return "";
}

/**
* Creates "LOCK" part of SQL query.
*/
Expand Down
41 changes: 41 additions & 0 deletions test/github-issues/4764/entity/Cart.ts
@@ -0,0 +1,41 @@
import {
Column,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from "../../../../src";
import { CartItems } from "./CartItems";
import { User } from "./User";

@Entity()
export class Cart {
@PrimaryGeneratedColumn()
ID!: number;

@Column()
UNID!: number;

@Column()
Type!: string;

@Column()
Cycle?: number;

@Column()
Term?: string;

@Column()
RegDate!: Date;

@Column()
ModifiedDate!: Date;

@OneToMany((type) => CartItems, (t) => t.Cart)
CartItems?: CartItems[];

@OneToOne((type) => User, (t) => t.Cart)
@JoinColumn({ name: "UNID" })
User?: User;
}
30 changes: 30 additions & 0 deletions test/github-issues/4764/entity/CartItems.ts
@@ -0,0 +1,30 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "../../../../src";
import { Cart } from "./Cart";

@Entity()
export class CartItems {
@PrimaryGeneratedColumn()
ID!: number;

@Column()
CartID!: number;

@Column()
ItemID!: number;

@Column()
OptionID!: number;

@Column()
Quantity!: number;

@Column()
RegDate!: Date;

@Column()
ModifiedDate!: Date;

@ManyToOne((type) => Cart, (t) => t.CartItems)
@JoinColumn({ name: "CartID" })
Cart?: Cart;
}
25 changes: 25 additions & 0 deletions test/github-issues/4764/entity/User.ts
@@ -0,0 +1,25 @@
import {
Column,
Entity,
OneToOne,
PrimaryGeneratedColumn,
} from "../../../../src";
import { Cart } from "./Cart";

@Entity()
export class User {
@PrimaryGeneratedColumn()
ID!: number;

@Column()
name!: string;

@Column()
RegDate!: Date;

@Column()
ModifiedDate!: Date;

@OneToOne((type) => Cart, (t) => t.User)
Cart?: Cart;
}

0 comments on commit 3284808

Please sign in to comment.