CodeFirst与EntityFramework
阅读原文时间:2023年07月16日阅读:5

项目添加EntityFramework命令:Install-Package EntityFramework

CodeFirst默认规则
1. 数据库映射:Code First 默认会在本地的SQL Expression数据库中建立一个和DbContext的子类的全名相同的数据库,全名指的是命名空间加上类名。当然后边会介绍怎么进行配置。
2.表映射:Code First 默认会按照类型名复数建立数据表,比如说ProductCatalog类对应的表名就叫ProductCatalogs.后边会介绍如何改变默认的表名。
3.列映射:Code
First
默认会按照类中的属性名建立column,它还有默认的数据类型映射习惯,int会映射为interger,string会映射为nvarchar(max),decimal会映射为decimal(18,2)。后边会介绍如何更改column的名称,类型以及其他特性。
4.主键映射:Code First 默认会在类的属性中需找名字为Id或类型名称+Id的int类型的属性作为主键,并且是自增字段。这些也是可以改的。

Code First有两种配置数据库映射的方式,一种是使用数据属性DataAnnotation,另一种是Fluent API.

另外一种配置的方式是使用Fluent
API,Code First Fluent API 是在DbContext中定义数据库配置的一种方式。要使用Fluent API
就必须在你自定义的继承自DbContext的类中重载OnModelCreating这个方法。这个方法的签名如下:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
通过modelBuilder这个对象的Entity<>泛型方法来配置你的DbContext中的每个类的数据库映射。

Fluent API优势:
1、可以设置一对多,多对多主外键关系;
2、可以设置外键名称;
3、可以设置级联删除;

我们可以通过Fluent API 配置数据表的名字:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   modelBuilder.Entity().ToTable(“CustomerInfo”);
}

配置数据表中各column的属性:
modelBuilder的Entity方法的返回值是EntityTypeConfiguration类,可以通过它的Property方法,并且通过lamda表达式设置数据表中指定列的属性。

我们现在可以看看怎样通过一些设定属性的方法来配置我们的数据表的列。
IsRequired():通过这个方法指定该列是not-null的。
HasMaxLength():设定nvarchar列的最大字符数。
HasPercision(percison,scale):设定decimal列的最大值和小数点后位数。
HasColumnType(“TypeName”):设定列的类型,但是指定的列的类型必须与类中的属性的类型相兼容。这个兼容规则后面将详细介绍。
HasDatabaseGeneratedOption(DatabaseGeneratedOption):指定列是否是自增长列。
DatabaseGeneratedOption有三个选项:Idnetity:自增长
None:非自增长
Computed:用于一些通过计算得到值的列。
modelBuilder的Entity方法的返回值还有一个HasKey方法用于设置数据表的主键。

例如:
public class OrderSystemContext:DbContext
{
    public DbSet Customers { get; set; }
  
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {           
   
    modelBuilder.Entity().HasKey(c =>
c.IDCardNumber).Property(c =>
c.IDCardNumber).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        modelBuilder.Entity().Property(c => c.IDCardNumber).HasMaxLength(20);
        modelBuilder.Entity().Property(c => c.CustomerName).IsRequired().HasMaxLength(50);
        modelBuilder.Entity().Property(c => c.Gender).IsRequired().HasMaxLength(1);
        modelBuilder.Entity().Property(c => c.PhoneNumber).HasMaxLength(20);
    }
}

改进:通过EntityTypeConfiguration配置单独配置domain中每个类的数据库映射:
public class CustomerEntityConfiguration:EntityTypeConfiguration
{
    public CustomerEntityConfiguration()
    {
   
    HasKey(c => c.IDCardNumber).Property(c =>
c.IDCardNumber).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        this.Property(c => c.IDCardNumber).HasMaxLength(20);
        this.Property(c => c.CustomerName).IsRequired().HasMaxLength(50);
        this.Property(c => c.Gender).IsRequired().HasMaxLength(1);
        this.Property(c => c.PhoneNumber).HasMaxLength(20);
    }
}

那么。改变了映射规则,如何同步到数据库对应的表呢?
答案:Database类的SetInitializer方法

通过Code First提供的Database类的SetInitializer方法设定Code First如何根据Fluent API数据库映射配置初始化数据库。
每次AppDomain加载的时候会执行SetInitializer指定的初始化方法。

