本文共 16893 字,大约阅读时间需要 56 分钟。
本篇作为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是通过构造器注入来共同有效工作并且互相依赖的服务的集合,实现EF的提供者(Provider)就是真正根据需要去创建这些服务的特定提供者(Provider-Specific)的实现,一切都需手动去配置,且更加灵活和利于后期扩展。
单个EF 7.0应用程序能够利用多个Provider,例如同一个应用程序能够访问SQL Server数据库和SQLite数据库 ,然而通过每个上下文实例定义的Session必须严格使用单个Provider。因此我们可以创建一个上下文实例去访问SQL Server数据库,创建另外一个实例上下文去访问SQLite数据库,但是我们不能用我们创建的单个上下文实例来同时去访问SQL Server和SQLite数据库。
对于关系提供者有一个 IRelationalDataStoreServices 接口,此接口通过EF 7.0关系架构添加了额外的服务。提供者若要连接一个关系数据库应该要实现此接口。 IRelationalDataStoreServices 和 IDataStoreServices 有基本实现,其包含了有具体实现的特定提供者的预定义映射,提供者一般使用这些基类之一并且重写其虚方法。
例如,通过 SQLite 提供者来实现 IRelationalDataStoreServices ,大概代码如下:
public class SqliteDataStoreServices : RelationalDataStoreServices { public SqliteDataStoreServices(IServiceProvider services) : base(services) { } //ovveride methods from RelationalDataStoreServices }
每个具体的服务必须被注册到DI容器中,以至于在调用 GetService 调用时将作出相应的行为。该注册通过在一个AddXxx扩展方法中进行,而Xxx是提供者的名称,例如对于SQLite的扩展方法如下:
public static EntityFrameworkServicesBuilder AddSqlite( this EntityFrameworkServicesBuilder services){ ((IAccessor)services.AddRelational()).Service .AddSingleton () .TryAdd(new ServiceCollection() .AddSingleton () .AddSingleton () .AddScoped () .AddScoped () .AddScoped () .AddSingleton () .AddScoped () .AddScoped () .AddScoped () .AddScoped () .AddScoped () .AddScoped ()); return services;}
上述AddXxx是在 EntityFrameworkServicesBuilder 上的扩展方法并且应该返回被传递进去的 EntityFrameworkServicesBuilder ,这将允许当在注册DI服务一个AddEntityFramework调用时它将被连接到并且允许额外的扩展方法从它中被连接。
在大部分情况下,特定提供者服务应该被注册在DI中通过使用AddScoped方法,这是因为上下文创建Scope是为了在不同的上下文实例中使用不同的提供者,同时这也取决于在一个Scope 服务上的任何服务必须自己也作为Scope被注册。
在少数情况下,服务被注册使用 AddSingleton 在此种情况下的服务不依赖人任何其他的Scope服务,同时单个实例能同时被所有上下文所使用-例如上述的 SqliteSqlGenerator 。【注意】所有单例服务必须是线程安全的。
当前有 IModelSource 和 IValueGeneratorCache 两种服务,此两种服务表现在作为特定提供者缓存,这些服务必须作为单例服务来注册那么缓存将一直始终能贯穿于上下文实例中,这是有缓存的某一点。他们因此不依赖于任何Scope服务,同时提供者对于这些服务要有自己的具体实现,所以每个提供者将获得不同的实例同时避免了缓存冲突。
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扩展方法。
创建一个提供者的几个步骤:
当然,上述关于创建任何一个提供者实际要实现这些服务是比较艰难,同时我们可以为特定提供者模型构建创建一些扩展方法。
我们创建一个空的应用程序来手动通过添加代码来实现EF并了解下VS2015,如图:
我们创建空的应用程序同时添加如上几个文件夹为接下来的工作做好准备(至于为什么不创建非空的应用程序,个人觉得首先得了解各种配置文件的作用,因为在VS2015上为了跨平台都是基于配置,而不是一上来就创建非空的应用程序,以至于看到里面各式各样的代码都懵逼了)。下面首先对上述默认创建配置文件进行简短说明(稍安勿躁,慢慢来)。
我们知道在ASP.NET 5版本之前global.asax被用来在不同启动时刻来触发不同的事件,所以我们能配置应用程序,而在ASP.NET 5中Startup就类似global.asax,但是不同的是该文件能对一切进行配置而不是局限于某个区间,也就是说其作用域更广了。下面我们就对Startup里面的内容进行解读。
首先在这个类中建立一个如下属性
public IConfiguration Configuration { get; set; }
var Configuration = new Configuration() .AddJsonFile("config.json") .AddIniFile("system.ini") .AddCommandLine(args) .AddEnvironmentVariables();
public void ConfigureServices(IServiceCollection services){ // 添加EF到服务容器中 services.AddEntityFramework(Configuration) .AddSqlServer() .AddDbContext(); // 添加角色验证到服务容器中 services.AddIdentity (Configuration) .AddEntityFrameworkStores (); // 添加MVC到服务容器中 services.AddMvc(); //添加其他的服务 services.AddScoped (); }
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" }); }); }
通过对上述的详细叙述,想必你应该对配置文件以及注入服务有了深刻的理解,接下来就是对于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 ICollectionStudents { 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命令来生成数据库和表你不得不知道的几个命令以及有关DNX命令,如下:
dnvm install -r coreclr latest -udnvm install -r clr latest -u
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 . run
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,刚开始以为是运行时的问题,后来指定运行时依然出错。【注】这个问题纠结我一天,查找资料也有类似错误,但是在我这上面却不适用,估计是别的问题导致同等错误。
当你通过迁移命令进行到如图,说明你已经成功一大半了。
接下来,快马加鞭, dnx . ef migration apply 生成数据库及表,至此我们生成数据库才算告一段落,如下:
依然以上述两个类(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呢?
不信我们将其外键属性修改如下:
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的外键即可。
【建议】:一般情况下建议用约定来映射主要是可读性强且易于理解,当然,你也手动去指定外键,可能会更灵活一点。
我们将Flower中的导航属性变成集合即可 :
public virtual ICollectionStudents { 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);
监控如下:
如我们所预期一样得到了相应的结果。
我们假设有如下场景:一个产品(Product)多个分类(Category),同时一个分类对应多个多个产品 ,同时用第三类(Product_Category)来维护这两者之间的关系。鉴于此,给出所需类,如下:
public class Product { public int Id { get; set; } public string ProductName { get; set; } public ICollectionCategorizations { get; set; } }
public class Category { public int Id { get; set; } public string CategoryName { get; set; } public ICollectionCategorizations { get; set; } }
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(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)映射关系做个总结:
当用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(); }
当我们在上述基础上添加如下操作下时才可成功添加到数据库
ctx.Set().Add(flower.Students.FirstOrDefault());
通过上述我们知道,之前对于添加只需一步则其关联的对象也会相应的进行添加,但是现在必须显式的去添加相应的对象。对于此EF官方的解释是:
上述导航属性标记为virtual,数据库插入数据并进行查找,如下:
var stu = ctx.Set().Where(d => d.Flower.Remark == "so bad").ToList();
此时获得数据却为空,如下:
在EF 7.0中不再支持延迟加载,即获得导航属性必须通过显示指定 Inlude 来获取,通过其不存在 LazyLoadingEnabled 和 ProxyCreationEnabled 方法也可得知。
之前版本关闭变更追踪是通过 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);
以上就是对EF 7.0和VS2015做了一个初步的了解,相信通过在VS2015对EF 7.0的操作会为以后在VS2015上熟练使用EF 7.0做一个铺垫。本人不才,同时也希望通过本人的学习及分享有能够帮助到大家的地方。
转载地址:http://oarfa.baihongyu.com/