首页 > 解决方案 > 如果我将虚拟关键字添加到属性,我需要帮助解决为什么实体框架不会保存

问题描述

我创建了一个名为 Random Data Generator 的开源项目: https ://github.com/DataJuggler/RandomDataGenerator 。

我还发布了一个简短的 6 分钟视频,以便您了解这是否值得克隆:https ://youtu.be/7XEXWhEW_Fw

随机数据生成器

Random Data Generator 是一个示例项目,用于演示我的开源项目 DataTier.Net 与 Entity Framework 相比有多快,因为 DataTier.Net 使用所有存储过程。

这个项目已经完成并且正在运行,但是在我开始吹嘘 DataTier.Net ( https://github.com/DataJuggler/DataTier.Net ) 比 EF 保存速度快 x 倍之前,我想确保我提供了 EF 属性表示。我承认我从我的另一个项目中复制了数据上下文并对其进行了修改,但可能遗漏了一些东西。

如果任何超级英雄(EF Man?)Entity Framework 爱好者有几分钟的时间来看看它可能会节省模糊的答案,因为代码和数据库已发布。

我想这是因为我累了,但是如果我将 virtual 关键字添加到 Address 属性中,Entity Framework 将停止保存 Member 对象,所以我必须先通过一次来保存 Member,然后再通过另一次来保存 Address。

(停止保存任何东西)

public virtual Address Address
{
   get; set;
}

我在两个表之间建立了关系(我认为):

(这可能更容易使用 [RandomData] GO

/****** Object:  Table [dbo].[Address]    Script Date: 8/16/2019 2:57:43 AM     
******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Address](
[Id] [int] IDENTITY(1,1) NOT NULL,
[MemberId] [int] NOT NULL,
[StreetAddress] [nvarchar](255) NOT NULL,
[Unit] [nvarchar](10) NULL,
[City] [nvarchar](50) NOT NULL,
[StateId] [int] NOT NULL,
[ZipCode] [nvarchar](10) NOT NULL,
 CONSTRAINT [PK_Address] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 

ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO

/****** Object:  Table [dbo].[Member]    Script Date: 8/16/2019 2:57:43 AM 
******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Member](
[Id] [int] IDENTITY(1,1) NOT NULL,
[FirstName] [nvarchar](25) NOT NULL,
[LastName] [nvarchar](25) NOT NULL,
[Active] [bit] NOT NULL,
 CONSTRAINT [PK_Member] PRIMARY KEY CLUSTERED 
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Member] ADD  CONSTRAINT [DF_Member_Active]  DEFAULT ((1)) 
FOR 
[Active]
GO

ALTER TABLE [dbo].[Address]  WITH CHECK ADD  CONSTRAINT [FK_Address_Member] 
FOREIGN KEY([MemberId])
REFERENCES [dbo].[Member] ([Id])
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[Address] CHECK CONSTRAINT [FK_Address_Member]
GO

ALTER TABLE [dbo].[Address]  WITH CHECK ADD  CONSTRAINT [FK_Address_State] 
FOREIGN KEY([StateId])
REFERENCES [dbo].[State] ([Id])
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[Address] CHECK CONSTRAINT [FK_Address_State]
GO

谢谢你的帮助。

标签: c#entity-framework

解决方案


virtual与问题无关(它控制与此处无关的延迟加载行为)。示例 EF 模型与示例数据库模型根本不匹配。在使用现有数据库时,实体模型的正确映射对于 EF 正常运行至关重要。

因为所有 EF 运行时行为(查询生成、插入/更新/删除操作及其执行顺序等)都是基于根据约定、数据注释和流畅配置构建的实体模型,而不是实际的数据库。

让您当前的实体模型有问题(删除所有不必要的显式字段/属性):

public partial class Member
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public bool Active { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    [NotMapped]
    public Address Address { get; set; }
}

public partial class Address
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string City { get; set; }
    public int MemberId { get; set; }
    public int StateId { get; set; }
    public string StreetAddress { get; set; }
    public string ZipCode { get; set; }
    public string Unit { get; set; }
    [NotMapped]
    public bool IsNew => this.Id < 1;
}

PK 和原始属性都可以。Member但是和之间的关系Address不是,这就妨碍了正确保存数据。

在 EF 中,引用另一个实体或另一个实体集合的属性称为导航属性,并表示关系的对应端以及基数(一个或多个)。

在您的情况下,导航属性是Member.Address. [NotMapped]实际上,如果没有属性,它本来会是。使用该属性,您将告诉 EF 在所有操作中忽略该属性,就像它不存在一样。

在这种情况下的效果是,从 EF 的角度来看和之间没有关系,因此 EF 无法正确处理插入操作,这需要获取实际插入的标识值并将其用作依赖实体记录插入中的 FK。从上面生成EF迁移可以看出:AddressMember

CreateTable(
    "dbo.Address",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            City = c.String(),
            MemberId = c.Int(nullable: false),
            StateId = c.Int(nullable: false),
            StreetAddress = c.String(),
            ZipCode = c.String(),
            Unit = c.String(),
        })
    .PrimaryKey(t => t.Id);

CreateTable(
    "dbo.Member",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Active = c.Boolean(nullable: false),
            FirstName = c.String(),
            LastName = c.String(),
        })
    .PrimaryKey(t => t.Id);

如您所见,只有创建带有列的表,但没有 FK 约束。

所以第一步将是删除该[NotMapped]属性。现在真正的问题来了。数据库中对MemberIdcolumn没有唯一约束的FK关系意味着一对多的关系,即一个Member可以有多个Addresses,因此导航属性不能是单个的Address,而是集合:

public partial class Member
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public bool Active { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public ICollection<Address> Addresses { get; set; }
}

现在 EF 迁移完全匹配数据库(注意.ForeignKey声明):

CreateTable(
    "dbo.Address",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            City = c.String(),
            MemberId = c.Int(nullable: false),
            StateId = c.Int(nullable: false),
            StreetAddress = c.String(),
            ZipCode = c.String(),
            Unit = c.String(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.Member", t => t.MemberId, cascadeDelete: true)
    .Index(t => t.MemberId);

CreateTable(
    "dbo.Member",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Active = c.Boolean(nullable: false),
            FirstName = c.String(),
            LastName = c.String(),
        })
    .PrimaryKey(t => t.Id);

现在模型与数据库匹配,您可以创建成员以及一个或多个关联地址。但这需要更改您的代码,该代码似乎假定成员具有零个或一个地址,即一对一的关系。

如果一对一是预期的关系,那么数据库设计是不合适的。对于这种情况(EF 自然支持)最好的是所谓的共享主键关联MemberId,其中的 PKAddress也用作FK,而不是单独的 FK。

这将是 EF 的最佳设计。不幸的是,这需要更改数据库设计,因此不能与您用于比较的通用数据库一起使用。

有一种方法可以保持数据库设计不变并创建所需的一对一映射(尽管在技术上并未在数据库中强制执行)。这需要映射 Address属性Member作为参考导航属性MemberId,从类中删除显式 FK 属性(EF6 限制)并使用流式 APIAddress将 FK 列映射为阴影属性:

public partial class Member
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public bool Active { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    //[NotMapped] <-- remove this
    public Address Address { get; set; }
}

public partial class Address
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string City { get; set; }
    //public int MemberId { get; set; } <-- remove this
    public int StateId { get; set; }
    public string StreetAddress { get; set; }
    public string ZipCode { get; set; }
    public string Unit { get; set; }
    [NotMapped]
    public bool IsNew => this.Id < 1;
}

和内部OnModelCreation覆盖:

modelBuilder.Entity<Member>()
    .HasOptional(e => e.Address)
    .WithRequired()
    .Map(m => m.MapKey("MemberId"))
    .WillCascadeOnDelete(true);

现在这样的代码将同时创建Member和关联Address

var member = new Member
{
    FirstName = "FN1",
    LastName = "LN1",
    Address = new Address
    {
        City = "C1",
        Unit = "U1",
        ZipCode = "ZC1",
        StreetAddress = "SA1",
    }
};
dbContext.Set<Member>().Add(member);
dbContext.SaveChanges();

回顾一下:

EF 对定义关系及其基数的方式更加严格,因为许多运行时行为都依赖于此。为了得到正确的操作处理,正确的映射是必须的。

另外作为旁注,CUD 性能绝对不是 EF 的优势之一,因此将它与另一个库恕我直言进行比较没有多大意义。它还有许多其他优点,如果需要,可以通过一些用于批量插入/更新/删除的第 3 方扩展来显着提高 CUD 操作的性能。


推荐阅读