前言 本篇作为EF 7.0的开篇也是Entity Framework目前系列末篇,因为关于EF 7.0学习资料实在是太少,我都是参考老外的资料花费了不少时间去研究去尝试同时也失败多次,个人觉得那是值得的,至少为今后在VS2015上来运用EF 7.0打下了坚定的基础,但是有些很深入的层面还得待EF 7.0比较成熟时再来补充及学习,当然在前面EF 6.0或6.1中系列中如果我发现又有好的内容也会进行适当补充同时也和大家一起学习以及分享。文中若有不妥之处或错处请指出。【注意】下面所演示代码都是基于VS2015上的ASP.NET 5和EF 7.0 Beta4,之前想要用最新版本Beta 7来演示,需要安装其扩展,但是安装后导致项目直接打不开,卸载后又出毛病,于是放弃使用Beta 7,用法基本上是大同小异。 EF 7.0相关概念 简介 EF 7.0利用了大量的依赖注入,也就意味着,EF是通过构造器注入来共同有效工作并且互相依赖的服务的集合,实现EF的提供者(Provider)就是真正根据需要去创建这些服务的特定提供者(Provider-Specific)的实现,一切都需手动去配置,且更加灵活和利于后期扩展。 上下文作用域(Context Scope) 单个EF 7.0应用程序能够利用多个Provider,例如同一个应用程序能够访问SQL Server数据库和SQLite数据库 ,然而通过每个上下文实例定义的Session必须严格使用单个Provider。因此我们可以创建一个上下文实例去访问SQL Server数据库,创建另外一个实例上下文去访问SQLite数据库,但是我们不能用我们创建的单个上下文实例来同时去访问SQL Server和SQLite数据库。 服务类型(Types of Services) •Core services:核心服务对于不同的Provider(提供者)不打算实现不同的行为。 •Provider-specific services:特定服务提供者没有具体的实现,所有的提供者必须提供这些服务的实现,然而经常有抽象的基类被用来当做是特定提供者(Provider-specific)实现的基础。 •Provider-specific services:特定服务提供者有具体的实现,对于这些服务,如果共同实现需要以某种方式被改变,那么一个提供者仅仅只需提供一种实现。 数据存储服务接口(IDataStoreServices) 对于关系提供者有一个 IRelationalDataStoreServices 接口,此接口通过EF 7.0关系架构添加了额外的服务。提供者若要连接一个关系数据库应该要实现此接口。 IRelationalDataStoreServices 和 IDataStoreServices 有基本实现,其包含了有具体实现的特定提供者的预定义映射,提供者一般使用这些基类之一并且重写其虚方法。 例如,通过 SQLite 提供者来实现 IRelationalDataStoreServices ,大概代码如下: public class SqliteDataStoreServices : RelationalDataStoreServices { public SqliteDataStoreServices(IServiceProvider services) : base(services) { } //ovveride methods from RelationalDataStoreServices } 注册服务(Registering services) 每个具体的服务必须被注册到DI容器中,以至于在调用 GetService 调用时将作出相应的行为。该注册通过在一个AddXxx扩展方法中进行,而Xxx是提供者的名称,例如对于SQLite的扩展方法如下: public static EntityFrameworkServicesBuilder AddSqlite( this EntityFrameworkServicesBuilder services) { ((IAccessor)services.AddRelational()).Service .AddSingleton<IDataStoreSource, SqliteDataStoreSource>() .TryAdd(new ServiceCollection() .AddSingleton() .AddSingleton() .AddScoped() .AddScoped() .AddScoped() .AddSingleton() .AddScoped() .AddScoped() .AddScoped() .AddScoped() .AddScoped() .AddScoped()); return services; } 上述AddXxx是在 EntityFrameworkServicesBuilder 上的扩展方法并且应该返回被传递进去的 EntityFrameworkServicesBuilder ,这将允许当在注册DI服务一个AddEntityFramework调用时它将被连接到并且允许额外的扩展方法从它中被连接。 注册范围(Registration scopes) 在大部分情况下,特定提供者服务应该被注册在DI中通过使用AddScoped方法,这是因为上下文创建Scope是为了在不同的上下文实例中使用不同的提供者,同时这也取决于在一个Scope 服务上的任何服务必须自己也作为Scope被注册。 在少数情况下,服务被注册使用 AddSingleton 在此种情况下的服务不依赖人任何其他的Scope服务,同时单个实例能同时被所有上下文所使用-例如上述的 SqliteSqlGenerator 。【注意】所有单例服务必须是线程安全的。 当前有 IModelSource 和 IValueGeneratorCache 两种服务,此两种服务表现在作为特定提供者缓存,这些服务必须作为单例服务来注册那么缓存将一直始终能贯穿于上下文实例中,这是有缓存的某一点。他们因此不依赖于任何Scope服务,同时提供者对于这些服务要有自己的具体实现,所以每个提供者将获得不同的实例同时避免了缓存冲突。 提供者的选择(Provider selection) EF 7.0应用程序选择去使用哪个提供者取决于所给的上下文实例中的UserXxx方法所要调用的,主要是在上下文中的 OnConfiguring 方法中,例如在一个应用程序中要使用SQLite可能需要像如下这样做: protected override void OnConfiguring( DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("MyDb.db"); } UseSqlite是在DbContextOptionsBuilder上的扩展方法,它的作用是创建或者更新一个已经存在的 IDbContextOptionsExtension 对象并且将其注册在OptionsBuilder上,例如: public static SqliteDbContextOptionsBuilder UseSqlite( this DbContextOptionsBuilder options, string connectionString) { var extension = GetOrCreateExtension(options); extension.ConnectionString = connectionString; ((IOptionsBuilderExtender)options).AddOrUpdateExtension(extension); return new SqliteDbContextOptionsBuilder(options); } 【注意】更新为可选的一个新的builder应该被返回以此来允许进一步的连接。 存在于option builder上的 IDbContextOptionsExtension 用途是通过EF堆栈来决定一个提供者是否已经被选择。对于Session,它也被用来存储特定提供者配置,这些配置,例如:连接字符串,通过关系基类被处理。 当EF正顾及所有内部DI注册时, IDbContextOptionsExtension 也被用来自动注册DI服务。一种简单的实现对于SQLite如下: public class SqliteOptionsExtension : RelationalOptionsExtension { public override void ApplyServices( EntityFrameworkServicesBuilder builder) => builder.AddSqlite(); } 当EF需要注册当前提供者的服务时通过调用ApplyServices,反过来说会调用以上被定义的AddXxx扩展方法。 总结 创建一个提供者的几个步骤: •通过使用IDataStoreServices和IRelationalDataStoreServices接口作为服务需要的前提来实现特定提供者。 •创建一个实现IDataStoreServices或者IRelationalDataStoreServices的接口来映射到你的实现,通过使用作为起始点的基类之一。 •创建一个AddXxx扩展方法在DI上来注册你的服务。 •创建一个IDbContextOptionsExtension的实现来处理提供者的选择和配置。 •创建UseXxx的扩展方法来使得应用程序去选择你的提供者。 •创建一个IDataStoreSource接口的实现来将一切捆绑在一起。 当然,上述关于创建任何一个提供者实际要实现这些服务是比较艰难,同时我们可以为特定提供者模型构建创建一些扩展方法。 接下来我们将手动一步一步通过EF创建数据库和表以及映射关系和实现增、删等操作以及注意事项,请继续往下看。 EF 7.0一步一步手动创建数据库及表 我们创建一个空的应用程序来手动通过添加代码来实现EF并了解下VS2015,如图: 我们创建空的应用程序同时添加如上几个文件夹为接下来的工作做好准备(至于为什么不创建非空的应用程序,个人觉得首先得了解各种配置文件的作用,因为在VS2015上为了跨平台都是基于配置,而不是一上来就创建非空的应用程序,以至于看到里面各式各样的代码都懵逼了)。下面首先对上述默认创建配置文件进行简短说明(稍安勿躁,慢慢来)。 Startup 我们知道在ASP.NET 5版本之前global.asax被用来在不同启动时刻来触发不同的事件,所以我们能配置应用程序,而在ASP.NET 5中Startup就类似global.asax,但是不同的是该文件能对一切进行配置而不是局限于某个区间,也就是说其作用域更广了。下面我们就对Startup里面的内容进行解读。 构造函数Startup 首先在这个类中建立一个如下属性 public IConfiguration Configuration { get; set; } 被创建的 IConfiguration 对象包含了来自 config.json 和执行环境的所有配置信息,我们在构造函数中通过类 Configuration 的实例来赋值给Configuration进行各种配置。也就是说通过Configuration配置的信息就代替了ASP.NET之前版本的Web.Config。在Web.Config中所有旧的配置被储存,AppSettings我们经常使用但是其不支持其他架构,仅仅只支持字符串。当然,对于复杂的场景我们可以自己手动创建处理程序但是还有其他应用也需要我们添加节点,这无疑使得Web.Config渐渐的膨胀起来,所以对于此Web.Config变得不是太灵活以及代码整体上来看似乎显得非常臃肿。在ASP.NET 5中这就发生了改变,IConfiguration和Configuration是配置的中心,而不是集中的配置位置。我们来看看如何灵活,如下: var Configuration = new Configuration() .AddJsonFile("config.json") .AddIniFile("system.ini") .AddCommandLine(args) .AddEnvironmentVariables(); 通过这种流式语法可以使我们从各种来源来添加相应的配置信息,通过能添加其他不同类型的配置来源我们可以看出,这是可扩展的,这种模式的配置也就意味着我们能将配置选项合并到单一的配置源中。 ConfigureServices 一旦构造函数被触发并读取配置信息,该方法以及Configure方法(下面讲)就会被执行以此来设置运行时环境, ConfigureServices 方法的作用是设置依赖注入(dependency injection),该方法中的参数 IServiceCollection 接口主要负责将我们注入的服务(如MVC、EntityFramework、Identity等)推入到运行的应用程序中,在此之外,ASP.NET 5有其自己内部的实现,尽管我们有其他的容器来提供相应的实现,即使我们不喜欢微软内部对于依赖注入的实现,但是我们可以让其默认存在,反正是无害的,何乐而不为呢!例如,如下: public void ConfigureServices(IServiceCollection services) { // 添加EF到服务容器中 services.AddEntityFramework(Configuration) .AddSqlServer() .AddDbContext(); // 添加角色验证到服务容器中 services.AddIdentity<ApplicationUser, IdentityRole>(Configuration) .AddEntityFrameworkStores(); // 添加MVC到服务容器中 services.AddMvc(); //添加其他的服务 services.AddScoped<IEmailer, Emailer>(); } 上述代码中我们添加了EF、MVC、Identity到DI容器中,在最底部有我们自己的服务并将其注入到容器中以供我们使用。通过一定量的配置也可以在此发生,比如设置EF环境的连接字符串以及对MVC的配置等等。基于这一点,我们所有的代码都是使这些服务依赖注入到容器中并进行访问。 Configure 这是当构造函数读取配置并执行该方法设置其运行时的环境的第二种方法,在ASP.NET之前版本不会启动任何其他的运行时框架,在ASP.NET 5中,你必须告诉将启用哪种框架(如MVC、EF实体框架以及Identity身份验证等框架),该方法是基于上述ConfigureServices方法,当Configure执行之后此方法然后将被调用如下: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory) { // 添加日志到控制台输出 loggerfactory.AddConsole(); // 添加静态文件到请求管道 app.UseStaticFiles(); // 添加基于Cookie的授权到请求管道 app.UseIdentity(); // 添加MVC路由到请求管道 app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index" }); }); } Configure 方法主要是通过 IApplicationBuilder 对象来允许我们加入到这些服务中。在上述中,我们可以选择加入静态文件、身份验证以及使用MVC对MVC路由的定义,通过了解这些我们就能随时跟踪应用程序的配置以及设置。 通过对上述的详细叙述,想必你应该对配置文件以及注入服务有了深刻的理解,接下来就是对于EF的实战。 数据库以及表生成 第一步 很显然由于要连接数据库以及初始化数据库和表,我们当然需要配置文件以及读取配置文件,默认创建空的应用程序是不会自动添加配置文件(config.json)的,我们得手动添加配置文件。如下: 在Statrup构造函数读取配置文件 var configurationBuilder = new ConfigurationBuilder(appEnv.ApplicationBasePath) .AddJsonFile("config.json") .AddEnvironmentVariables(); Configuration = configurationBuilder.Build(); 第二步 注入EntityFramework,添加我们需要使用的数据库以及上下文并通过字符串连接数据库 var connectionString = Configuration.Get("Data:DefaultConnection:ConnectionString"); services.AddEntityFramework() .AddSqlServer() .AddDbContext(options => { options.UseSqlServer(connectionString); }); 第三步 在Models文件夹下添加Student和Flower(一个Student对应一个Flower,一个Flower对应多个Student即一对多) public class Student { public int Id { get; set; } public string Name { get; set; } public int FlowerId { get; set; } public virtual Flower Flower { get; set; } } public class Flower { public int Id { get; set; } public string Remark { get; set; } public virtual ICollection Students { get; set; } } 第四步 在Core文件夹建立EF上下文进行映射以及初始化数据库以及表 public class EFDbContext : DbContext { public EFDbContext() { } protected override void OnModelCreating(ModelBuilder builder)【改变】EF 7.0之前版本为DbModelBuilder { builder.Entity(d => { d.Key(p => p.Id); d.Property(p => p.Name).Required(); d.Reference(p => p.Flower).InverseCollection(p => p.Students).ForeignKey(p => p.FlowerId); }); builder.Entity(d => { d.Key(p => p.Id); d.Property(p => p.Remark).Required(); }); } } 【注意】关于映射关系,下面讲,稍安勿躁。 第五步 通过PowerShell命令执行命令生成数据库及表。通过PowerShell来生成数据库和表我们必须要知道的几个命令以及有关DNX命令,如下: 安装.NET Core以及CLR命令 dnvm install -r coreclr latest -u dnvm install -r clr latest -u 指定安装32或者64位版本 dnvm install -r coreclr -arch x64 latest -u 或者 dnvm install -r coreclr -arch x86 latest -u 指定运行时(执行环境)以及版本或者只需指定运行时(执行环境) dnvm use -r coreclr -arch x86 1.0.0-beta5 或者 dnvm use 1.0.0-beta5 重新恢复包 dnu restore 用DNX运行应用程序 dnx . run EF迁移命令 dnx . ef migration add MyGrations(自定义名称) 在VS2015 RTM中为 dnx ef migrations add MyGrations(自定义名称) 更新数据库 dnx . ef migration apply 在VS2015 RTM中为 dnx ef database update 鉴于上述描述命令,由于初始化数据库是基于Migration,所以直接用 dnx . ef migration apply 无法创建数据库,我们得首先用 dnvm . ef migration add MyGgrations 来进行迁移。【注意】VS2015默认运行时为beta5,若你为此之上,首先指定运行时为beta5,然后重新恢复包才行,如下: //指定运行时 dnvm use 1.0.0-beta5 //重新恢复包 dnu restore 接下来就是提升逼格的时刻到了,全部用命令来执行,我们直接在NuGet管理控制台来进行迁移,不迁不知道,一迁吓一跳,吓死宝宝了,如下: 看那出错意思就是:对于运行时为4.5.1,解析下列依赖失败。我默认的运行时为beta6,刚开始以为是运行时的问题,后来指定运行时依然出错。【注】这个问题纠结我一天,查找资料也有类似错误,但是在我这上面却不适用,估计是别的问题导致同等错误。 * 重要: 要进行迁移,必须将命令行移到项目文件夹中 project.json 文件所在位置才行,在NuGet控制台进行是行不通的。 当你通过迁移命令进行到如图,说明你已经成功一大半了。 接下来,快马加鞭, dnx . ef migration apply 生成数据库及表,至此我们生成数据库才算告一段落,如下: EF 7.0映射关系 一对一映射(one-one) 依然以上述两个类(Student)和(Flower)为例来实现一对一映射,如下: public class Student { public int Id { get; set; } public string Name { get; set; } public int FloweId{ get; set; } public virtual Flower Flower { get; set; } } public class Flower { public int Id { get; set; } public string Remark { get; set; } public virtual Student Student { get; set; } } 映射如下: builder.Entity(d => { d.Key(p => p.Id); d.Property(p => p.Name).Required(); d.Reference(p => p.Flower).InverseReference(p => p.Student); }); builder.Entity(d => { d.Key(p => p.Id); d.Property(p => p.Remark).Required(); }); 上述 Reference 表示Student关联到Flower即指向Flower,通过 InverseReference 说明二者为一对一(one-one)关系。通过SQL Profiler监控生成的代码如下: 那问题来了,虽然表明这二者为一对一关系,但是怎么知道外键是FlowerId呢? 因为默认情况下EF将会去查找Student类中有无Flower类的类名+Id的属性(不区分大小写),如果有则将其作为外键,如果没有则将Student类的主键作为外键。 不信我们将其外键属性修改如下: public int FId{ get; set; } 映射如下: 验证了我的结论,有人就说了,这EF太平洋的警察-管的也太宽了,要是迫于无奈,无法将其外键属性进行上述约定呢?你想到的EF 7.0也早就想到了,就以修改的为例,我们只需将映射进行如下修改即可。 d.Reference(p => p.Flower).InverseReference(p => p.Student).ForeignKey(typeof(Student), "FId").PrincipalKey(typeof(Flower), "Id"); 用 ForeignKey 方法显式指定其FId再通过 PrincipalKey 方法将FId作为Flower的外键即可。 【建议】:一般情况下建议用约定来映射主要是可读性强且易于理解,当然,你也手动去指定外键,可能会更灵活一点。 一对多映射(one-many) 我们将Flower中的导航属性变成集合即可 : public virtual ICollection Students { get; set; } 修改映射关系: d.Reference(p => p.Flower).InverseCollection(p => p.Students).ForeignKey(p => p.FlowerId); Reference 指向其导航属性Flower,并用 InverseCollection 来说明二者关系为一对多(one-many)。【注意】关于外键上述已经说明,如果依据约定则无需指定,否则需要显式指定。 我们进行如下查询并监控 var list = ctx.Set().Include(d => d.Students).FirstOrDefault(d => d.Id == 1); 此时生成的SQL如下,满足要求: 此时我们反过来查询Student对应的是哪个Flower,并进行监控 var list = ctx.Set().Include(d => d.Flower).FirstOrDefault(d => d.Id == 1); 监控如下: 如我们所预期一样得到了相应的结果。 * 多对多映射(many-many)【重要】 在EF 7.0之前版本能够利用API来映射多对多关系 ,但是在EF 7.0版本中只有一对一(one-one)和一对多(one-many)的映射关系,却没有处理多对多(many-many)关系的API,虽然利用API实现不了,但是在EF 7.0中利用实体之间的关系来将自动完成映射。 我们假设有如下场景:一个产品(Product)多个分类(Category),同时一个分类对应多个多个产品 ,同时用第三类(Product_Category)来维护这两者之间的关系。鉴于此,给出所需类,如下: 产品类(Product) public class Product { public int Id { get; set; } public string ProductName { get; set; } public ICollection<Product_Category> Categorizations { get; set; } } 分类(Category) public class Category { public int Id { get; set; } public string CategoryName { get; set; } public ICollection<Product_Category> Categorizations { get; set; } } 产品分类(Product_Category) public class Product_Category { public int Product_Category_Id { get; set; } public int ProductId { get; set; } public Product Product { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } } 接下来必须指定(Product_Category)中的主键(当然你也可以指定主键为外键属性),如下: builder.Entity<Product_Category>(d=> { d.Key(p => p.Product_Category_Id); }); 此时通过SQL Profiler监控生成SQL如下: 接下来我们通过向表中插入数据,并进行相应的测试: 第一:找出单个产品对应的多个分类 var list = ctx.Set().Include(d => d.Categorizations).FirstOrDefault(d => d.Id == 3); 输出数据,如我们预期,如下: 第二:找出单个分类来对应的多个产品 对应代码如下: var Catgorylist = ctx.Set().Include(d => d.Categorizations).FirstOrDefault(d => d.Id == 1).Categorizations.ToList(); var ProductList = ctx.Set().Include(d => d.Categorizations).ToList(); var pList = from p in ProductList join c in Catgorylist on p.Id equals c.ProductId select p; 查询数据如下,so perfect,完成! 补充 突然发现EF 7.0在多对多的映射关系上的强大,为何如此说呢?当我将(Product_Category)关系类进行如下修改时(去掉上述对应的外键属性): public class Product_Category { public int Product_Category_Id { get; set; } public Product Product { get; set; } public Category Category { get; set; } } 生成表字段时会自动生成对应的表外键属性: 总结 对于上述叙述我们需要多对多(many-many)映射关系做个总结: •在两个类的关系类中(如上述Product_Category)只需给定需要建立关系的类,无需指定外键属性,EF 7.0会根据类名+Id自动生成。 •在EF 7.0上下文中初始化模型时必须要显式指定主键。 EF 7.0增、删等操作中的注意事项 连接数据库问题 当用EF上下文进行数据操作时首先得在上下文中的 OnConfiguring 方法中 连接数据库,否则会出错。(这也是我纳闷的地方,明明在Startup类中配置了上下文以及数据库的连接,为什么还要配置一遍),如下即可: protected override void OnConfiguring(EntityOptionsBuilder optionBuilder) { optionBuilder.UseSqlServer("Server=.;Database=EF7.0-Beta4-EntityFramework;Trusted_Connection=True;uid=sa;pwd=sa123"); } *添加数据操作(注意) 我们借助之前利用的(Student)和(Flower)来进行数据添加操作,代码如下: using (var ctx = new EFDbContext()) { var flower = new Flower() { Id = 1, Remark = "so bad", Students = new List() { new Student() {Id=1,Name="xpy0928",FlowerId=1 } } }; ctx.Set().Add(flower); ctx.SaveChanges(); } 以我们之前的经验通过上述操作肯定在Student表和Flower表各自添加了一条数据,那你就错了,结果却不是你想象的那样!!!空口无凭,用事实说话。如下: 当我们在上述基础上添加如下操作下时才可成功添加到数据库 ctx.Set().Add(flower.Students.FirstOrDefault()); 通过上述我们知道,之前对于添加只需一步则其关联的对象也会相应的进行添加,但是现在必须显式的去添加相应的对象。对于此EF官方的解释是: 不像EF之前版本,在当前版本上对于一个对象上调用Add()方法则不会标记它的关联对象为Added状态。但是此种情况是EF团队想要暴露的,因为在过去的EF版本中收到许多k开发者回馈不应该将关联的对象标记为Added状态。开发者中对于要使用无连接图的呼声非常高,因为在EF中没有提供对于无连接图的高级API,所以在此种情况下,将通过使用一种算法将每个对象标记为特定的状态,因此不会对其他关联对象产生其他影响。【注意】EF团队可能在未来会针对不同的行为给出相应的解决方案。 延迟加载 上述导航属性标记为virtual,数据库插入数据并进行查找,如下: var stu = ctx.Set().Where(d => d.Flower.Remark == "so bad").ToList(); 此时获得数据却为空,如下: 总结 在EF 7.0中不再支持延迟加载,即获得导航属性必须通过显示指定 Inlude 来获取,通过其不存在 LazyLoadingEnabled 和 ProxyCreationEnabled 方法也可得知。 变更追踪(Change Tracking) 之前版本关闭变更追踪是通过 Configuration.DetectChanges 来进行设置,在EF 7.0版本中将通过 ChangeTracker.AutoDetectChangesEnabled 来进行追踪。同时有关变更追踪的方法及属性都通过属性 ChangeTracker 来进行访问。其变更追踪原理和之前版本原理一样,未发生改变。 我们接下来看如下操作: 首先关闭变更追踪 public EFDbContext() { this.ChangeTracker.AutoDetectChangesEnabled = false; } 修改数据并保存 using (var ctx = new EFDbContext()) { var stu = ctx.Set().FirstOrDefault(d => d.Name == "xpy0928"); stu.Name = "xpy0929"; ctx.Entry(stu).State = EntityState.Modified; ctx.SaveChanges(); } 因为关闭了变更追踪所以需要手动修改其状态自动调用DetectChanges方法检测其状态,最后更新到数据库。这是我们在之前版本的做法,但是在EF 7.0中无需这么麻烦。将上述修改操作进行如下修改即可: ctx.Set().Update(stu); 此时调用Update方法进行更改自动回调用DetectChanges方法,最后进行更新。EF 7.0中对实体以及实体集合批量的基本操作都已经实现,通过对应的方法进行操作即可。 总结 以上就是对EF 7.0和VS2015做了一个初步的了解,相信通过在VS2015对EF 7.0的操作会为以后在VS2015上熟练使用EF 7.0做一个铺垫。本人不才,同时也希望通过本人的学习及分享有能够帮助到大家的地方。