简体   繁体   English

EF Core - 在 PK 上没有标识的情况下在 DB 中按顺序插入记录

[英]EF Core - insert record sequentially in DB without identity on PK

Disclaimer: This question is regarding "modernization" of almost 17 years old system.免责声明:这个问题是关于近 17 岁系统的“现代化”。 ORM that was used 17 years ago required that PK's don't use Identity . 17 年前使用的 ORM 要求 PK 不使用Identity I know how bad it is, and I can't change anything regarding this.我知道这有多糟糕,对此我无法改变任何事情。

So I have (simplified) following table in database:所以我在数据库中有(简化)下表:

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

As you can see, table doesn't have Identity set on PK, so it has to be inserted manually.如您所见,表没有在 PK 上设置标识,因此必须手动插入。

Project is being rebuilt in Web Api .Net Core 5 , that is going to do all of the CRUD operations.项目正在Web Api .Net Core 5中重建,它将执行所有 CRUD 操作。 It is using EF Core as ORM, and it is agreed that Unit of Work pattern is going to be used here ( please do keep reading, UoW is not issue here ).它使用EF Core作为 ORM,并且同意这里将使用工作单元模式(请继续阅读,UoW 不是这里的问题)。

For those curious or for what it's worth, you can take a look at it here ( https://pastebin.com/58bSDkUZ ) ( this is by no means full UoW, just partially and without comments).对于那些好奇的人或它的价值,您可以在此处查看( https://pastebin.com/58bSDkUZ )(这绝不是完整的 UoW,只是部分内容且没有评论)。

UPDATE: Caller Controller Action:更新:调用方控制器操作:

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

When I am inserting record I am doing it like so:当我插入记录时,我是这样做的:

// 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;
    }
}

I get "Duplicate key" exception because 2 of the records are overlapping when inserting.我收到“重复键”异常,因为插入时有 2 条记录重叠。

I have tried to wrap it into the transaction like so:我试图将它包装到交易中,如下所示:

_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();
});

and it does not work.它不起作用。 I still get "Duplicate key" exception.我仍然收到“重复键”异常。

From what I know, shouldn't EF by default lock database until transaction is complete?据我所知,在事务完成之前EF 不应该默认锁定数据库吗?

I tried to manually write insert SQL like so:我尝试像这样手动编写插入 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;
        }
    }
}

Same thing.一样。

I did a lot research to try to tackle this issue:我做了很多研究来尝试解决这个问题:

  1. .NET Core - ExecuteSqlRaw - last inserted id? .NET Core - ExecuteSqlRaw - 最后插入的 ID? - solutions either don't work, or work only with identity columns - 解决方案要么不起作用,要么仅适用于标识列
  2. I got idea to use SQL OUTPUT variable, but issue is that it's really hard or even impossible to achieve, feels hacky and overall if managed, there is no guarantee that it will work.我有使用SQL OUTPUT变量的想法,但问题是它真的很难甚至不可能实现,感觉很糟糕,如果管理好,不能保证它会起作用。 As seen here: SQL Server Output Clause into a scalar variable and here How can I assign inserted output value to a variable in sql server?如此处所示: SQL Server Output Clause into a scalar variable和此处如何将插入的输出值分配给 sql server 中的变量?

As noted above, ORM at the time did some magic where it did sequentially insert records in database and I would like to keep it that way.如上所述,当时的 ORM 做了一些神奇的事情,它在数据库中顺序插入记录,我想保持这种方式。

Is there any way that I can achieve sequential insert when dealing with given scenario?在处理给定场景时,有什么方法可以实现顺序插入?

Ok, I have figured this out finally.好的,我终于想通了。

What I tried that did not work:我尝试过的方法不起作用:

1.) IsolationLevel as proposed by @bluedot: 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
  }
  • Unspecified // Failed half of them Violation of PRIMARY KEY constraint 'PK_KlantDocument'.未指定// 失败一半违反 PRIMARY KEY 约束“PK_KlantDocument”。 Cannot insert duplicate key in object 'dbo.KlantDocument'.无法在对象“dbo.KlantDocument”中插入重复键。 The duplicate key value is (1652).重复的键值为 (1652)。

  • Chaos // Failed all of them Exception thrown: 'System.ArgumentOutOfRangeException' in System.Private.CoreLib.dll混乱// 所有这些都失败抛出异常:System.Private.CoreLib.dll 中的“System.ArgumentOutOfRangeException”

  • ReadUncommitted // Failed half of them Violation of PRIMARY KEY constraint 'PK_KlantDocument'. ReadUncommitted // 其中一半失败违反 PRIMARY KEY 约束 'PK_KlantDocument'。 Cannot insert duplicate key in object 'dbo.KlantDocument'.无法在对象“dbo.KlantDocument”中插入重复键。 The duplicate key value is (1659).重复的键值为 (1659)。

  • ReadCommitted // Failed half of them Violation of PRIMARY KEY constraint 'PK_KlantDocument'. ReadCommitted // 其中一半失败违反 PRIMARY KEY 约束 'PK_KlantDocument'。 Cannot insert duplicate key in object 'dbo.KlantDocument'.无法在对象“dbo.KlantDocument”中插入重复键。 The duplicate key value is (1664).重复的键值为 (1664)。

  • RepeatableRead // Failed half of them Violation of PRIMARY KEY constraint 'PK_KlantDocument'. RepeatableRead // 其中一半失败违反 PRIMARY KEY 约束 'PK_KlantDocument'。 Cannot insert duplicate key in object 'dbo.KlantDocument'.无法在对象“dbo.KlantDocument”中插入重复键。 The duplicate key value is (1626).重复的键值为 (1626)。

  • Serializable // Failed 3 of them Transaction (Process ID 66) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Serializable // 失败 3 个事务(进程 ID 66)在锁定资源上与另一个进程发生死锁,并已被选为死锁受害者。 Rerun the transaction.重新运行事务。

  • Snapshot // Failed all of them Snapshot isolation transaction failed accessing database 'Test' because snapshot isolation is not allowed in this database.快照// 全部失败快照隔离事务访问数据库 'Test' 失败,因为该数据库中不允许快照隔离。 Use ALTER DATABASE to allow snapshot isolation.使用 ALTER DATABASE 允许快照隔离。

