簡體   English   中英

EF Core - 在 PK 上沒有標識的情況下在 DB 中按順序插入記錄

[英]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;
        }
    }
}

一樣。

我做了很多研究來嘗試解決這個問題:

  1. .NET Core - ExecuteSqlRaw - 最后插入的 ID? - 解決方案要么不起作用,要么僅適用於標識列
  2. 我有使用SQL OUTPUT變量的想法,但問題是它真的很難甚至不可能實現,感覺很糟糕,如果管理好,不能保證它會起作用。 如此處所示: SQL Server Output Clause into a scalar variable和此處如何將插入的輸出值分配給 sql server 中的變量?

如上所述,當時的 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 的一個非常棒的人,我設法實現了這個工作。 與上面相同,我在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'
)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM