本文出自8天掌握EF的Code First开发系列,经过自己的实践整理出来。
本篇目录
- 视图View
- 存储过程
- 异步API
- 本章小结
咱们接着上一篇继续深入学习,这一篇说说Entity Framework之Code First方式如何使用视图,存储过程以及EF提供的一些异步接口。我们会看到如何充分使用已存在的存储过程和函数来检索、修改数据。此外,我们还要理解异步处理的优势以及EF是如何通过内置的API来支持这些概念的。
视图View
视图在RDBMS中扮演了一个重要的角色,它是将多个表的数据联结成一种看起来像是一张表的结构,但是没有提供持久化。因此,可以将视图看成是一个原生表数据顶层的一个抽象。例如,我们可以使用视图提供不同安全的级别,也可以简化必须编写的查询,尤其是我们可以在代码中的多个地方频繁地访问使用视图定义的数据。EF Code First现在还不完全支持视图,因此我们必须使用一种变通方法。这种方法就是将视图真正看成是一张表,让EF定义这张表,然后再删除它,最后再创建一个代替它的视图。下面具体看看是如何实现的吧。
创建一个控制台项目,取名“ViewsAndStoreProcedure”。
1、创建实体类
public class Province { public Province() { Donators = new HashSet<Donator>(); } public int Id { get; set; } [StringLength(225)] public string ProvinceName { get; set; } public virtual ICollection<Donator> Donators { get; set; } }
public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } public virtual Province Province { get; set; } }
2、创建模拟视图类
暂且这样称呼吧,就是从多个实体中取出想要的列组合成一个新的实体。
public class DonatorViewInfo { public int DonatorId { get; set; } public string DonatorName { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } [StringLength(225)] public string ProvinceName { get; set; } }
3、为模拟视图类创建配置类
下面的代码指定了主键和表名(也是视图的名字,注意这里的表名一定要和创建视图的语句中的视图名一致):
public class DonatorViewInfoMap : EntityTypeConfiguration<DonatorViewInfo> { public DonatorViewInfoMap() { HasKey(m => m.DonatorId).ToTable("DonatorViews"); } }
4、上下文中添加模拟视图类和配置类
web.config文件中的连接字符串我已配置好,不在此处展示!
public class DonatorContext :DbContext { public DonatorContext() : base("name = EFCodeFirst") { } public virtual DbSet<DonatorViewInfo> DonatorViews { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new DonatorViewInfoMap()); base.OnModelCreating(modelBuilder); } }
5、创建初始化器
public class Initializer : DropCreateDatabaseAlways<DonatorContext> { protected override void Seed(DonatorContext context) { string drop = @"DROP TABLE DonatorViews"; context.Database.ExecuteSqlCommand(drop); var createView = @"CREATE VIEW [dbo].[DonatorViews] AS SELECT dbo.Donators.Id as DonatorId, dbo.Donators.Name as DonatorName, dbo.Donators.Amount as Amount, dbo.Donators.DonateDate as DonateDate, dbo.Provinces.ProvinceName as ProvinceName FROM dbo.Donators inner join dbo.Provinces on dbo.Donators.Province_Id = dbo.Provinces.Id "; context.Database.ExecuteSqlCommand(createView); base.Seed(context); } }
上面的代码中,我们先使用Database对象的ExecuteSqlCommand方法销毁生成的表,然后又调用该方法创建了我们的视图。该方法在允许开发者对后端执行任意的SQL代码时很有用。
上面的代码写完之后,在Main方法中只要写这一句代码Database.SetInitializer(new Initializer());,运行程序,就会看到数据库中已经生成了Donators和Provinces两张表和一个视图DonatorView,见下图:
如果在运行程序时出现如下异常:EF code first - Model compatibility cannot be checked because the database does not contain model metadata,那么要将DropCreateDatabaseIfModelChanges修改为DropCreateDatabaseAlways即可
刚才新建的数据库是没有数据的,然后我们插入数据,在数据库中查询一下,可以看到视图中已经存在数据了:
下面,一切工作准备就绪,就可以开始查询数据了:
static void Main(string[] args) { Database.SetInitializer(new Initializer()); using (var context = new DonatorContext()) { foreach (var donator in context.DonatorViews) { Console.WriteLine(donator.ProvinceName + " " + donator.DonatorId + " " + donator.DonatorName + " " + donator.Amount + " " + donator.DonateDate); } } Console.WriteLine("Finished"); Console.ReadKey(); }
执行结果如下图所示:
正如上面的代码所示,访问视图和任何数据表在代码层面没有区别,需要注意的地方就是在Seed方法中定义的视图名称要和定义的表名称一致,否则就会因为找不到表对象而报错,这一点要格外注意。
虽然视图看起来很像一张表,但是如果我们尝试修改或更新视图中定义的实体,那么就会抛异常。
另一种方法
如果我们不想这么折腾(先定义一张表,然后删除这张表,再定义视图),当然了,我们还是要在初始化器中定义视图,但是我们使用Database对象的另一个方法SqlQuery查询数据。该方法和ExecuteSqlCommand方法有相同的形参,但是最终返回一个结果集,在我们这里例子中,返回的就是DonatorViewInfo集合对象,如下代码所示:
static void Main(string[] args) { Database.SetInitializer(new Initializer()); using (var context = new DonatorContext()) { string sql = "SELECT DonatorId ,DonatorName ,Amount ,DonateDate ,ProvinceName FROM dbo.DonatorViews WHERE ProvinceName = {0}"; var donators = context.Database.SqlQuery<DonatorViewInfo>(sql, "广东省"); foreach (var donator in donators) { Console.WriteLine(donator.ProvinceName + " " + donator.DonatorId + " " + donator.DonatorName + " " + donator.Amount + " " + donator.DonateDate); } } Console.WriteLine("Finished"); Console.ReadKey(); }
SqlQuery方法需要一个泛型类型参数,该参数定义了原生SQL命令执行之后,将查询结果集物质化成何种类型的数据。该文本命令本身就是参数化的SQL。我们需要使用参数来确保动态sql不是SQL注入的目标。SQL注入是恶意用户通过提供特定的输入值执行任意SQL代码的过程。EF本身不是这些攻击的目标。
我们不仅看到了如何在EF中使用视图,而且看到了两个很有用的Database对象,SqlQuery和ExecuteSqlCommand方法。SqlQuery方法的泛型参数不一定非得是一个类,也可以.Net的基本类型,如string或者int。
执行结果如下:
存储过程
1、在EF中使用已存在的存储过程
在EF中使用存储过程和使用视图是很相似的,一般会使用Database对象上的两个方法——SqlQuery和ExecuteSqlCommand。为了从存储过程中读取很多数据行,我们只需要定义一个类,我们会将检索到的所有数据行物质化到该类实例的集合中。比如,从下面的存储过程读取数据:
CREATE PROCEDURE SelectDonators @provinceName AS NVARCHAR(10) AS BEGIN SELECT ProvinceName,Name,Amount,DonateDate FROM dbo.Donators JOIN dbo.Provinces ON dbo.Provinces.Id = dbo.Donators.Province_Id WHERE ProvinceName=@provinceName END
我们只需要定义一个匹配了存储过程结果的类(类的属性名必须和表的列名一致)即可,如下所示:
public class DonatorFromStoreProcedure { public string ProvinceName { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } }
还是插入以下数据进行测试:
INSERT dbo.Provinces VALUES( N'山东省') INSERT dbo.Provinces VALUES( N'河北省') INSERT dbo.Donators VALUES ( N'陈志康', 50, '2016-04-07',1) INSERT dbo.Donators VALUES ( N'海风', 5, '2016-04-08',1) INSERT dbo.Donators VALUES ( N'醉、千秋', 12, '2016-04-13',1) INSERT dbo.Donators VALUES ( N'雪茄', 18.8, '2016-04-15',2) INSERT dbo.Donators VALUES ( N'王小乙', 10, '2016-04-09',2)
当然如果你原有数据库已经保存有数据,则可以不添加这些信息。
现在我们就可以使用SqlQuery方法读取数据了(注意:在使用存储过程前,先要在数据库中执行存储过程),如下所示:
static void Main(string[] args) { Database.SetInitializer(new Initializer()); using (var context = new DonatorContext()) { string sql = "[dbo].[SelectDonators] {0}"; var donators = context.Database.SqlQuery<DonatorFromStoreProcedure>(sql, "山东省"); foreach (var donator in donators) { Console.WriteLine(donator.ProvinceName + " " + donator.Name + " " + donator.Amount + " " + donator.DonateDate); } } Console.WriteLine("Finished"); Console.ReadKey(); }
上面的代码中,我们指定了使用哪个类读取查询的结果,创建SQL语句时,也为存储过程的参数提供了一个格式化占位符,调用SqlQuery时为那个参数提供了一个值。假如要提供多个参数的话,多个格式化占位符必须用逗号分隔,还要给SqlQuery提供值的数组(后面会举例)。我们也可以使用表值函数代替存储过程。
存储过程成功执行,结果如下:
另一个用例就是假如存储过程没有返回任何值,只是对数据库中的一张或多张表执行了一条命令的情况。一个存储过程干了多少事情不重要,重要的是它压根不需要返回任何东西。例如,下面的存储过程只是更新了一些东西:
CREATE PROCEDURE UpdateDonator @namePrefix AS NVARCHAR(10), @addedAmount AS DECIMAL AS BEGIN UPDATE dbo.Donators SET Name=@namePrefix+Name,Amount=Amount+@addedAmount WHERE Province_Id=2/*给河北省的打赏者名字前加个前缀,并将金额加上指定的数量*/ END
现在数据库中执行该存储过程,然后,要调用该存储过程,我们使用ExecuteSqlCommand方法。该方法会返回存储过程或者其他任何SQL语句影响的行数。如果你对这个返回值不感兴趣,那么你可以不理它。下面小试牛刀一把:
static void Main(string[] args) { Database.SetInitializer(new Initializer()); using (var context = new DonatorContext()) { string sql = "[dbo].[UpdateDonator] {0}, {1}"; context.Database.ExecuteSqlCommand(sql, "前缀", 2M); } Console.WriteLine("Finished"); Console.ReadKey(); }
这里我们为上面定义的存储过程提供了两个参数,一个是在每个打赏者的姓名前加个前缀,另一个是将打赏金额加2。这里需要注意的是,我们必须严格按照它们在存储过程中定义的顺序依次传入相应的值,它们会以参数数组传入ExecuteSqlCommand。执行结果如下:
很大程度上,EF降低了存储过程的需要,然而,仍旧有很多原因要使用它们。这些原因包括安全标准,遗留数据库或者效率等问题。比如,如果需要在单个操作中更新几千条数据,然后再通过EF检索出来;如果每次都更新一行,然后再保存那些实例,效率是很低的。开发者可以执行任意的SQL语句,只需要将上面SqlQuery或ExecuteSqlCommand方法中的存储过程名称改为要执行的SQL语句就可以了。
2、使用 MapToStoredProcedures 生成存储过程
至今,我们都是使用EF内置的功能生成插入,更新或者删除实体的SQL语句,总有某种原因使我们想使用存储过程来实现相同的结果。开发者可能会为了安全原因使用存储过程,也可能是要处理一个已存在的数据库,而这些存储过程已经内置到该数据库了。
EF Code First全面支持这些查询。我们可以使用熟悉的EntityTypeConfiguration类来给存储过程配置该支持,只需要简单地调用MapToStoredProcedures方法就可以了。如果我们让EF管理数据库结构,那么它会自动为我们生成存储过程。此外,我们还可以使用MapToStoredProcedures方法合适的重载来重写存储过程名称或者参数名。下面以donator类为例:
class DonatorMap : EntityTypeConfiguration<Donator> { public DonatorMap() { MapToStoredProcedures(); } }
如果我们运行程序来创建或更新数据库,就会看到为我们创建了新的存储过程,默认为插入操作生成了Donator_Insert,其他的操作名称类似,如下图:
如果有必要的话,我们可以自定义存储过程名,例如:
class DonatorMap : EntityTypeConfiguration<Donator> { public DonatorMap() { MapToStoredProcedures(config => { //将删除打赏者的默认存储过程名称更改为“DonatorDelete”, //同时将该存储过程的参数名称更改为“donatorId”,并指定该值来自Id属性 config.Delete( procConfig => { procConfig.HasName("DonatorDelete"); procConfig.Parameter(d => d.Id, "donatorId"); }); //将默认的插入存储过程名称更改为“DonatorInsert” config.Insert(procConfig => procConfig.HasName("DonatorInsert")); //将默认的更新存储过程名称更改为“DonatorUpdate” config.Update(procConfig => procConfig.HasName("DonatorUpdate")); }); } }
总之,要自定义的话,代码肯定更冗余,不管怎样了,取决于你!
异步API
目前为止,我们所有使用EF的数据库操作都是同步的。换言之,我们的.NET程序会等待给定的数据库操作(例如一个查询或者一个更新)完成之后才会继续向前执行。在很多情况下,使用这种方式没有什么问题,然而,在某些情况下,异步地执行这些操作的能力是很重要的。在这些情况下,当该软件等待数据库操作完成时,我们让.Net使用它的的执行线程。例如,如果使用了异步的方式在创建一个Web应用,当我们等待数据库完成处理一个请求(无论它是一个保存还是检索操作)时,通过将web工作线程释放回线程池,就可以更有效地利用服务器资源。
即使在桌面应用中,异步API也很有用,因为用户可能会潜在执行应用中的其他任务,而不是等待一个可能耗时的查询或保存操作。换言之,.Net线程不需要等待数据库线程完成跟数据库有关的工作。在许多应用程序中,异步API没有带来好处,从性能的角度来说,甚至可能是有害的,因为线程上下文的切换开销。因此,在使用异步API之前,开发者需要确定使用异步API会让你受益!
EF暴露了很多异步操作,按照约定,所有的这些方法都以Async后缀结尾。对于保存操作,我们可以使用DbContext上的SaveChangesAsync方法。也有很多查询的方法,比如,许多聚合函数都有异步副本,比如SumAsync和AverageAsync。还可以使用ToListAsync和ToArrayAsync将一个结果集读入到一个list或者array中。此外,还可以使用ForEachAsync方法对一个查询结果进行枚举。
1、异步地从数据库中获取对象的列表
private static async Task<List<Donator>> GetDonatorsAsync() { using (var context = new DonatorContext()) { return await context.Donators.ToListAsync(); } }
值得注意的是,这里使用了典型的async/await用法模式。函数被标记为 async 并返回一个task对象,确切地说是一个Donator集合的task。然后,调用了DbContext的集合属性创建了一个返回所有Donator的查询。然后,使用ToListAsync扩展方法对该查询结果进行枚举。最后,由于我们需要遵守async/await模式,所以必须等待返回值。
任何EF查询都可以使用ToListAsync或者ToArrayAsync转换成异步版本。
2、异步创建一个新的对象
private static async Task InsertDonatorAsync(Donator donator) { using (var context = new DonatorContext()) { context.Donators.Add(donator); await context.SaveChangesAsync(); } }
代码很简单,和一般的同步模式比较,只是返回类型为Task,方法多了async修饰,调用了SaveChangesAsync方法,同时注意,自己定义的方法最好也以Async后缀结尾,不是必须的,只是为了遵守规范。
3、异步定位一条记录
我们可以异步定位一条记录,可以使用很多方法,比如Single或First,这两个方法都有异步版本。
private static async Task<Donator> FindDonatorAsync(int donatorId) { using (var context = new DonatorContext()) { return await context.Donators.FindAsync(donatorId); } }
一般来说,就参数而言,EF中的所有异步方法和它们的同步副本都有相同的方法签名。
4、异步聚合函数
对应于同步版本,异步聚合函数包括这么几个方法,MaxAsync、MinAsync、CountAsync、SumAsync、AverageAsync。
private static async Task<int> GetDonatorCountAsync() { using (var context = new DonatorContext()) { return await context.Donators.CountAsync(); } }
5、异步遍历查询结果
如果要对查询结果进行异步遍历,可以使用ForEachAsync,可以在任何查询之后使用该方法。比如,下面将每个打赏者的打赏日期设置为今天。
private static async Task LoopDonatorsAsync() { using (var db = new DonatorContext()) { await db.Donators.ForEachAsync(d => { d.DonateDate = DateTime.Today; }); } }
如果要在一个同步方法中使用一个异步方法,那么我们可以使用Task的API等待一个任务完成。比如,我们可以访问task的Result属性,这会造成当前的线程暂停并且让该task完成执行,但一般不建议这么做,最佳实践是总是使用async。
同步方法中调用异步方法的代码如下:
Console.WriteLine(FindDonatorAsync(1).Result.DonateDate);
上面这句代码在Main方法中,调用了之前定义的异步方法,然后访问了该Task的Result属性,这会造成异步函数完成执行。
当决定是否使用异步API的时候,首先要研究一下,并确定为什么要使用异步API。既然用了异步API,为了获得最大的编码好处,就要确保整个方法的调用连都是异步的。最后,当需要时在使用Task API。
本章小结
EF给开发者带来了很大价值,允许我们使用C#代码管理数据库数据。然而,有时我们需要通过动态的SQL语句或者存储过程,更直接地对视图访问数据,就可以使用ExecuteSqlCommand方法来执行任意的SQL代码,包括原生SQL或者存储过程。也可以使用SqlQuery方法从视图、存储过程或任何SQL语句中检索数据,EF会基于我们提供的结果类型物质化查询结果。当给这两个方法提供参数时,避免SQL注入漏洞很重要。
EF也可以自动为实体生成插入、更新和删除的存储过程,假如你对这些存储过程的命名规范和编码标准满意的话,我们只需要在配置伙伴类中写一行代码就可以了。
EF也提供了异步操作支持,包括查询和更新。为了避免潜在的性能影响,开发者使用这些技术时务必谨慎。在某些技术中,异步API很适合,Web API就是一个好的例子。