SQL Server What is the proper way of autoincrementing non primary key column?

vlju58qv  于 2023-10-15  发布在  其他
关注(0)|答案(3)|浏览(116)

I have a table which has an integer field which is supposed to be unique and autoincremented.

After doing some research I came up with two different approaches:

Case A

modelBuilder.Entity<MyTable>()
                .Property(x => x.MyIntegerField)
                .ValueGeneratedOnAdd()
                .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);

            modelBuilder.Entity<MyTable>()
                .HasIndex(x => x.MyIntegerField)
                .IsUnique();

This works, but I have questions regarding it:

  1. When does it increment the value of the MyIntegerField? Does it create it on the App level or on the Database level? Why would I ask this is because imagine two different HTTP requests are coming to the app. I'm afraid it might throw UniqueConstraint exception if value generation happened on app-level.

Case B

modelBuilder.Entity<MyTable>()
                .Property(x => x.MyIntegerField)
                .ValueGeneratedOnAdd()
                .UseIdentityColumn();

            modelBuilder.Entity<MyTable>()
                .HasIndex(x => x.MyIntegerField)
                .IsUnique();

This works too, but the again I have questions:

  1. Does this make the field managed on the Database level?

In the docs it says UseIdentityColumn() is for Primary keys, but MyIntegerField is not a primary key. I did not mark it as one, and in the schema after migration I see it didn't create it as one.

  1. Are there any possible problems I might face using this method?

What is the correct way of solving my issue?

P.S: I am using GUID for Primary Key

zfycwa2u

zfycwa2u1#

Looks like both do the same... I tested both cases, and checked for Identity column using the following command:

SELECT 
    c.name AS ColumnName,
    t.name AS TableName,
    CASE 
        WHEN c.is_identity = 1 THEN 'Yes'
        ELSE 'No'
    END AS IsIdentityColumn
FROM sys.columns c
INNER JOIN sys.tables t ON c.object_id = t.object_id
WHERE 
    c.name = 'My column'  -- Replace with your column name
    AND t.name = 'My table'  -- Replace with your table name
    AND t.schema_id = SCHEMA_ID('dbo');  -- Specify the schema (dbo in this case)

It resulted with Yes in both cases Now I don't know why exactly they mentioned "for key properties" in docs for UseIdentityColumn() method, but both seem to be working the same way. Creating an Identity inside the DB, which means DB itself is gonna manage the insert values.

z6psavjg

z6psavjg2#

I have made a quick experiment trying out the second approach.

Those are versions of packages I used:

  • Microsoft.EntityFrameworkCore.Design Version="7.0.11"
  • Microsoft.EntityFrameworkCore.SqlServer Version="7.0.11"

I started with this DbContext:

using Microsoft.EntityFrameworkCore;

namespace Test;

public class CustomDbContext : DbContext
{
    public class CustomEntity
    {
        required public int Id { get; set; }
        required public string Name { get; set; }
        required public int Incremental { get; set; }
    }

    public CustomDbContext(DbContextOptions<CustomDbContext> options)
    : base(options)
    {
        
    }

    public DbSet<CustomEntity> CustomEntities { get; set; } = null!
}

And created a fresh migration using dotnet ef migrations add InitialCreate getting this migration file:

using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Test.Migrations
{
    /// <inheritdoc />
    public partial class InitialCreate : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "CustomEntities",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
                    Incremental = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_CustomEntities", x => x.Id);
                });
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "CustomEntities");
        }
    }
}

Now I changed the DbContext by overloading OnModelCreating method:

protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<CustomEntity>()
                .Property(x => x.Incremental)
                .ValueGeneratedOnAdd()
                .UseIdentityColumn();
    }

now calling dotnet ef migrations add Incremental resulted in an error:

The properties 'CustomEntity.Id', 'CustomEntity.Incremental' are configured to use 'Identity' value generation and are mapped to the same table 'CustomEntities', but only one column per table can be configured as 'Identity'. Call 'ValueGeneratedNever' in 'OnModelCreating' for properties that should not use 'Identity'.

It seems like ChatGPT was lying once again, and this approach is not valid for code first. It should be possible to do it when working in database first though since nothing prevents you from manually adding a second identity column to a table. I think you are stuck with solving the problem on C# side.

oalqel3c

oalqel3c3#

I have answered previously for a problem when Id is also an identity column, now I'm answering for a case when Id is a Guid .

This is the DbContext I used:

public class CustomDbContext : DbContext
{
    public class CustomEntity
    {
        required public Guid Id { get; set; }
        required public string Name { get; set; }
        public int Incremental { get; set; }
    }

    public CustomDbContext(DbContextOptions<CustomDbContext> options)
    : base(options)
    {
        
    }

    public DbSet<CustomEntity> CustomEntities { get; set; } = null!;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<CustomEntity>()
                .Property(x => x.Incremental)
                .ValueGeneratedOnAdd()
                .UseIdentityColumn();
    }
}

And this is a part of migration script I got from it:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateTable(
        name: "CustomEntities",
        columns: table => new
        {
            Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
            Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
            Incremental = table.Column<int>(type: "int", nullable: false)
                .Annotation("SqlServer:Identity", "1, 1")
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_CustomEntities", x => x.Id);
        });
}

now I tried getting the client side Incremental value into the database:

app.MapGet("/api/hello", (CustomDbContext ctx) =>
{
    ctx.Database.EnsureDeleted();
    ctx.Database.EnsureCreated();

    ctx.Add(new CustomDbContext.CustomEntity()
    {
        Id = Guid.NewGuid(),
        Name = "Incremental Provided",
        Incremental = 616
    });

    ctx.SaveChanges();

    return new { Message = "Hello World!" };
});

I got an exception.

Cannot insert explicit value for identity column in table 'CustomEntities' when IDENTITY_INSERT is set to OFF.

All was fine when I didn't provide the Incremental value.

ctx.Add(new CustomDbContext.CustomEntity()
{
    Id = Guid.NewGuid(),
    Name = "Incremental Provided"
});

The Query sent to the database on SaveChanges looked correct.

SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [CustomEntities] ([Id], [Name])
OUTPUT INSERTED.[Incremental]
VALUES (@p0, @p1);

The DbContext created the row. Allowed Incremental to be filled by the identity logic from inside the SQL Server, and query ended with returning the Incremental value from the database back to DbContext. So after SaveChanges the entity within C# should have updated value.

And it seems like it does.

var entity = new CustomDbContext.CustomEntity()
{
    Id = Guid.NewGuid(),
    Name = "Incremental Provided"
};

ctx.Add(entity);
Console.WriteLine($"Before SaveChanges - {entity.Incremental}");
ctx.SaveChanges();
Console.WriteLine($"After SaveChanges - {entity.Incremental}");
Before SaveChanges = 0
(...)
After SaveChanges = 1

In conclusion. Second approach looks completely correct and safe. You will not end up with duplicate values inside the identity column even in case of a race condition since EFCore is simply letting SqlServer handle it with its internal mechanisms.

相关问题