65.9K
CodeProject 正在变化。 阅读更多。
Home

自动确定 EntityFramework 中所有实体在图中的状态

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (8投票s)

2016年9月23日

CPOL

6分钟阅读

viewsIcon

22663

downloadIcon

126

最常见的问题之一是,是否存在一种方法可以自动决定在 Entity-Framework 中插入、更新或删除实体。现在,使用 EntityGraphOperations 库可以实现这一点。

问题

最常见的问题之一是,是否存在一种方法可以自动决定在 Entity Framework 中插入、更新或删除实体。在 EF 中是不可能实现的。

通常,我们发现自己需要编写非常相似的代码来定义实体的状态。通常,程序如下:

  • 确定定义实体在数据库中存在所需的属性(这可以是主键属性或唯一键属性)
  • 如果结果为 null,则必须插入实体。
  • 如果结果不为 null,并且实体的任何部分发生了更改,则必须更新实体。
  • 如果我们有一个实体集合,那么我们需要将其与数据库中的实体进行比较,并删除那些不再存在于集合中的实体。

等等……

关于为什么有时除了主键属性之外还需要唯一键属性的额外解释。

假设我们有一个 Phone 实体,它有一些属性:ID、Digits、Prefix ……

ID 是自动生成的主键。同时,我们不希望将相同的电话号码插入到具有不同 ID 的表中。因此,DigitsPrefix 属性共同是唯一的。这种情况迫使我们考虑这一点:

如果主键有默认值,并且数据库中没有具有指定 Digitsprefix 的相应实体,则必须插入。否则,如果发生更改,则必须更新等等……

现在,让我们对不同的实体图重复同样的操作。一次又一次……

背景

建议了解 Entity Framework 中的 Code First 方法。但是,您必须遵循以下 2 条规则:

  1. 创建模型时使用外键关联。这意味着,除了相应的导航属性之外,您的模型中必须有一个外键属性。例如:

    public class Person
    {
        ...
        public int DocumentId {get; set;} //Foreign-Key property
        public virtual Document Document { get; set; } // Navigational Property  
        ...
    }
  2. 如果您必须在 PersonPhone 实体类之间配置多对多关系,则必须显式创建 PersonPhone 模型。

我将在分步教程中向您展示这两个规则的实践。所以,不用担心。

需要安装什么

我已经在 Github 上发布了存储库。此外,您可以使用 Nuget 轻松下载该库及其依赖项。

Install-Package EntityGraphOperations

此 API 提供什么

我已经在本文的引言中解释了这个问题。解决方案是使用 EntityGraphOperations for Entity Framework Code First

特点

  • 自动定义所有实体的状态
  • 仅更新已更改的实体
  • 属性配置(例如,您可以选择哪些属性不必用于定义实体是否已修改)
  • 支持重复实体
  • 特殊实体配置的 Fluent API 样式映射
  • 在自动确定所有实体状态后,让用户手动管理图

此 API 如何工作?

假设我有一个 Person 对象。Person 可以有多个 phone,一个 Document,并且可以有一个 spouse

public class Person
{
     public int Id { get; set; }
     public string FirstName { get; set; }
     public string LastName { get; set; }
     public string MiddleName { get; set; }
     public int Age { get; set; }
     public int DocumentId {get; set;}

     public virtual ICollection<PersonPhone> PersonPhones { get; set; }
     public virtual Document Document { get; set; }
     public virtual PersonSpouse PersonSpouse { get; set; }
}

我想确定图中包含的所有实体的状态。

context.InsertOrUpdateGraph(person)
       .After(entity =>
       {
            // Delete missing phones.
            entity.HasCollection(p => p.PersonPhones)
               .DeleteMissingEntities();

            // Delete if spouse is not exist anymore.
            entity.HasNavigationalProperty(m => m.PersonSpouse)
                  .DeleteIfNull();
       });

此外,正如您所记得的,唯一键属性在定义 Phone 实体状态时可能会发挥作用。为此类特殊目的,我们有 ExtendedEntityTypeConfiguration<> 类,它继承自 EntityTypeConfiguration<>。如果我们要使用此类特殊配置,则我们的映射类必须继承自 ExtendedEntityTypeConfiguration<>,而不是 EntityTypeConfiguration<>。例如:

public class PhoneMap: ExtendedEntityTypeConfiguration<Phone>
{
    public PhoneMap()
    {
         // Primary Key
         this.HasKey(m => m.Id);

          …

         // Unique keys
         this.HasUniqueKey(m => new { m.Prefix, m.Digits });
    }
}