2.) Sequence in DbContext: 2.) DbContext 中的序列:

Thanks to a really awesome human being from EF Core team, Roji .感谢来自EF Core 团队的一位非常棒的人Roji I asked this on GitHub as well ( https://github.com/dotnet/efcore/issues/26480 ) and he actually pushed me in right direction.我也在 GitHub ( https://github.com/dotnet/efcore/issues/26480 ) 上问过这个问题,他实际上把我推向了正确的方向。

I followed this documentation: https://docs.microsoft.com/en-us/ef/core/modeling/sequences , I was ecstatic to find this could actually be " automated " (read, I could just add them to context and commit changes using my UoW) and I won't be having the need to write the SQL queries when inserting data all the time.我遵循了这个文档: https://docs.microsoft.com/en-us/ef/core/modeling/sequences ,我欣喜若狂地发现这实际上可以是“自动化的”(阅读,我可以将它们添加到上下文和使用我的 UoW 提交更改),并且在插入数据时我不需要编写 SQL 查询。 Thus I created following code to setup the sequence in DbContext and to run it using migrations.因此,我创建了以下代码来设置DbContext的序列并使用迁移来运行它。

My OnModelCreating method changes looked like so:我的OnModelCreating方法更改如下所示:

        modelBuilder.HasSequence<long>("KlantDocumentSeq")
            .StartsAt(2000)
            .IncrementsBy(1)
            .HasMin(2000);
            
        modelBuilder.Entity<KlantDocument>()
            .Property(o => o.Oid)
            .HasDefaultValueSql("NEXT VALUE FOR KlantDocumentSeq");

But since my Oid (PK) is not nullable , whenever I tried to insert my document like so:但是由于我的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();
            
            

It produced this SQL (beautified for visibility purposes):它产生了这个 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');

And naturally I got exception:自然我得到了例外:

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."}

Since it is default value of long (non-nullable) OID value to 0 , when creating doc variable, as it is not nullable .由于它是 long (non-nullable) OID value to 0 的默认值,因此在创建 doc 变量时,因为它不可为 null

NOTE: Be very very cautios if you are using this approach even if you can.注意:即使可以使用这种方法,也要非常小心。 It will alter all of your Stored procedures where this OID is used.它将更改使用此 OID 的所有存储过程。 In my case it created all of these:就我而言,它创建了所有这些:

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

and I had to manually ALTER each table to drop these constrains .我不得不手动 ALTER 每个表来删除这些约束

What worked:什么工作:

Again, thanks to a really awesome human being from EF Core team, Roji, I managed to implement this to work.再次感谢来自 EF Core 团队 Roji 的一个非常棒的人,我设法实现了这个工作。 Same as above, I created change in DbContext , OnModelCreating method:与上面相同,我在DbContextOnModelCreating方法中创建了更改:

        modelBuilder.HasSequence<long>("KlantDocumentSeq")
            .StartsAt(2000)
            .IncrementsBy(1)
            .HasMin(2000);

Added migration that produced this code:添加了生成此代码的迁移:

    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateSequence(
            name: "KlantDocumentSeq",
            startValue: 2000L,
            minValue: 2000L);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropSequence(
            name: "KlantDocumentSeq");
    }

Updated database, and for DB work that was it.更新了数据库,数据库工作就是这样。 Final change came into my method where I had to replace notorious code block ((SELECT MAX(OID) + 1 FROM KlantDocument) or (SELECT TOP(1) (OID + 1) FROM KlantDocument ORDER BY OID DESC)最后的变化是我的方法,我不得不替换臭名昭著的代码块((SELECT MAX(OID) + 1 FROM KlantDocument)(SELECT TOP(1) (OID + 1) FROM KlantDocument ORDER BY OID DESC)

with

NEXT VALUE FOR KlantDocumentSeq

and it worked.它奏效了。

Full code of my method is here:我的方法的完整代码在这里:

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

and SQL it generates is:它生成的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