SetInitializer方法的参数可以使以下三个泛型类的对象:
CreateDatabaseIfNotExists<>:只有在没有数据库的时候才会根据数据库连接配置创建新的数据库。这种配置主要用于production环境,因为你不可能把你现在使用的数据库删除掉,那样会损失重要的数据。你需要让你的实施人员拿着与Fluent
API配置对应的数据库脚本去更新数据库。

DropCreateDatabaseIfModelChanges<>:只要Fluent

API配置的数据库映射发生变化或者domain中的model发生变化了,就把以前的数据库删除掉,根据新的配置重新建立数据库。这种方式比较适合开发数据库,可以减少开发人员的工作量。

DropCreateDatabaseAlways<>:不管数据库映射或者model是否发生变化,每次都重新删除并根据配置重建数据库。这种方式可以适用于一些特殊情况的测试,比如说当每次测试结束之后把所有的测试数据都删除掉,并且在测试开始前插入一些基础数据。

一般Database.SetInitializer方法都是在应用程序的入口,比如Global.ascx.cs,Main方法等地方调用的。
但是通过代码调用设置数据库的初始化方式并不是很方便,因为每种初始化方式都应用于不同的场合,当我们从开发环境变化到production环境时,肯定会使用不同的初始化方式,比如说从DropCreateDatabaseIfModelChanges变为CreateDatabaseIfNotExists。如果每次变化都要重新改代码,重新编译的话,太不方便了。

我们可以通过配置指定数据库初始化的方式,这样就可以更灵活的改变我们的初始化方式:

