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






4.75/5 (8投票s)
最常见的问题之一是,是否存在一种方法可以自动决定在 Entity-Framework 中插入、更新或删除实体。现在,使用 EntityGraphOperations 库可以实现这一点。
问题
最常见的问题之一是,是否存在一种方法可以自动决定在 Entity Framework 中插入、更新或删除实体。在 EF 中是不可能实现的。
通常,我们发现自己需要编写非常相似的代码来定义实体的状态。通常,程序如下:
- 确定定义实体在数据库中存在所需的属性(这可以是主键属性或唯一键属性)
- 如果结果为
null
,则必须插入实体。 - 如果结果不为
null
,并且实体的任何部分发生了更改,则必须更新实体。 - 如果我们有一个实体集合,那么我们需要将其与数据库中的实体进行比较,并删除那些不再存在于集合中的实体。
等等……
关于为什么有时除了主键属性之外还需要唯一键属性的额外解释。
假设我们有一个 Phone
实体,它有一些属性:ID、Digits、Prefix
……
ID
是自动生成的主键。同时,我们不希望将相同的电话号码插入到具有不同 ID
的表中。因此,Digits
和 Prefix
属性共同是唯一的。这种情况迫使我们考虑这一点:
如果主键有默认值,并且数据库中没有具有指定 Digits
和 prefix
的相应实体,则必须插入。否则,如果发生更改,则必须更新等等……
现在,让我们对不同的实体图重复同样的操作。一次又一次……
背景
建议了解 Entity Framework 中的 Code First 方法。但是,您必须遵循以下 2 条规则:
-
创建模型时使用外键关联。这意味着,除了相应的导航属性之外,您的模型中必须有一个外键属性。例如:
public class Person { ... public int DocumentId {get; set;} //Foreign-Key property public virtual Document Document { get; set; } // Navigational Property ... }
- 如果您必须在
Person
和Phone
实体类之间配置多对多关系,则必须显式创建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
将自动将实体状态设置为 Added
或 Modified
。之后,我们将定义哪些实体如果不再存在则必须删除。
分步示例项目
先决条件
在开始创建演示应用程序之前,请确保您拥有创建应用程序所需的所有要素。
- 您需要 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 个表
Persons
- ID、名字、姓氏、中间名、年龄、文档 IDDocuments
- ID、文档编号、文档系列Spouses
- 人员 ID、配偶名字、配偶姓氏、配偶中间名Phones
- ID、数字、前缀PersonPhones
- 电话 ID、人员 ID
我希望您已经理解这些表之间的关系。无论如何,我想简要解释一下这种关系。
一个人可以有多个 phone
、一个 document
和一个 spouse
。Digits
和 Prefix
属性共同是唯一的。因此,我们必须确保只插入一次相同的 phone
。如果任何其他人拥有相同的 phone
号码,则 API 将获取并使用其主键。
第 1 步 - 创建控制台应用程序
首先,创建一个名为 EntityGraphOperationsDemo
的全新控制台应用程序。
让我们花一些时间来理解所创建的解决方案。您会注意到的第一件事是创建了一些文件夹和类,请参阅下图:
如您所见,Entities 文件夹包含两个子文件夹:Models 和 Mappings。
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
这不是很棒吗?
我期待您的反馈。