就是这样!InsertOrUpdateGraph 将自动将实体状态设置为 AddedModified。之后,我们将定义哪些实体如果不再存在则必须删除。

分步示例项目

先决条件

在开始创建演示应用程序之前,请确保您拥有创建应用程序所需的所有要素。

  • 您需要 Visual Studio 2010+
  • 如果您使用 Oracle,建议从 nuget 安装 Oracle.ManagedDataAccess.EntityFramework 包。
    Install-Package Oracle.ManagedDataAccess.EntityFramework
  • 复制并粘贴并执行这些 .sql 文件以创建表(仅当您使用 Oracle 时)

顺便说一句,这是个人偏好,但我只允许 EF Code-First 在数据库位于 MS Sql Server 上时才在数据库中创建表。但是,如果我使用 Oracle,那么我更喜欢自己创建数据库并将 DbContext 类中的初始化器设置为 null。(我将展示我的意思,所以不用担心)。这就是为什么如果您使用 Oracle,我为您提供了创建表的 .sql 文件。

数据库结构

我创建了 5 个表

  1. Persons - ID、名字、姓氏、中间名、年龄、文档 ID
  2. Documents - ID、文档编号、文档系列
  3. Spouses - 人员 ID、配偶名字、配偶姓氏、配偶中间名
  4. Phones - ID、数字、前缀
  5. PersonPhones - 电话 ID、人员 ID

我希望您已经理解这些表之间的关系。无论如何,我想简要解释一下这种关系。
一个人可以有多个 phone、一个 document 和一个 spouseDigitsPrefix 属性共同是唯一的。因此,我们必须确保只插入一次相同的 phone。如果任何其他人拥有相同的 phone 号码,则 API 将获取并使用其主键。

第 1 步 - 创建控制台应用程序

首先,创建一个名为 EntityGraphOperationsDemo 的全新控制台应用程序。

让我们花一些时间来理解所创建的解决方案。您会注意到的第一件事是创建了一些文件夹和类,请参阅下图:

如您所见,Entities 文件夹包含两个子文件夹:ModelsMappings

Models 文件夹包含我们的实体模型。它们的映射详细信息存储在 Mappings 文件夹中。

第 2 步 - 创建我们的模型

正如我在本文的背景部分所解释的那样,在创建实体模型时,我们将使用外键关联技术。

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    string LastName { get; set; }
    public string MiddleName { get; set; }
    public int Age { get; set; }
    public int DocumentId { get; set; }

    public virtual ICollection<PersonPhone> PersonPhones { get; set; }
    public virtual Document Document { get; set; }
    public virtual PersonSpouse PersonSpouse { get; set; }
}

public class Document
{
    public int Id { get; set; }
    public string DocumentNumber { get; set; }
    public string DocumentSeries { get; set; }
}

public class PersonSpouse
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleName { get; set; }

    public virtual Person Person { get; set; }
}

public class Phone
{
    public int Id { get; set; }
    public string Prefix { get; set; }
    public string Digits { get; set; }
}

public class PersonPhone
{
    public int PersonId { get; set; }
    public int PhoneId { get; set; }

    public virtual Person Person { get; set; }
    public virtual Phone Phone { get; set; }
}

而且,如果我们要利用此 API,那么我们的映射类必须继承自 ExtendedEntityTypeConfiguration<T>

public class PersonMap : ExtendedEntityTypeConfiguration<Person>
{
    public PersonMap()
    {
        // Primary Key
        this.HasKey(m => m.Id);

        // Table & Column Mappings
        this.ToTable("PERSONS", "EGO_TEST");

        this.Property(m => m.Id)
            .HasColumnName("ID")
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
            .IsRequired();

        this.Property(m => m.FirstName)
            .HasColumnName("FIRST_NAME")
            .HasMaxLength(60)
            .IsRequired();

        this.Property(m => m.LastName)
            .HasColumnName("LAST_NAME")
            .HasMaxLength(60)
            .IsRequired();

        this.Property(m => m.MiddleName)
            .HasColumnName("MIDDLE_NAME")
            .HasMaxLength(60)
            .IsRequired();

        this.Property(m => m.Age)
            .HasColumnName("AGE")
            .IsRequired();

        this.Property(m => m.DocumentId)
            .HasColumnName("DOCUMENT_ID")
            .IsRequired();

        // Relationships
        this.HasRequired(m => m.Document)
            .WithMany()
            .HasForeignKey(m => m.DocumentId);
    }
}

