在插入数据库时​​,如何设置要设置为其他类的生成ID值的类属性的值?

ESi*_*lis 6 c# sql-server entity-framework entity-framework-core

我很难说清楚这句话,所以我将使用一些代码来帮助解释自己和我的问题。因此,假设我有两个班级ClassAClassB

class ClassA
{
    public int ClassAId { get; set; }
    public string Name { get; set; }
}

[Owned]
class ClassAOwned
{
    public int ClassAId { get; set; }
    public string Name { get; set; }
}

class ClassB
{
    public int ClassBId { get; set; }
    public string Action { get; set; }
    public ClassAOwned ClassA { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的ClassBcontains ClassA,但是我有另一个类,ClassAOwned因为我想ClassB拥有ClassA(将其列平展到ClassB表中),而且还拥有ClassADbSet作为单独的表(并且据我所知,实体类不能拥有,并且在拥有自己的DbSet同时),所以我不得不使用2个不同的类。这是我的背景,使其更易于理解:

class TestContext : DbContext
{
    public DbSet<ClassA> ClassAs { get; set; }
    public DbSet<ClassB> ClassBs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("TestContext");
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,当我尝试同时插入ClassAClassB上下文并且必须匹配ClassAId数据库提供程序生成的它们的值时,我的问题就来了:

var testContext = new TestContext();
var classA = new ClassA
{
    Name = "classAName"
};
var classB = new ClassB
{
    Action = "create",
    ClassA = new ClassAOwned
    {
        ClassAId = classA.ClassAId,
        Name = classA.Name
    }
};
testContext.ClassAs.Add(classA);
testContext.ClassBs.Add(classB);
classB.ClassA.ClassAId = classA.ClassAId;
testContext.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

使用InMemoryDatabase以下调用时:

testContext.ClassAs.Add(classA);
Run Code Online (Sandbox Code Playgroud)

实际上会更改classA.ClassAId为正确的生成值,但是在使用SQL Server时classA.ClassAId将其设置为int.MinValue下一个调用:

classB.ClassA.ClassAId = classA.ClassAId;
Run Code Online (Sandbox Code Playgroud)

设置classB.ClassA.ClassAIdint.MinValue。最后的电话:

testContext.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

更改classA.ClassAId为正确的生成值,但classB.ClassA.ClassAId保持不变int.MinValue,这就是被插入数据库的值。

我的问题是:有没有办法告诉EF核心,将两个实体添加到上下文中时,将一个人的属性设置为为另一个实体的主键生成的任何值?因此,我要寻找的功能与添加两个具有外键的实体完全相同,只是在这种情况下,它并不是真正的外键。

一个简单的解决方法是在“ foreign key”(classB.ClassA.ClassAId)之后设置testContext.SaveChanges()并再次保存更改,但是随后它将变成两个单独的操作,如果第二个操作失败了怎么办?数据库将处于无效状态。

Iva*_*oev 2

这是可能的,但需要一些技巧,它适用于目前最新的 EF Core 2.2,并且可能在 3.0+ 中停止工作(至少需要验证)。

首先,它必须被映射为关系——没有其他方法。不过,它不需要是真正的数据库关系,从 EF Core 模型的角度来看应该是这样的。

其次,这一点非常重要,删除级联行为应设置为 Restrict,这当前意味着在数据库中强制执行,但对内存中跟踪的相关实体不执行任何操作。

那么让我们对您的示例进行此操作。上述两个映射都需要类似于以下的流畅配置:

modelBuilder.Entity<ClassB>().OwnsOne(e => e.ClassA)
    .HasOne<ClassA>().WithMany() // (1)
    .OnDelete(DeleteBehavior.Restrict); // (2)
Run Code Online (Sandbox Code Playgroud)

如果您使用迁移,生成的迁移将包含如下内容:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateTable(
        name: "ClassA",
        columns: table => new
        {
            ClassAId = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Name = table.Column<string>(nullable: true)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_ClassA", x => x.ClassAId);
        });

    migrationBuilder.CreateTable(
        name: "ClassB",
        columns: table => new
        {
            ClassBId = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Action = table.Column<string>(nullable: true),
            ClassA_ClassAId = table.Column<int>(nullable: false),
            ClassA_Name = table.Column<string>(nullable: true)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_ClassB", x => x.ClassBId);
            table.ForeignKey(
                name: "FK_ClassB_ClassA_ClassA_ClassAId",
                column: x => x.ClassA_ClassAId,
                principalTable: "ClassA",
                principalColumn: "ClassAId",
                onDelete: ReferentialAction.Restrict);
        });

    migrationBuilder.CreateIndex(
        name: "IX_ClassB_ClassA_ClassAId",
        table: "ClassB",
        column: "ClassA_ClassAId");
}
Run Code Online (Sandbox Code Playgroud)

手动编辑它并删除ForeignKey命令(行),因为您不需要真正的 FK。您也可以删除相应的CreateIndex命令,尽管这不会有什么坏处。

就这样。您需要记住的唯一重要的事情是仅在新实体已添加到上下文(因此被跟踪)之后才使用主体TableAId属性。IE

var testContext = new TestContext();
var classA = new ClassA
{
    Name = "classAName"
};
testContext.ClassAs.Add(classA); // <--
var classB = new ClassB
{
    Action = "create",
    ClassA = new ClassAOwned
    {
        ClassAId = classA.ClassAId, // <--
        Name = classA.Name
    }
};
testContext.ClassBs.Add(classB);
testContext.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

它将生成临时负值,但之后SaveChanged两个 id 将使用实际数据库生成的值进行更新。