![](/img/trans.png)
[英]EF4.1 DBContext: Insert/update in one Save() function without identity PK
[英]EF Core - insert record sequentially in DB without identity on PK
免责声明:这个问题是关于近 17 岁系统的“现代化”。 17 年前使用的 ORM 要求 PK 不使用Identity 。 我知道这有多糟糕,对此我无法改变任何事情。
所以我在数据库中有(简化)下表:
CREATE TABLE [dbo].[KlantDocument](
[OID] [bigint] NOT NULL,
[Datum] [datetime] NOT NULL,
[Naam] [varchar](150) NOT NULL,
[Uniek] [varchar](50) NOT NULL,
[DocumentSoort] [varchar](50) NULL,
CONSTRAINT [PK_KlantDocument] PRIMARY KEY CLUSTERED
(
[OID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[KlantDocument] CHECK CONSTRAINT [FK_KlantDocument_Klant]
GO
如您所见,表没有在 PK 上设置标识,因此必须手动插入。
项目正在Web Api .Net Core 5中重建,它将执行所有 CRUD 操作。 它使用EF Core作为 ORM,并且同意这里将使用工作单元模式(请继续阅读,UoW 不是这里的问题)。
对于那些好奇的人或它的价值,您可以在此处查看( https://pastebin.com/58bSDkUZ )(这绝不是完整的 UoW,只是部分内容且没有评论)。
更新:调用方控制器操作:
[HttpPost]
[Route("UploadClientDocuments")]
public async Task<IActionResult> UploadClientDocuments([FromForm] ClientDocumentViewModel model)
{
if (!ModelState.IsValid)
return BadRequest(ModelStateExtensions.GetErrorMessage(ModelState));
var dto = new ClientDocumentUploadDto
{
DocumentTypeId = model.DocumentTypeId,
ClientId = model.ClientId,
FileData = await model.File.GetBytes(),
FileName = model.File.FileName, // I need this property as well, since I have only byte array in my "services" project
};
var result = await _documentsService.AddClientDocument(dto);
return result ? Ok() : StatusCode(500);
}
当我插入记录时,我是这样做的:
// Called by POST method multiple times at once
public async Task<bool> AddClientDocument(ClientDocumentUploadDto dto)
{
try
{
var doc = new KlantDocument
{
/* All of the logic below to fetch next id will be executed before any of the saves happen, thus we get "Duplicate key" exception. */
// 1st try to fetch next id
// Oid = _uow.Query<KlantDocument>().OrderByDescending(s => s.Oid).First().Oid + 1,
// 2nd try to fetch next id
// Oid = _uow.Query<KlantDocument>().OrderBy(s => s.Oid).Last().Oid + 1,
// 3rd try to fetch next id
// Oid = await _uow.Query<KlantDocument>().OrderBy(s => s.Oid).AsNoTracking().Select(s => s.Oid).LastAsync() + 1,
// 4th try to fetch next id
// Oid = _uow.Query<KlantDocument>().OrderBy(s => s.Oid).AsNoTracking().Select(s => s.Oid).Last() + 1,
// 5th try to fetch next id
// Oid = (_uow.Query<KlantDocument>().OrderBy(s => s.Oid).AsNoTracking().Max(s => s.Oid) + 1),
Naam = dto.FileName,
DocumentSoort = dto.DocumentTypeId,
Datum = DateTime.Now,
Uniek = Guid.NewGuid() + "." + dto.FileName.GetExtension()
};
_uow.Context.Set<KlantDocument>().Add(doc); // Does not work
_uow.Commit();
}
catch (Exception e)
{
_logger.Error(e);
return false;
}
}
我收到“重复键”异常,因为插入时有 2 条记录重叠。
我试图将它包装到交易中,如下所示:
_uow.ExecuteInTransaction(() => {
var doc = new KlantDocument
{
/* All of the logic below to fetch next id will be executed before any of the saves happen, thus we get "Duplicate key" exception. */
// 1st try to fetch next id
// Oid = _uow.Query<KlantDocument>().OrderByDescending(s => s.Oid).First().Oid + 1,
// 2nd try to fetch next id
// Oid = _uow.Query<KlantDocument>().OrderBy(s => s.Oid).Last().Oid + 1,
// 3rd try to fetch next id
// Oid = await _uow.Query<KlantDocument>().OrderBy(s => s.Oid).AsNoTracking().Select(s => s.Oid).LastAsync() + 1,
// 4th try to fetch next id
// Oid = _uow.Query<KlantDocument>().OrderBy(s => s.Oid).AsNoTracking().Select(s => s.Oid).Last() + 1,
// 5th try to fetch next id
// Oid = (_uow.Query<KlantDocument>().OrderBy(s => s.Oid).AsNoTracking().Max(s => s.Oid) + 1),
Naam = dto.FileName,
DocumentSoort = dto.DocumentTypeId,
Datum = DateTime.Now,
Uniek = Guid.NewGuid() + "." + dto.FileName.GetExtension()
};
_uow.Context.Set<KlantDocument>().Add(doc); // Does not work
_uow.Commit();
});
它不起作用。 我仍然收到“重复键”异常。
据我所知,在事务完成之前, EF 不应该默认锁定数据库吗?
我尝试像这样手动编写插入 SQL:
using (var context = _uow.Context)
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
// VALUES ((SELECT MAX(OID) + 1 FROM KlantDocument), -- tried this also, but was not yielding results
var commandText = @"INSERT INTO KlantDocument
(
OID
,Datum
,Naam
,Uniek
,DocumentSoort
)
VALUES (
(SELECT TOP(1) (OID + 1) FROM KlantDocument ORDER BY OID DESC),
@Datum,
@Naam,
@Uniek,
@DocumentSoort
)";
var datum = new SqlParameter("@Datum", DateTime.Now);
var naam = new SqlParameter("@Naam", dto.FileName);
var uniek = new SqlParameter("@Uniek", Guid.NewGuid() + "." + dto.FileName.GetExtension());
var documentSoort = new SqlParameter("@DocumentSoort", dto.DocumentTypeId ?? "OrderContents");
context.Database.ExecuteSqlRaw(commandText, datum, naam, uniek, documentSoort);
dbContextTransaction.Commit();
}
catch (Exception e)
{
dbContextTransaction.Rollback();
return false;
}
}
}
一样。
我做了很多研究来尝试解决这个问题:
如上所述,当时的 ORM 做了一些神奇的事情,它在数据库中顺序插入记录,我想保持这种方式。
在处理给定场景时,有什么方法可以实现顺序插入?
好的,我终于想通了。
我尝试过的方法不起作用:
1.) @bluedot 提出的 IsolationLevel:
public enum IsolationLevel
{
Unspecified = -1, // 0xFFFFFFFF
Chaos = 16, // 0x00000010
ReadUncommitted = 256, // 0x00000100
ReadCommitted = 4096, // 0x00001000
RepeatableRead = 65536, // 0x00010000
Serializable = 1048576, // 0x00100000
Snapshot = 16777216, // 0x01000000
}
未指定// 失败一半违反 PRIMARY KEY 约束“PK_KlantDocument”。 无法在对象“dbo.KlantDocument”中插入重复键。 重复的键值为 (1652)。
混乱// 所有这些都失败抛出异常:System.Private.CoreLib.dll 中的“System.ArgumentOutOfRangeException”
ReadUncommitted // 其中一半失败违反 PRIMARY KEY 约束 'PK_KlantDocument'。 无法在对象“dbo.KlantDocument”中插入重复键。 重复的键值为 (1659)。
ReadCommitted // 其中一半失败违反 PRIMARY KEY 约束 'PK_KlantDocument'。 无法在对象“dbo.KlantDocument”中插入重复键。 重复的键值为 (1664)。
RepeatableRead // 其中一半失败违反 PRIMARY KEY 约束 'PK_KlantDocument'。 无法在对象“dbo.KlantDocument”中插入重复键。 重复的键值为 (1626)。
Serializable // 失败 3 个事务(进程 ID 66)在锁定资源上与另一个进程发生死锁,并已被选为死锁受害者。 重新运行事务。
快照// 全部失败快照隔离事务访问数据库 'Test' 失败,因为该数据库中不允许快照隔离。 使用 ALTER DATABASE 允许快照隔离。
2.) DbContext 中的序列:
感谢来自EF Core 团队的一位非常棒的人Roji 。 我也在 GitHub ( https://github.com/dotnet/efcore/issues/26480 ) 上问过这个问题,他实际上把我推向了正确的方向。
我遵循了这个文档: https://docs.microsoft.com/en-us/ef/core/modeling/sequences ,我欣喜若狂地发现这实际上可以是“自动化的”(阅读,我可以将它们添加到上下文和使用我的 UoW 提交更改),并且在插入数据时我不需要编写 SQL 查询。 因此,我创建了以下代码来设置DbContext
的序列并使用迁移来运行它。
我的OnModelCreating
方法更改如下所示:
modelBuilder.HasSequence<long>("KlantDocumentSeq")
.StartsAt(2000)
.IncrementsBy(1)
.HasMin(2000);
modelBuilder.Entity<KlantDocument>()
.Property(o => o.Oid)
.HasDefaultValueSql("NEXT VALUE FOR KlantDocumentSeq");
但是由于我的Oid (PK) 不是 nullable ,每当我尝试像这样插入我的文档时:
var doc = new KlantDocument
{
Naam = dto.FileName,
DocumentSoort = dto.DocumentTypeId,
Datum = DateTime.Now,
Uniek = Guid.NewGuid() + "." + dto.FileName.GetExtension()
};
_uow.Context.Set<KlantDocument>().Add(doc);
_uow.Commit();
它产生了这个 SQL(为了可见性目的而美化):
SET NOCOUNT ON;
INSERT INTO [KlantDocument] ([OID], [Datum], [DocumentSoort], [Naam], [Uniek])
VALUES (0, '2021-10-29 14:34:06.603', NULL, 'sample 1 - Copy (19).pdf', '56311d00-4d7c-497c-a53e-92330f9f78d4.pdf');
自然我得到了例外:
InnerException = {"Violation of PRIMARY KEY constraint 'PK_KlantDocument'. Cannot insert duplicate key in object 'dbo.KlantDocument'. The duplicate key value is (0).\r\nThe statement has been terminated."}
由于它是 long (non-nullable) OID value to 0 的默认值,因此在创建 doc 变量时,因为它不可为 null 。
注意:即使可以使用这种方法,也要非常小心。 它将更改使用此 OID 的所有存储过程。 就我而言,它创建了所有这些:
SELECT * FROM sys.default_constraints
WHERE
name like '%DF__ArtikelDocu__OID__34E9A0B9%' OR
name like '%DF__KlantDocume__OID__33F57C80%' OR
name like '%DF__OrderDocume__OID__33015847%'
我不得不手动 ALTER 每个表来删除这些约束。
什么工作:
再次感谢来自 EF Core 团队 Roji 的一个非常棒的人,我设法实现了这个工作。 与上面相同,我在DbContext
, OnModelCreating
方法中创建了更改:
modelBuilder.HasSequence<long>("KlantDocumentSeq")
.StartsAt(2000)
.IncrementsBy(1)
.HasMin(2000);
添加了生成此代码的迁移:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateSequence(
name: "KlantDocumentSeq",
startValue: 2000L,
minValue: 2000L);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropSequence(
name: "KlantDocumentSeq");
}
更新了数据库,数据库工作就是这样。 最后的变化是我的方法,我不得不替换臭名昭著的代码块((SELECT MAX(OID) + 1 FROM KlantDocument)
或(SELECT TOP(1) (OID + 1) FROM KlantDocument ORDER BY OID DESC)
和
NEXT VALUE FOR KlantDocumentSeq
它奏效了。
我的方法的完整代码在这里:
using (var context = _uow.Context)
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
var commandText = @"INSERT INTO KlantDocument
(
OID
,Datum
,Naam
,Uniek
,DocumentSoort
)
VALUES (
NEXT VALUE FOR KlantDocumentSeq,
@Datum,
@Naam,
@Uniek,
@DocumentSoort
)";
var datum = new SqlParameter("@Datum", DateTime.Now);
var naam = new SqlParameter("@Naam", dto.FileName);
var uniek = new SqlParameter("@Uniek", Guid.NewGuid() + "." + dto.FileName.GetExtension());
var documentSoort = new SqlParameter("@DocumentSoort", dto.DocumentTypeId ?? "OrderContents");
context.Database.ExecuteSqlRaw(commandText, datum, naam, uniek, documentSoort);
dbContextTransaction.Commit();
}
catch (Exception e)
{
dbContextTransaction.Rollback();
return false;
}
}
}
它生成的SQL是:
INSERT INTO KlantDocument
(
OID
,Datum
,Naam
,Uniek
,DocumentSoort
)
VALUES (
NEXT VALUE FOR KlantDocumentSeq,
'2021-10-29 15:34:43.943',
'sample 1 - Copy (13).pdf',
'd4419c6c-dff9-431c-a7ed-6db54407051d.pdf',
'OrderContents'
)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.