public class DocumentMap : ExtendedEntityTypeConfiguration<Document>
{
    public DocumentMap()
    {
        // Primary key
        this.HasKey(m => m.Id);

        // Table & Column Mappings
        this.ToTable("DOCUMENTS", "EGO_TEST");

        this.Property(m => m.Id)
            .HasColumnName("ID")
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
            .IsRequired();

        this.Property(m => m.DocumentNumber)
            .HasColumnName("DOCUMENT_NUMBER")
            .HasMaxLength(12)
            .IsRequired();

        this.Property(m => m.DocumentSeries)
            .HasColumnName("DOCUMENT_SERIES")
            .HasMaxLength(3)
            .IsRequired();
    }
}

public class PersonSpouseMap : ExtendedEntityTypeConfiguration<PersonSpouse>
{
    public PersonSpouseMap()
    {
        // Primary key
        this.HasKey(m => m.PersonId);

        // Table & Column Mappings
        this.ToTable("SPOUSES", "EGO_TEST");

        this.Property(m => m.PersonId)
            .HasColumnName("PERSON_ID")
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
            .IsRequired();

        this.Property(m => m.FirstName)
            .HasColumnName("SPOUSE_FIRST_NAME")
            .HasMaxLength(60)
            .IsRequired();

        this.Property(m => m.LastName)
            .HasColumnName("SPOUSE_LAST_NAME")
            .HasMaxLength(60)
            .IsRequired();

        this.Property(m => m.MiddleName)
            .HasColumnName("SPOUSE_MIDDLE_NAME")
            .HasMaxLength(60)
            .IsRequired();

        // Relationships
        this.HasRequired(m => m.Person)
            .WithRequiredDependent(m => m.PersonSpouse);
    }
}

public class PhoneMap : ExtendedEntityTypeConfiguration<Phone>
{
    public PhoneMap()
    {
        // Primary key
        this.HasKey(m => m.Id);

        // Table & Column Mappings
        this.ToTable("PHONES", "EGO_TEST");

        this.Property(m => m.Id)
            .HasColumnName("ID")
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
            .IsRequired();

        this.Property(m => m.Prefix)
            .HasColumnName("PREFIX")
            .HasMaxLength(5)
            .IsRequired();

        this.Property(m => m.Digits)
            .HasColumnName("DIGITS")
            .HasMaxLength(12)
            .IsRequired();

        // Extended Configurations
        // Unique Key
        this.HasUniqueKey(m => new {m.Digits, m.Prefix });
    }
}

public class PersonPhoneMap : ExtendedEntityTypeConfiguration<PersonPhone>
{
    public PersonPhoneMap()
    {
        // Primary Key
        this.HasKey(m => new { m.PersonId, m.PhoneId });

        // Table & Column Mappings
        this.ToTable("PERSON_PHONES", "EGO_TEST");

        this.Property(m => m.PersonId)
            .HasColumnName("PERSON_ID");

        this.Property(m => m.PhoneId)
            .HasColumnName("PHONE_ID");

        // Relationships
        this.HasRequired(m => m.Phone)
            .WithMany()
            .HasForeignKey(m => m.PhoneId);

        this.HasRequired(m => m.Person)
            .WithMany(m => m.PersonPhones)
            .HasForeignKey(m => m.PersonId);
    }
}

第 3 步 - 让我们创建我们的 DbContext 类

我们已经完成了模型及其映射,现在我们可以创建 DbContext 类了。我在 App.config 文件中添加了两个不同的连接字符串。您可以根据您的 RDBMS 使用其中任何一个。

 <connectionStrings>
    <add name="EgoDbConnectionOracle" providerName="Oracle.ManagedDataAccess.Client"
      connectionString="data source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)
      (HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)));password=123;
      persist security info=True;user id=EGO_TEST;"/>

    <add name="EgoDbConnectionMsSqlServer" providerName="System.Data.SqlClient"
      connectionString="data source=FJABIYEV-PC\SQLEXPRESS;initial catalog=EGO_TEST;
      integrated security=True;pooling=False;MultipleActiveResultSets=True;App=EntityFramework"/>
  </connectionStrings>

这是我们的 DbContext 类。没什么特别的。

public class EgoDbContext : DbContext
{
    public EgoDbContext()
        : base("name=EgoDbConnectionMsSqlServer") // connection string name, 
                                // change it to EgoDbConnectionOracle if you are using Oracle.
    {
        this.Configuration.ProxyCreationEnabled = true;
        this.Configuration.LazyLoadingEnabled = true;
        this.Configuration.ValidateOnSaveEnabled = true;
        this.Configuration.AutoDetectChangesEnabled = false;
    }