自定义数据库初始化类--定制初始化数据(如产品类别、角色名称等公用数据):
假设我们在测试环境中测试对Product类的相关操作,我们需要一些ProductCatalog的基础数据,因为Product中有一个Productcatalog的引用。我们可以定义一个自己的数据库初始化类,继承DropCreateDatabaseAlways,让Code
First每次在执行测试之前都删除掉原来的数据库并且插入一些ProductCatalog的测试数据。
例如:
public class DropCreateOrderDatabaseWithSeedValueAlways : DropCreateDatabaseAlways
{
   protected override void Seed(OrderSystemContext context)
   {
   
   context.ProductCatalogs.Add(new ProductCatalog { CatalogName = "DELL
E6400", Manufactory = "DELL", ListPrice = 5600, NetPrice = 4300 });
   
   context.ProductCatalogs.Add(new ProductCatalog { CatalogName = "DELL
E6410", Manufactory = "DELL", ListPrice = 6500, NetPrice = 5100 });
   
   context.ProductCatalogs.Add(new ProductCatalog { CatalogName = "DELL
E6420", Manufactory = "DELL", ListPrice = 7000, NetPrice = 5400 });
   }
}

我们在测试类的测试初始化方法中就可以指定Code First使用我们自定义的初始化类进行数据库的初始化:
Database.SetInitializer(new DropCreateOrderDatabaseWithSeedValueAlways());

然后我们就可以使用初始化时插入的基础数据进行我们的测试了:
[TestMethod]
public void CanAddProduct()
{
    OrderSystemContext unitOfWork = new OrderSystemContext();
    ProductRepository repository = new ProductRepository(unitOfWork);
    ProductCatalog catalog = repository.SearchProductCatalog(c => c.CatalogName == "DELL E6400", 1, 10)[0];
   
Product product = new Product { Catalog = catalog, CreateDate =
DateTime.Parse("2010-2-10"), ExpireDate = DateTime.Parse("2012-2-10") };
    repository.AddNewProduct(product);
    unitOfWork.CommitChanges();
}

DbContext的数据库配置(两种方法):
默认情况下,Code

First会在连接字符串的配置节中寻找与你自定的DbContext的子类同名的连接字符串。例如你自定义的DbContext的子类叫OrderSystemContext,
那么name是OrderSystemContext的连接字符串就是你程序中要访问的数据库的连接字符串。

方法一:
如果我们想改变数据库连接字符串的名字,而不用改变我们自定义的DbContext子类,我们就可以使用DbContext的另一个构造函数
public OrderSystemContext(string databaseName): base(databaseName){}
我们可以将连接字符串的名字作为参数传到DbContext子类的构造函数中,然后我们就可以让我们的DbContext子类和连接字符串的名字脱离关系了.
App.Config

例如:
OrderSystemContext unitOfWork = new OrderSystemContext("OrderSystem");

方法二:
还有一个更简单的方式,如果你想使用固定的连接字符串的名字而不是通过参数把名字传进来,可以直接在默认构造函数中调用基类的构造函数,传入固定的连接字符串名称:
public OrderSystemContext()
            :base("OrderSystem"){}

多个DbContext如何保证共用一个DbConnection:
如果我们程序中同时存在多个DbContext的子类的时候,Entity

Framework会为每个DbContext的子类都创建一个数据库连接,那么我们的程序就保留了很多个数据库连接的实例。如果我们要优化我们的程序,就可以让多个DbContext子类使用同一个数据库连接实例。我们还是要通过重载DbContext的另一个构造函数来实现这个功能。

DbContext的这个构造函数有两个参数,一个是我们要共用的数据库连接的实例,另一个是一个开关,用于控制是否由当前的DbContext子类控制数据库连接实例。如果contextOwnsContext属性为true,那么当DbContext子类Dispose的时候,就会把共用的DbConnection实例也Dispose掉。如果这个值是false,则数据库连接实例就需要由程序员自己写程序释放。

public OrderSystemContext(DbConnection connection, bool contextOwnsConnection)
            : base(connection, contextOwnsConnection){}

EF中的值对象(Value Object):

所谓的值对象就是一些没有生命周期,也没有业务逻辑上唯一标识符的类。哪些类是Entity,哪些类是Value

Object不是固定的,取决于具体的业务逻辑。比如说Customer这个类,如果在CRM系统当中,它是最重要的信息,我们需要跟踪它的状态,管理它的生命周期。但是在其他系统中,客户信息可能只代表一个名字和一些其他的属性。
值对象应该是不可修改的,因为它只代表一些临时的属性集合,并没有生命周期需要维护。

我们的订单管理系统中有一个Customer类。在我们以前的示例中,Customer类的Address属性是一个字符串。现在我们的业务逻辑发生了改变,需要把地址信息分类显示和保存,把地址信息细分为国家,省,城市,街道及门牌号,还有邮政编码。在我们的业务中,地址信息仅仅是一些属性的集合,不需要跟踪它的生命周期,也不存在业务中的唯一标识符。所以我们把它定义为一个Value
Object:
例如:
public class Address
{
    public string Country { get; set; }
    public string Province { get; set; }
    public string City { get; set; }
    public string StreetAddress { get; set; }
    public string ZipCode { get; set; }
}

想要让Entity Framework Code First默认地识别出值对象,我们的类必须具备三个条件:
1.值对象类不能有主键。
2.值对象类只能包含.net基础类型的属性。
3.使用值对象的类,只能包含值对象的一个实例,不能使用值对象的集合。

如果值对象不满足三个默认的值对象识别条件,我们就需要在我们自定义的DbContext类的OnModelCreating方法中声明Address是一个值对象。
modelBuilder.ComplexType

();

然后我们需要改变我们的Customer类,使用Address值对象来替代以前的字符串。
public class Customer
{
    public string IDCardNumber { get; set; }
    public string CustomerName { get; set; }
    public string Gender { get; set; }
    public Address Address { get; set; }
    public string PhoneNumber { get; set; }
}

然后,我们使用可插入基础数据的DropCreateOrderDatabaseWithSeedValueAlways自定义数据库初始化类,每次执行测试方法之前都重新建立数据库。
[TestInitialize]
public void InitializeCustomerRepositoryTest()
{
   Database.SetInitializer(new DropCreateOrderDatabaseWithSeedValueAlways());
}
测试程序执行之后的Customer表结构如下:

我们可以看到Entity Framework Code First默认会把Address值对象的属性作为Customers表的列。列的名字默认是值对象类的名字+“_”+值对象属性的名字。
我们可以改变Entity Framework Code First默认的处理方式。
我们可以定义一个继承自ComplexTypeConfiguration

的类,在这个类中重载Code First对值对象的默认处理方式。我们可以改变那些列的名字和它们的数据类型和长度。
public class AddressComplexTypeConfiguration:ComplexTypeConfiguration

{
    public AddressComplexTypeConfiguration()
    {
        Property(a => a.Country).HasColumnName("Country").HasMaxLength(100);
        Property(a => a.Province).HasColumnName("Province").HasMaxLength(100);
        Property(a => a.City).HasColumnName("City").HasMaxLength(100);
        Property(a => a.StreetAddress).HasColumnName("StreetAddress").HasMaxLength(500);
        Property(a => a.ZipCode).HasColumnName("ZipCode").HasMaxLength(6);
    }
}
我们通过HasColumnName这个方法,可以改变属性对应的列的名字。Entity Framework Code First默认会使用类中属性的名字作为列的名字。
重新执行我们的测试方法,Customer表的结构如下:

Fluent API设置表的关联关系:
1、实现一对多的关系。
如果两个类互相包含另一个的实例或实例的集合,那么Code First就会默认为这两个表之间有一对多的关系,包含实例集合的类为主表,包含单个实例的类为子表。
其实在两个类中,只要有一个类包含了另一个类的实例,Code First就可以按照为我们建立数据表之间的一对多关系(只限制于有且只有一个外键关联的情况,多个外键的情况下面将会讲到)。

i、Fluent API更改外键的nullable属性
我们从上面的数据库结构中可以看出,CodeFirst默认为我们建立的外键是可以为null的,但是按照我们的业务逻辑,OrderItem是属于某个Order的,不可能存在单独的OrderItem。我们可以通过Fluent
API使数据库的结构和我们的业务需求一致。我们可以按照以前使用Fluent
API进行配置时使用的方法,定义一个继承了EntityTypeConfiguration泛型类的子类对Order类相关的数据库映射进行配置。

public class OrderEntityConfiguration: EntityTypeConfiguration
{
    public OrderEntityConfiguration()
    {
        this.HasMany(order => order.OrderItems).WithRequired(item => item.Order);
    }
}
Has…With…方法说明:
    HasMany:表示Order类应该包括OrderItem实例的集合;
    HasRequired:表示Order类应该包括OrderItem的一个不为null的实例;
    HasOptional:表示Order类应该包括OrderItem的一个实例或是null;

WithMany:表示OrderItem应该包含Order类实例的集合;
    WithRequired:表示OrderItem类必须包含一个不为null的Order类的实例;
    WithOptional:表示OrderItem类可以包含一个Order类的实例或是null;
   
ii、Fluent API设置外键的名字
我们还可以通过Fluent API设置外键的名字。我们可以通过在Has…With…方法设置主外键关系之后调用Map方法,设置外键的名字:
this.HasMany(order => order.OrderItems).WithRequired(item => item.Order).Map(o => o.MapKey("OrderId"));

iii、Fluent API建立两个一对多数据表之间的多个外键
如果两个表之间存在多个一对多关系,Code First默认无法处理这种情况(存在一个一对多关系可以默认处理)。所以我们必须用Fluent API帮助Code First建立表之间的主外键关系。
例如:
public class SalesPersonValueObjectConfiguration: EntityTypeConfiguration
{
    public SalesPersonValueObjectConfiguration()
    {
        HasKey(p => p.EmployeeID).Property(p => p.EmployeeID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        Property(p => p.Name).IsRequired().HasMaxLength(100);
        Property(p => p.Gender).IsRequired().HasMaxLength(1);
        HasMany(p => p.CreatedOrders).WithOptional(o => o.CreatedBy).Map(p => p.MapKey("CreatedBy"));
        HasMany(p => p.ApprovedOrders).WithOptional(o => o.ApprovedBy).Map(p => p.MapKey("ApprovedBy"));
    }
}

iiii、如果两个表之间存在一对多关系,Code First默认会开启两个表之间的级联删除功能。
我们可以通过WillCascadeOnDelete方法将级联删除功能关闭掉。
 this.HasMany(order => order.OrderItems)
                .WithRequired(item => item.Order)
                .Map(o => o.MapKey("OrderId"))
                .WillCascadeOnDelete(false);
               
2、    实现多对多的关系。
i、 Code First建立多对多关系时的默认配置:
假设我们的业务领域再次发生了改变,客户要求我们记录每种产品的打折信息。根据我们和客户的交谈,我们可以得到如下的业务逻辑:
每个产品种类需要保留所有和它相关的打折促销记录。每次打折促销都有固定的开始时间,结束时间,促销的产品列表以及打折的折扣率。
我们根据这个业务领域的变化重新改写了我们的ProductCatalog类:
public class ProductCatalog
    {
        public int ProductCatalogId { get; set; }
        public string CatalogName { get; set; }
        public string Manufactory { get; set; }
        public decimal ListPrice { get; set; }
        public decimal NetPrice { get; set; }
        public List ProductInStock { get; set; }
        public List SalesPromotionHistory { get; set; }

public ProductCatalog()
        {
            SalesPromotionHistory = new List();
            ProductInStock = new List();
        }
        //其他方法略。。
    }
我们也根据业务领域定义了我们的SalesPromotion类:
public class SalesPromotion
{
    public int SalesPromotionId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public decimal SalesDiscount { get; set; }
    public List PromotionProductCatalog { get; set; }

public SalesPromotion()
    {
        PromotionProductCatalog = new List();
    }

public void AddProductCatalogToPromotion(ProductCatalog catalog)
    {
        catalog.SalesPromotionHistory.Add(this);
        PromotionProductCatalog.Add(catalog);
    }
}                
我们可以写一次单元测试方法,测试一下Entity Framework Code First会根据类的定义建立出怎样的数据表关系。
[TestMethod]
public void CanAddSalesPromotion()
{
    OrderSystemContext unitOfWork = new OrderSystemContext();
    ProductRepository repository = new ProductRepository(unitOfWork);
    ProductCatalog catalog = repository.GetProductCatalogById(1);
   
SalesPromotion promotion = new SalesPromotion { StartDate =
DateTime.Parse("2013-1-18"), EndDate = DateTime.Parse("2013-1-25"),
SalesDiscount = 0.75M };
    promotion.AddProductCatalogToPromotion(catalog);
    unitOfWork.CommitChanges();
}
我们打开数据库看一下,看看Entity Framework Code First从类的定义映射出来的数据库结构是怎样的?    
如图:

大家可以看到由于我们的SalesPromotion和ProductCatalog类中都包含对方类的实例集合,在这种情况下,Entity
Framework Code First默认地会将类之间的引用关系映射为数据库表之间的多对多关系。Entity
Framework会建立一个多对多的连接表,表的名字是两个关联表的名字加在一起,然后按照它自己的规则去给你加上复数形式。表中的两个字段,既是引用两个关联表的外键,并且也作为这个新的连接表的联合主键。

但是大家可能会发现连接表的名字和连接表中字段的名字都是Entity Framework Code First按照自己的规则定义的,我们可以对它们进行修改。

ii、Fluent API更改多对多默认配置:
我们可以通过Has和With方法去配置连接表的名字和连接表中两个外键列的名字:
public class SalesPromotionEntityConfiguration:EntityTypeConfiguration
{
    public SalesPromotionEntityConfiguration()
    {
        Property(p => p.SalesDiscount).HasPrecision(18, 4);
        HasMany(p => p.PromotionProductCatalog)
            .WithMany(ca => ca.SalesPromotionHistory)
            .Map(r =>
                {
                    r.ToTable("ProductCatalogSalesPromotion");
                    r.MapLeftKey("PromotionId");
                    r.MapRightKey("CatalogId");
                }
        );
    }
}    
由于我们是对多对多关系产生的数据库结构进行配置,所以我们需要使用HasMany WithMany。然后我们通过Map方法,可以指定多对多连接中连接表的名字以及连接表中两个联合主键即外键的名字。
我们重新执行我们的单元测试程序,我们可以发现新建的连接表和列的名字都是我们设置的名字。    

Entity Framework Code First如何处理类之间的继承关系。
Entity Framework Code First有三种处理类之间继承关系的方法,我们将逐一介绍这三种处理方法。
1.Table Per Hierarchy(TPH): 只建立一个表,把基类和子类中的所有属性都映射为表中的列。
2.Table Per Type(TPT): 为基类和每个子类建立一个表,每个与子类对应的表中只包含子类特有的属性对应的列。
3.Table Per Concrete Type(TPC):为每个子类建立一个表,每个与子类对应的表中包含基类的属性对应的列和子类特有属性对应的列。

内容来源:
http://www.cnblogs.com/lk8167/archive/2013/01/09/2853721.html