EF6数据库表的创建和迁移 电脑版发表于:2021/6/3 23:14 ![](https://img.tnblog.net/arcimg/hb/f170e5179e8f44c18b1cd575d5276c66.png) >#EF6数据库连接和初始化策略 [TOC] 数据库连接 ------------ tn>创建一个控制台应用程序 ###安装EF6 ![](https://img.tnblog.net/arcimg/hb/c4defa8b4b744514b0ed8373dfa255d8.png) ###创建代码内容 >Blog类 ```csharp public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public DateTime? CreatedTime { get; set; } public double Double { get; set; } public float Float { get; set; } } ``` tn>创建一个EF上下文实例类,我们可以称为会话,并且为每一个模型公开一个`IDbSet`。 >EfDbContext类 ```csharp public class EfDbContext:DbContext { public IDbSet<Blog> Blogs { get; set; } } ``` ###添加连接字符串 tn>在VS工具栏中根据下面的路径连接并创建数据库的实例 ```mermaid graph LR A(视图) -->B(服务器资源管理器) B(服务器资源管理器) -->C(右键创建新的数据库) ``` ![](https://img.tnblog.net/arcimg/hb/efb176c77d34409c81fd190159826695.png) ![](https://img.tnblog.net/arcimg/hb/ecbbaf82131e4f85959a09958b41a9cf.png) ![](https://img.tnblog.net/arcimg/hb/52d2ec7c04a74b169192d14153bb79f3.png) tn>将连接字符串放到`App.config`或`Web.config`中,并将连接字符串取名为`ConnectionString` ```xml <!-- 连接字符串 --> <connectionStrings> <add name="ConnectionString" connectionString="Data Source=CNLT1824\CNLT1824;Initial Catalog=EF6Learning;User ID=sa;Password=cn@49akING;Pooling=False" providerName="System.Data.SqlClient" /> </connectionStrings> ``` ![](https://img.tnblog.net/arcimg/hb/82b84929b409471e9330fc07148e0a88.png) >为`EfDbContext`上下文实例指定连接字符串 ```csharp public class EfDbContext:DbContext { public EfDbContext():base("name=ConnectionString"){} public IDbSet<Blog> Blogs { get; set; } } ``` ###添加数据 ```csharp class Program { static void Main(string[] args) { using (var efDbContext = new EfDbContext()) { efDbContext.Blogs.Add(new Blog() { Name = "Hemingyang", Url = "http://www.tnblog.net" }); efDbContext.SaveChanges(); } } } ``` tn>运行后,在数据库中已经创建好了相应的表。 ![](https://img.tnblog.net/arcimg/hb/a6d17b76529643d4b5d78b9367612e0b.png) 数据库初始化策略 ------------ ### 代码初始化策略 tn>在EF中初始化数据库有三种策略,需要在EF上下文派生类的构造函数中定义。 >- 如果数据库不存在,就创建: ```csharp Database.SetInitializer(new CreateDatabaseIfNotExists<EfDbContext>()); ``` - 总是创建数据库,无论存在与否 ```csharp Database.SetInitializer(new DropCreateDatabaseAlways<EfDbContext>()); ``` - 如果EF检测到数据库模型发生了改变,将更新模型: ```csharp Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfDbContext>()); ``` ### 配置文件设置初始化策略 ```xml <appSettings> <add key="DatabaseInitializerForTypeEntityFramework_CreateDbContext.EfDbContext, EntityFramework_CreateDbContext" value="System.Data.Entity.DropCreateDatabaseAlways, EntityFramework"/> </appSettings> ``` tn>上述 `key` 值 `DatabaseInitializerForType`后面紧跟命名空间+DbConext派生类,其余两种初始化策略同理。如果想要禁用数据库初始化策略,那么在`DbContext`派生类和配置文件中该如何配置呢?代码如下: ```csharp public class EfDbContext:DbContext { public EfDbContext():base("name=ConnectionString"){ Database.SetInitializer<EfDbContext>(null); } public IDbSet<Blog> Blogs { get; set; } } ``` ```xml <appSettings> <add key="DatabaseInitializerForTypeEntityFramework_CreateDbContext.EfDbContext, EntityFramework_CreateDbContext" value="Disabled"/> </appSettings> ``` >#约定 类型发现 ------------ ### 创建Department类 >我们先定义一个`Department`类,并在`EfDbContext`上下文中通过定义一个`IDbSet`属性来进行暴露模型的一部分,当然我们还可以通过`Fluent API`来忽略将其映射到数据库中。 ```csharp public class Department { /// <summary> /// 主键 /// </summary> public int DepartmentID { get; set; } public string Name { get; set; } } ``` ```csharp public class EfDbContext:DbContext { public EfDbContext():base("name=ConnectionString"){ // 总是创建数据库,无论存在与否 Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfDbContext>()); } public IDbSet<Blog> Blogs { get; set; } public IDbSet<Department> Departments { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // 忽略映射 modelBuilder.Ignore<Department>(); base.OnModelCreating(modelBuilder); } } ``` ```csharp // 测试代码 using (var efDbContext = new EfDbContext()) { efDbContext.Departments.Add(new Department() { Name = "Hemingyang" }); efDbContext.SaveChanges(); } ``` ![](https://img.tnblog.net/arcimg/hb/94847c80fe414619b3e6b9f0c0a11ab8.png) >接着我们在数据库中,发现并没有相关的表创建 ![](https://img.tnblog.net/arcimg/hb/42864a67f8b145b48a765a2b1d677477.png) 主键约定 ------------ tn>Code First 根据模型中定义的ID,或者是以类名加上ID的属性,推断这样的属性为主键,如果主键为`int`或者`guid`类型,那么主键将会被映射成标识列(自增长)。在`Department`中的`DepartmentID`将被映射成主键且自增长 当我们定义一个学生地址类(`StudentAddress`),在里面定义两个ID试试 ```csharp public class StudentAddress { public int AddressID { get; set; } public int StudentID { get; set; } } ``` ```csharp public class EfDbContext:DbContext { public EfDbContext():base("name=ConnectionString"){ // 总是创建数据库,无论存在与否 Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfDbContext>()); } public IDbSet<Blog> Blogs { get; set; } public IDbSet<Department> Departments { get; set; } public IDbSet<StudentAddress> StudentAddresss { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // modelBuilder.Ignore<Department>(); base.OnModelCreating(modelBuilder); } } ``` ![](https://img.tnblog.net/arcimg/hb/0082add920bb4a30937dc06755d6ce52.png) tn>上述错误信息表明`StudentAddress`未定义主键,主键必须是**ID(不区分大小写)或者是类名加上ID**,`StudentAddress`主键的定义必须是`StudentAddressID`或者`ID`,否者将抛出异常。 ```csharp public class StudentAddress { public int StudentAddressID { get; set; } public int StudentID { get; set; } } ``` ![](https://img.tnblog.net/arcimg/hb/7275ef3e21ee417aaec958c159b940ac.png) 关系约定 ------------ >在EF中,导航属性可以为两个实体模型建立联系。 - 定义导航属性 - 有依赖对象的类型上建立外键属性 tn>导航属性的定义如下 | | | ------------ | | <导航属性名称><主体主键属性名称> | | <主体类名><主键属性名称> | | <主体主键属性名称> | >若有多个匹配,则根据上述列举的优先级进行外键属性映射,同样对于外键属性的检测不区分大小写,当推断出外键属性后,接着根据外键属性的可空性来推断关系的形式,如果外键属性为空,那么模型关系将配置为可选(Code First 不会在关系上配置级联删除),也就是说当主体被删除时,外键将被设置为级联删除。例如: ```csharp public class Department { /// <summary> /// 主键 /// </summary> public int DepartmentID { get; set; } public string Name { get; set; } public virtual ICollection<Course> Courses { get; set; } } public class Course { /// <summary> /// 主键 /// </summary> public int CourseID { get; set; } public string Title { get; set; } public int Credits { get; set; } /// <summary> /// 外键 /// </summary> public int DepartmentID { get; set; } /// <summary> /// 导航属性 /// </summary> public virtual Department Department { get; set; } } ``` 复杂类型的约定 ------------ tn>当Code First不能推断出一个模型有没有主键且主键没有通过Data Annotations或者Fluent API来进行手动配置时,该类型将自动被配置为复杂类型,复杂类型检测同时也要求该类型没有引用实体类型的属性,例如: ```csharp public class Order { public int Id { get; set; } public string Name { get; set; } public Address OrderAddress { get; set; } public class Address { public string Street; public string Region; public string Country; } } ``` tn>我们可以这样理解复杂类型:一个复杂类型(Address)作为已存在对象的属性,但是会将复杂类型的列映射到已存在的表(Order)表中,已存在的表包含这些列而不是将复杂类型映射成另外单独的一张表(单独映射成表还需要主键)。 我们可以进行如下映射: ```csharp public IDbSet<Order> Orders { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Order>().ToTable("Orders"); modelBuilder.ComplexType<Address>(); base.OnModelCreating(modelBuilder); } ``` tn>虽然按照官方的说法去进行映射该复杂类型,但貌似在EF Code First中并不可取。 自定义约定 ------------ tn>从EF 6.0后,我们可以通过自定义约定来确定所有实体和属性的规则。 ###全局约定 >如果我们希望在项目中,所有实体的主键都叫ID,在重写的`OnModelCreating`方法里面我们可以这样写: ```csharp modelBuilder.Properties() .Where(x => x.Name == "Id") .Configure(p => p.IsKey()); ``` >还可以通过自定义类进行约定 ```csharp public class CustomKeyConvention:Convention { public CustomKeyConvention() { Properties() .Where(x => x.Name == "Id") .Configure(p => p.IsKey()); } } ``` ```csharp protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Add<CustomKeyConvention>(); } ``` >还可以通过`Properties`方法对所有模型中属性为`decimal`的类型,配置位数为10位保留2位小数进行全局映射: ```csharp modelBuilder.Properties<decimal>() .Configure(config => config.HasPrecision(10, 2)); ``` ### 执行约定 >当我们对多个属性进行相同的约定配置时,最后一个约定将覆盖前面所有相同的约定。举例: ```csharp modelBuilder.Properties<string>() .Configure(c => c.HasMaxLength(500)); modelBuilder.Properties<string>() .Where(x => x.Name == "Name") .Configure(c => c.HasMaxLength(300)); //除了名为Name的字符串长度300以外,其他字符串长度都为500 ``` ### 内置约定 >为了保证自定义约定执行规则的先后顺序,提供了两个方法`AddBefore`和`AddAfter` ```csharp modelBuilder.Conventions.Add<CustomKeyConvention>(); // 定义在CustomKeyConvention约定之前执行该约定 modelBuilder.Conventions.AddAfter<CustomKeyConvention>(new DateTime2Convention()); // 定义在CustomKeyConvention约定之后执行该约定 modelBuilder.Conventions.AddBefore<CustomKeyConvention>(new DateTime2Convention()); ``` tn>关于`DateTime2Convention`将在下面的自定义类约定呈现代码 ### 自定义类约定 >自定义约定包含一个约定接口(`IConvention`),`IConceptualModelConvention`为概念模型接口,在模型创建后被调用。`IStoreModelConvention`接口为存储模型接口,在模型创建后用于操作对模型的存储,自定义的约定都必须在重载的`OnModelCreating`方法中显示配置。若我们把Datetime类型转换为datetime2,则进行如下操作: ```csharp public class DateTime2Convention : Convention { public DateTime2Convention() { Properties<DateTime>() .Configure(c => c.HasColumnType("datetime2")); } } ``` ```csharp modelBuilder.Conventions.Add<DateTime2Convention>(); ``` ### 自定义类型约束 tn>通常不同的公司对命名都有一定的规范,所以我们也可以为不同的实体对象定制一些规则。这里我们以下划线隔开,对此我们可以通过调用`Types`方法更改表名约定。 ```csharp protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Add<CustomKeyConvention>(); modelBuilder.Types() .Configure(c => c.ToTable(GetTableName<Blog>())); } private string GetTableName<T>() { return GetTableName(typeof(T)); } private string GetTableName(Type type) { var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]); return result.ToLower(); } ``` ![](https://img.tnblog.net/arcimg/hb/9544757a84f94261a17c9943365a89aa.png) ### 自定义特性 tn>约定最大的一个用途在于:当配置模型时,启动的新的属性。