    static EgoDbContext()
    {
       // Uncomment this, if you are using Oracle
       // Database.SetInitializer<EgoDbContext>(null); 
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Dynamically get all mapping classes.
        var typesToRegister = Assembly.GetExecutingAssembly()
            .GetTypes()
            .Where(type => !String.IsNullOrEmpty(type.Namespace))
            .Where(type => type.BaseType != null && type.BaseType.IsGenericType)
            .Where(type => type.BaseType.GetGenericTypeDefinition() == 
                                    typeof(ExtendedEntityTypeConfiguration<>))
            .Where(type => !type.ContainsGenericParameters);

        // Register mapping classes.
        foreach (var type in typesToRegister)
        {
            dynamic configurationInstance = Activator.CreateInstance(type);
            modelBuilder.Configurations.Add(configurationInstance);
        }

        base.OnModelCreating(modelBuilder);
    }   
}

第 4 步 - 主要对决

就是这样。现在,我们可以测试这个 API 是否像我承诺的那样工作。:)

将此示例代码添加到 Program.cs 中的 Main 方法中,然后运行应用程序。 Program.cs,然后运行应用程序。

#region Let's create a Person entity and check if all datas are inserted correctly.
using (EgoDbContext context = new EgoDbContext())
{
    // Create person entity and set it's properties.
    Person person = new Person()
    {
        FirstName = "Farhad",
        LastName = "Jabiyev",
        MiddleName = "John",
        Age = 25,
        Document = new Document
        {
            DocumentNumber = "11112222",
            DocumentSeries = "POL"
        },
        PersonSpouse = new PersonSpouse
        {
            FirstName = "Kate",
            MiddleName = "Tina",
            LastName = "Jabiyev"
        }
    };

    person.PersonPhones = new List<PersonPhone>
    {
        new PersonPhone { Phone = new Phone { Digits="57142", Prefix="14"} },
        new PersonPhone { Phone = new Phone { Digits="57784478", Prefix="50"} }
    };

    context.InsertOrUpdateGraph(person);

    context.SaveChanges();
}
#endregion

所有实体都将被添加到数据库中。请去检查一切是否正常。

现在,所有实体的状态都已添加,因为我们的数据库是空的。所以,让我们注释掉这个 region,做一些更改,然后重新运行应用程序。

#region Now, let's update a Person entity and check if all datas are affected correctly.
using (EgoDbContext context = new EgoDbContext())
{
    Person person = new Person()
    {
        Id = 1,
        FirstName = "New Farhad", // Name has been changed - It must be updated
        LastName = "Jabiyev",
        MiddleName = "John",
        Age = 26, // Age has been changed - It must be updated
        Document = new Document
        {
            DocumentNumber = "11112222",
            DocumentSeries = "USA"
            // Series has been changed, so new Document entity will be inserted and 
            // DocumentId property will me modified in the Person entity
        },
        // Spouse does not exist anymore - It must be deleted
        //PersonSpouse = new PersonSpouse 
        //{
        //    FirstName = "Kate",
        //    MiddleName = "Tina",
        //    LastName = "Jabiyev"
        //}
    };

    person.PersonPhones = new List<PersonPhone>
    {
        // new PersonPhone { Phone = new Phone { Digits="57142", Prefix="14"} }, // It must be deleted
        new PersonPhone { Phone = new Phone 
                   { Id = 2, Digits="68745877", Prefix="50"} }, // It must be updated
        new PersonPhone { Phone = new Phone { Digits="66658", Prefix="14"} }, // It must be inserted
    };

    context.InsertOrUpdateGraph(person)
        .After(entity =>
        {
            // Delete missing phones.
            entity.HasCollection(p => p.PersonPhones)
               .DeleteMissingEntities();

            // Delete if spouse is not exist anymore.
            entity.HasNavigationalProperty(m => m.PersonSpouse)
                  .DeleteIfNull();
        });

    context.SaveChanges();
}
#endregion

请运行应用程序。API 将再次执行这些查询到数据库:

  • 更新人员的名字和年龄
  • 从表中删除配偶
  • 向表中插入新文档
  • 根据新插入的文档更新 Person 实体的文档 ID
  • 从表中删除集合中不存在的 PersonPhones
  • 更新具有主键和不同数字/前缀对的 Phone

这不是很棒吗?

我期待您的反馈。

在 EntityFramework 中自动定义图中所有实体的状态 - CodeProject - 代码之家
© . All rights reserved.