EF Core - 在数据库中按顺序插入记录,PK 上没有标识

Posted

技术标签:

【中文标题】EF Core - 在数据库中按顺序插入记录,PK 上没有标识【英文标题】:EF Core - insert record sequentially in DB without identity on PK 【发布时间】:2021-12-13 10:16:31 【问题描述】:

免责声明:这个问题是关于近 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

如您所见,table 没有在 PK 上设置 Identity,所以必须手动插入。

项目正在 Web Api .Net Core 5 中重建,它将执行所有 CRUD 操作。 它使用 EF Core 作为 ORM,并且同意这里将使用 Unit of Work 模式(请继续阅读,UoW在这里不是问题)。

对于那些好奇或想知道它的价值的人,你可以在这里查看它 (https://pastebin.com/58bSDkUZ)(这绝不是完整的 UoW,只是部分没有 cmets)。 p>

更新:调用者控制器操作:

[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;
        
    

同样的事情。

我做了很多研究来尝试解决这个问题:

    .NET Core - ExecuteSqlRaw - last inserted id? - 解决方案要么不起作用,要么仅适用于标识列 我想到了使用 SQL OUTPUT 变量,但问题是它真的很难甚至不可能实现,感觉很笨拙,总体而言,如果管理,不能保证它会起作用。如此处所示:SQL Server Output Clause into a scalar variable 和此处How can I assign inserted output value to a variable in sql server?

如上所述,当时的 ORM 做了一些神奇的事情,它在数据库中顺序插入记录,我希望保持这种状态。

在处理给定场景时,有什么方法可以实现顺序插入?

【问题讨论】:

你在使用多线程吗?如果是这样,也许您可​​以尝试锁定这部分代码。 这是从异步 Web Api 控制器操作中使用的。我将用它的调用方式更新我的问题。 什么是_uow?请记住,工作单元模式是内置的EntityFramework...如果您使用的是EntityFramework,那么您正在使用工作单元模式。小心不要过度设计解决方案。 // Called by POST method multiple times at once -> 发布的代码没有显示这个 @bluedot 我要感谢你,不管你是否扩展了我的观点。我还没有听说过事务隔离级别。尽管这似乎不容易阅读主题,但我一定会研究一下。 【参考方案1】:

好的,我终于想通了。

我尝试过的方法不起作用:

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
  

未指定 // 失败了一半 违反主键约束“PK_KlantDocument”。无法在对象“dbo.KlantDocument”中插入重复键。重复键值为 (1652)。

混乱 // 全部失败 抛出异常:System.Private.CoreLib.dll 中的“System.ArgumentOutOfRangeException”

ReadUncommitted // 失败了一半 违反主键约束“PK_KlantDocument”。无法在对象“dbo.KlantDocument”中插入重复键。重复键值为 (1659)。

ReadCommitted // 失败了一半 违反主键约束“PK_KlantDocument”。无法在对象“dbo.KlantDocument”中插入重复键。重复键值为 (1664)。

RepeatableRead // 一半失败 违反主键约束“PK_KlantDocument”。无法在对象“dbo.KlantDocument”中插入重复键。重复键值为 (1626)。

Serializable // 失败 3 个 事务(进程 ID 66)在锁定资源上与另一个进程死锁,并已被选为死锁牺牲品。重新运行事务。

快照 // 全部失败 快照隔离事务无法访问数据库“测试”,因为此数据库中不允许快照隔离。使用 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) 不可为空,每当我尝试像这样插入文档时:

            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(不可为空)的默认值OID值为0,所以在创建doc变量时,不可为空

注意:如果您使用这种方法,即使可以,也要非常小心。它将更改使用此 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%'

我必须手动更改每个表以删除这些约束

什么有效:

再次感谢 EF Core 团队的一位非常棒的人 Roji,我设法实现了这一点。 同上,我在 DbContextOnModelCreating 方法中创建了变化:

        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'
)

【讨论】:

以上是关于EF Core - 在数据库中按顺序插入记录,PK 上没有标识的主要内容,如果未能解决你的问题,请参考以下文章

如何在 EF Core 中按条件获取最新条目?

从零开始自动递增 PK - EF Core 代码优先

如何避免在 EF Core 中插入后选择插入的实体?

EF Core 错误地为 Array 执行多次插入

.net ef core 检查是不是插入成功

.net ef core 检查是不是插入成功