linqtodb 在 asp.net 内核中使用 connection.BeginTransactionAsync
linqtodb use connection.BeginTransactionAsync inside asp.net core
我在 asp.net 6.0 api 中使用 linqtodb 取得了巨大成功。
但现在我正处于一个看起来我需要使用交易的地步,看起来我在那里误解了一些事情。
我正在获取 _connection 对象作为服务中的注入对象
我得到的错误:
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action\u00601 wrapCloseInAction)\r\n at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action\u00601 wrapCloseInAction)\r\n at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock
...
...
"message":"Transaction (Process ID 56) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction."
有问题的代码是:
...
await _connection.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
sql = "SELECT ISNULL(MAX(CAST(Code AS INT)),0) FROM [@COR_DIA_AKM_LOTAT]";
var maxCode = await _connection.ExecuteAsync<int>(sql) + 1;
string newCode = maxCode.ToString("00000000");
sql = "INSERT INTO [@COR_DIA_AKM_LOTAT] (Code, Name, U_DocEntry, U_LineNum, U_LotNum, U_LotQty, U_ItemCode, U_Typ, U_DeletedF, U_LotCode) VALUES (@newCode, @newCode, @docEntry, @lineNum, @lot, @qty, @itemCode, 'F', 'N', '')";
await Task.Delay(10000);
await _connection.ExecuteAsync(sql,
new DataParameter { Name = "@newCode", Value = newCode },
new DataParameter { Name = "@qty", Value = l.qty },
new DataParameter { Name = "@docEntry", Value = updatePos.docEntry },
new DataParameter { Name = "@lineNum", Value = updatePos.lineNum },
new DataParameter { Name = "@itemCode", Value = updatePos.itemCode },
new DataParameter { Name = "@lot", Value = l.lot }
);
await _connection.CommitTransactionAsync();
...
因此您可能会看到需要先创建一个增量字母数字 ID(结构已给出,我无法更改)
然后我会在插页中使用它。
所以我需要确保以上部分的并发使用会等待彼此完成
await Task.Delay(...) 就在那里,所以我可以测试并发使用
当我现在从 2 个不同的客户端执行此代码时,来自第二个客户端的第二次调用失败并显示上述消息
我考虑过但不适用的事情:
- 使用存储过程
- 使用一个 sql 语句并从子查询中获取新的 id
- 在应用程序中使用互斥锁(我猜是个坏主意)
我对代码的期望:
- 第二个客户端一直等到它可以拿到锁。这种等待将在我的 await ...BeginTransaction() 中自动完成,对吗?
这里是有问题的 table 的型号:
[Table(Schema = "dbo", Name = "@COR_DIA_AKM_LOTAT")]
public partial class @COR_DIA_AKM_LOTAT : IUDT
{
[PrimaryKey, NotNull] public string Code { get; set; }
[Column, NotNull] public string Name { get; set; }
[Column, NotNull] public int U_DocEntry { get; set; }
[Column, NotNull] public int U_LineNum { get; set; }
[Column] public decimal U_LotQty { get; set; }
[Column] public string U_DeletedF { get; set; }
[Column] public string U_LotNum { get; set; }
[Column] public decimal U_PkgMandatoryQty { get; set; }
}
非常感谢任何启发
更新
我的误会是我在想
...BeginTransaction(..)
已经以某种方式锁定了 table。但是现在,在@SvyatoslavDanyliv 的更多阅读和指示之后,我发现锁定发生在查询级别。所以要获得我想要的行为,我需要:
// read committed is fine because i just would like to lock -> execute queries -> commit
using var trans = await _connection.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted);
// UPDLOCK is important here because it will start the lock
sql = @"SELECT ISNULL(MAX(CAST(Code AS INT)),0) FROM [@COR_DIA_AKM_LOTAT] WITH (UPDLOCK)";
var maxCode = await _connection.ExecuteAsync<int>(sql) + 1;
string newCode = maxCode.ToString("00000000");
sql = "INSERT INTO [@COR_DIA_AKM_LOTAT] (Code, Name, U_DocEntry, U_LineNum, U_LotNum, U_LotQty, U_ItemCode, U_Typ, U_DeletedF, U_LotCode) VALUES (@newCode, @newCode, @docEntry, @lineNum, @lot, @qty, @itemCode, 'F', 'N', '')";
await Task.Delay(4000);
await _connection.ExecuteAsync(sql, ...)
...more queries
await _connection.CommitTransactionAsync();
由于创建 linq2db
是为了最大程度地避免使用 Raw SQL,因此有一种方法可以在没有事务的情况下插入此类记录。
// tricky part, creating LINQ query which returns result set with calculated 'newCode'
var maxQuery = db.SelectQuery<string>(() =>
Sql.ConvertTo<string>.From(
db.GetTable<COR_DIA_AKM_LOTAT>().Max(x => Sql.ConvertTo<int?>.From(x.Code)) ?? 0 + 1)
)
.AsSubQuery() // introducing subquery to help better PadLeft calculation
.Select(x => Sql.PadLeft(x, 8, '0'))
.AsSubQuery(); // additional subquery because value will be used twice
// inserting prepared value into destination table
maxQuery.Insert(db.GetTable<COR_DIA_AKM_LOTAT>(),
newCode =>
new COR_DIA_AKM_LOTAT
{
Code = newCode,
Name = newCode,
U_DocEntry = updatePos.docEntry,
U_LineNum = updatePos.lineNum,
U_LotNum = l.lot,
U_LotQty = l.qty,
U_ItemCode = updatePos.itemCode,
U_Typ = "F",
U_DeletedF = "N",
U_LotCode = ""
});
查询应生成以下内容SQL(为成功执行测试更改了一些值):
INSERT INTO [dbo].[@COR_DIA_AKM_LOTAT]
(
[Code],
[Name],
[U_DocEntry],
[U_LineNum],
[U_LotNum],
[U_LotQty],
[U_DeletedF],
[U_PkgMandatoryQty]
)
SELECT
[t1].[c1],
[t1].[c1],
1,
1,
N'1',
1,
N'N',
1
FROM
(
SELECT
IIF(Len([x_1].[c1]) > 8, [x_1].[c1], Replicate(N'0', 8 - Len([x_1].[c1])) + [x_1].[c1]) as [c1]
FROM
(
SELECT
Convert(NVarChar(11), Coalesce((
SELECT
Max(Convert(Int, [x].[Code]))
FROM
[dbo].[@COR_DIA_AKM_LOTAT] [x]
), 1)) as [c1]
) [x_1]
) [t1]
无论如何最好考虑包含最后增量值的特殊 table。因为按字符串字段计算最大值不是性能的好选择。
我在 asp.net 6.0 api 中使用 linqtodb 取得了巨大成功。 但现在我正处于一个看起来我需要使用交易的地步,看起来我在那里误解了一些事情。 我正在获取 _connection 对象作为服务中的注入对象
我得到的错误:
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action\u00601 wrapCloseInAction)\r\n at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action\u00601 wrapCloseInAction)\r\n at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock
...
...
"message":"Transaction (Process ID 56) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction."
有问题的代码是:
...
await _connection.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
sql = "SELECT ISNULL(MAX(CAST(Code AS INT)),0) FROM [@COR_DIA_AKM_LOTAT]";
var maxCode = await _connection.ExecuteAsync<int>(sql) + 1;
string newCode = maxCode.ToString("00000000");
sql = "INSERT INTO [@COR_DIA_AKM_LOTAT] (Code, Name, U_DocEntry, U_LineNum, U_LotNum, U_LotQty, U_ItemCode, U_Typ, U_DeletedF, U_LotCode) VALUES (@newCode, @newCode, @docEntry, @lineNum, @lot, @qty, @itemCode, 'F', 'N', '')";
await Task.Delay(10000);
await _connection.ExecuteAsync(sql,
new DataParameter { Name = "@newCode", Value = newCode },
new DataParameter { Name = "@qty", Value = l.qty },
new DataParameter { Name = "@docEntry", Value = updatePos.docEntry },
new DataParameter { Name = "@lineNum", Value = updatePos.lineNum },
new DataParameter { Name = "@itemCode", Value = updatePos.itemCode },
new DataParameter { Name = "@lot", Value = l.lot }
);
await _connection.CommitTransactionAsync();
...
因此您可能会看到需要先创建一个增量字母数字 ID(结构已给出,我无法更改) 然后我会在插页中使用它。
所以我需要确保以上部分的并发使用会等待彼此完成
await Task.Delay(...) 就在那里,所以我可以测试并发使用
当我现在从 2 个不同的客户端执行此代码时,来自第二个客户端的第二次调用失败并显示上述消息
我考虑过但不适用的事情:
- 使用存储过程
- 使用一个 sql 语句并从子查询中获取新的 id
- 在应用程序中使用互斥锁(我猜是个坏主意)
我对代码的期望:
- 第二个客户端一直等到它可以拿到锁。这种等待将在我的 await ...BeginTransaction() 中自动完成,对吗?
这里是有问题的 table 的型号:
[Table(Schema = "dbo", Name = "@COR_DIA_AKM_LOTAT")]
public partial class @COR_DIA_AKM_LOTAT : IUDT
{
[PrimaryKey, NotNull] public string Code { get; set; }
[Column, NotNull] public string Name { get; set; }
[Column, NotNull] public int U_DocEntry { get; set; }
[Column, NotNull] public int U_LineNum { get; set; }
[Column] public decimal U_LotQty { get; set; }
[Column] public string U_DeletedF { get; set; }
[Column] public string U_LotNum { get; set; }
[Column] public decimal U_PkgMandatoryQty { get; set; }
}
非常感谢任何启发
更新
我的误会是我在想
...BeginTransaction(..)
已经以某种方式锁定了 table。但是现在,在@SvyatoslavDanyliv 的更多阅读和指示之后,我发现锁定发生在查询级别。所以要获得我想要的行为,我需要:
// read committed is fine because i just would like to lock -> execute queries -> commit
using var trans = await _connection.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted);
// UPDLOCK is important here because it will start the lock
sql = @"SELECT ISNULL(MAX(CAST(Code AS INT)),0) FROM [@COR_DIA_AKM_LOTAT] WITH (UPDLOCK)";
var maxCode = await _connection.ExecuteAsync<int>(sql) + 1;
string newCode = maxCode.ToString("00000000");
sql = "INSERT INTO [@COR_DIA_AKM_LOTAT] (Code, Name, U_DocEntry, U_LineNum, U_LotNum, U_LotQty, U_ItemCode, U_Typ, U_DeletedF, U_LotCode) VALUES (@newCode, @newCode, @docEntry, @lineNum, @lot, @qty, @itemCode, 'F', 'N', '')";
await Task.Delay(4000);
await _connection.ExecuteAsync(sql, ...)
...more queries
await _connection.CommitTransactionAsync();
由于创建 linq2db
是为了最大程度地避免使用 Raw SQL,因此有一种方法可以在没有事务的情况下插入此类记录。
// tricky part, creating LINQ query which returns result set with calculated 'newCode'
var maxQuery = db.SelectQuery<string>(() =>
Sql.ConvertTo<string>.From(
db.GetTable<COR_DIA_AKM_LOTAT>().Max(x => Sql.ConvertTo<int?>.From(x.Code)) ?? 0 + 1)
)
.AsSubQuery() // introducing subquery to help better PadLeft calculation
.Select(x => Sql.PadLeft(x, 8, '0'))
.AsSubQuery(); // additional subquery because value will be used twice
// inserting prepared value into destination table
maxQuery.Insert(db.GetTable<COR_DIA_AKM_LOTAT>(),
newCode =>
new COR_DIA_AKM_LOTAT
{
Code = newCode,
Name = newCode,
U_DocEntry = updatePos.docEntry,
U_LineNum = updatePos.lineNum,
U_LotNum = l.lot,
U_LotQty = l.qty,
U_ItemCode = updatePos.itemCode,
U_Typ = "F",
U_DeletedF = "N",
U_LotCode = ""
});
查询应生成以下内容SQL(为成功执行测试更改了一些值):
INSERT INTO [dbo].[@COR_DIA_AKM_LOTAT]
(
[Code],
[Name],
[U_DocEntry],
[U_LineNum],
[U_LotNum],
[U_LotQty],
[U_DeletedF],
[U_PkgMandatoryQty]
)
SELECT
[t1].[c1],
[t1].[c1],
1,
1,
N'1',
1,
N'N',
1
FROM
(
SELECT
IIF(Len([x_1].[c1]) > 8, [x_1].[c1], Replicate(N'0', 8 - Len([x_1].[c1])) + [x_1].[c1]) as [c1]
FROM
(
SELECT
Convert(NVarChar(11), Coalesce((
SELECT
Max(Convert(Int, [x].[Code]))
FROM
[dbo].[@COR_DIA_AKM_LOTAT] [x]
), 1)) as [c1]
) [x_1]
) [t1]
无论如何最好考虑包含最后增量值的特殊 table。因为按字符串字段计算最大值不是性能的好选择。