8天掌握EF的Code First开发系列之3 管理数据库创建,填充种子数据以及LINQ操作详解
Posted JustYong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8天掌握EF的Code First开发系列之3 管理数据库创建,填充种子数据以及LINQ操作详解相关的知识,希望对你有一定的参考价值。
- 管理数据库创建
- 管理数据库连接
- 管理数据库初始化
- 填充种子数据
- LINQ to Entities详解
- 什么是LINQ to Entities
- 使用LINQ to Entities操作实体
- LINQ操作
- 懒加载和预加载
- 插入数据
- 更新数据
- 删除数据
- 本章小结
本人的实验环境是VS 2013 Update 5,windows 10,MSSQL Server 2008。
上一篇《Code First开发系列之领域建模和管理实体关系》,我们主要介绍了EF中“约定大于配置”的概念,如何创建数据库的表结构,以及如何管理实体间的三种关系和三种继承模式。这一篇我们主要说三方面的问题,数据库创建的管理,种子数据的填充以及CRUD的操作详细用法。
管理数据库创建
1、管理数据库连接
(1) 使用配置文件管理连接
在数据库上下文类中,如果我们只继承了无参数的DbContext,并且在配置文件中创建了和数据库上下文类同名的连接字符串,那么EF会使用该连接字符串自动计算出该数据库的位置和数据库名。比如,我们的上下文定义如下:
public class SampleDbEntities : DbContext { }
如果我们在配置文件中定义的连接字符串如下:
<add name="SampleDbEntities" connectionString="Data Source=(LocalDb)\\v11.0;Initial Catalog=myTestDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\myTestDb.mdf" providerName="System.Data.SqlClient" />
这样,EF会使用该连接字符串执行数据库操作。究竟发生了什么呢?我们通过示例代码来验证一下。
public class SampleDbEntities : DbContext { public DbSet<Student> Students { get; set; } } public class Student { public int Id { get; set; } public string Name { get; set; } }
class Program { static void Main(string[] args) { using (var context = new SampleDbEntities()) { var stu1 = new Student() {Name = "Paul Huang"}; context.Students.Add(stu1); context.SaveChanges(); } Console.WriteLine("Finished"); Console.ReadKey(); } }
当运行应用程序时,可能会出现“Error: A file activation error occurred. CREATE DATABASE failed.”的异常,解决方案请参考,示例代码如下代码:
public class SampleDbEntities : DbContext { public SampleDbEntities() { AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Directory.GetCurrentDirectory()); } public DbSet<Student> Students { get; set; } }
当运行应用程序时,EF会寻找我们的上下文类名,即“SampleDbEntities”,并在配置文件中寻找和它同名的连接字符串,然后它会使用该连接字符串计算出应该使用哪个数据库provider,之后检查数据库位置(例子中是当前的数据目录),之后会在指定的位置创建一个名为myTestDb.mdf的数据库文件,同时根据连接字符串的Initial Catalog属性创建了一个名为myTestDb的数据库。
使用配置文件指定数据库位置和名字对于控制上下文类的连接参数也许是最简单和最有效的方式,另一个好处是如果我们想为开发,生产和临时环境创建各自的连接字符串,那么在配置文件中更改连接字符串并在开发时将它指向确定的数据库也是一种方法。
(2) 使用已存在的ConnectionString
如果我们已经有了一个定义数据库位置和名称的ConnectionString,并且我们想在数据库上下文类中使用这个连接字符串,如下:
<add name="AppConnection" connectionString="Data Source=(LocalDb)\\v11.0;Initial Catalog=myTestDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\myTestDb2.mdf" providerName="System.Data.SqlClient" />
那么我们可以将该连接字符串的名字传入数据库上下文DbContext的构造函数中,如下所示:
public SampleDbEntities() : base("name = AppConnection") { AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Directory.GetCurrentDirectory()); }
上面的代码将连接字符串的名字传给了DbContext类的构造函数,这样一来,我们的数据库上下文就会开始使用连接字符串了。但是注意有一个问题,如果使用“(1) 使用配置文件管理连接”已经在Bin/Debug下面生成了myTestDb.mdf文件,再运行"(2) 使用已存在的ConnectionString"时就会出现如下错误:
解决方法如下:
public class SampleDbEntities : DbContext { public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer(new DropCreateDatabaseAlways<SampleDbEntities>()); AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Directory.GetCurrentDirectory()); } public DbSet<Student> Students { get; set; } }
如果在配置文件中还有一个和数据库上下文类名同名的connectionString,也不会使用这个同名的连接字符串,也就是说显式指定的连接字符串优先权更大。
(3) 使用已存在的连接
通常在一些老项目中,我们只会在项目中的某个部分使用EF Code First,同时,我们想对数据上下文类使用已经存在的数据库连接,如果要实现这个,可将连接对象传给DbContext类的构造函数,如下:
public SampleDbEntities(DbConnection dbConnection) : base(dbConnection, false) { }
这里要注意一下contextOwnsConnection
参数,之所以将它作为false传入到上下文,是因为它是从外部传入的,当上下文出了范围时,可能会有人想要使用该连接。如果传入true的话,那么一旦上下文出了范围,数据库连接就会立即关闭。
2、管理数据库初始化
首次运行EF Code First应用时,EF会做下面的这些事情:
- 检查正在使用的
DbContext
类。 - 找到该上下文类使用的
connectionString
。 - 找到领域实体并提取模式相关的信息。
- 创建数据库。
- 将数据插入系统。
一旦模式信息提取出来,EF会使用数据库初始化器将该模式信息推送给数据库。数据库初始化器有很多可能的策略,EF默认的策略是如果数据库不存在,那么就重新创建;如果存在的话就使用当前存在的数据库。当然,我们有时也可能需要覆盖默认的策略,可能用到的数据库初始化策略如下:
CreateDatabaseIfNotExists
:顾名思义,如果数据库不存在,那么就重新创建,否则就使用现有的数据库。如果从领域模型中提取到的模式信息和实际的数据库模式不匹配,那么就会抛出异常。DropCreateDatabaseAlways
:如果使用了该策略,那么每次运行程序时,数据库都会被销毁。这在开发周期的早期阶段通常很有用(比如设计领域实体时),从单元测试的角度也很有用。DropCreateDatabaseIfModelChanges
:这个策略的意思就是说,如果领域模型发生了变化(具体而言,从领域实体提取出来的模式信息和实际的数据库模式信息失配时),就会销毁以前的数据库(如果存在的话),并创建新的数据库。MigrateDatabaseToLatestVersion
:如果使用了该初始化器,那么无论什么时候更新实体模型,EF都会自动地更新数据库模式。这里很重要的一点是,这种策略更新数据库模式不会丢失数据,或者是在已有的数据库中更新已存在的数据库对象。
(1) 设置初始化策略
EF默认使用CreateDatabaseIfNotExists
作为默认初始化器,如果要覆盖这个策略,那么需要在DbContext类中的构造函数中使用Database.SetInitializer
方法,下面的例子使用DropCreateDatabaseIfModelChanges
策略覆盖默认的策略:
public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SampleDbEntities>()); }
这样一来,无论什么时候创建上下文类,Database.SetInitializer
方法都会被调用,并且将数据库初始化策略设置为DropCreateDatabaseIfModelChanges
。
如果处于生产环境,那么我们肯定不想丢失已存在的数据。这时我们就需要关闭该初始化器,只需要将null
传给Database.SetInitializer
方法,如下所示:
public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer<SampleDbEntities>(null); }
填充种子数据
到目前为止,无论我们选择哪种策略初始化数据库,生成的数据库都是一个空的数据库。但是许多情况下我们总想在数据库创建之后、首次使用之前就插入一些数据,此外,开发阶段可能想以admin的资格为其填充一些数据,或者为了测试应用在特定的场景中表现如何,想要伪造一些数据。
当我们使用 DropCreateDatabaseAlways
和 DropCreateDatabaseIfModelChanges
初始化策略时,插入种子数据非常重要,因为每次运行应用时,数据库都要重新创建,每次数据库创建之后再手动插入数据非常乏味。接下来我们看一下当数据库创建之后如何使用EF来插入种子数据。
为了向数据库插入一些初始化数据,我们需要创建满足下列条件的数据库初始化器类:
- 从已存在的数据库初始化器类中派生数据
- 在数据库创建期间种子化
1、定义领域实体
假设我们的数据模型Employer定义如下:
public class Employer { public int Id { get; set; } public string EmployerName { get; set; } }
2、创建数据库上下文
使用EF的Code First方法对上面的模型创建数据库上下文:
public class SampleDbEntities : DbContext { public SampleDbEntities() : base("name = AppConnection") { } public DbSet<Employer> Employers { get; set; } }
3、创建数据库初始化器类
假设我们使用的是 DropCreateDatabaseAlways
数据库初始化策略,那么初始化器类就要从该泛型类继承,并传入数据库上下文作为类型参数。接下来,要种子化数据库就要重写DropCreateDatabaseAlways
类的Seed
方法,而Seed方法拿到了数据库上下文,因此我们可以使用它来将数据插入数据库:
public class SeedingDataInitializer : DropCreateDatabaseAlways<SampleDbEntities> { protected override void Seed(SampleDbEntities context) { for (int i = 0; i < 6; i++) { var employer = new Employer { EmployerName = "Employer" + (i + 1) }; context.Employers.Add(employer); } base.Seed(context); } }
前面的代码通过for循环创建了6个Employer对象,并将它们添加给数据库上下文类的Employers
集合属性。这里值得注意的是我们并没有调用DbContext.SaveChanges()
,因为它会在基类中自动调用。
4、将数据库初始化器类用于数据库上下文类
public class SampleDbEntities : DbContext { public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer(new SeedingDataInitializer()); } public DbSet<Employer> Employers { get; set; } }
5、Main方法中访问数据库
static void Main(string[] args) { using (var db = new SampleDbEntities()) { var employers = db.Employers; foreach (var employer in employers) { Console.WriteLine("Id={0}\\tName={1}", employer.Id, employer.EmployerName); } } Console.WriteLine("DB创建成功,并完成种子化!"); Console.Read(); }
6、运行程序,查看效果
Main方法中只是简单的创建了数据库上下文对象,然后将数据读取出来:
此外,我们可以从数据库初始化的Seed
方法中,通过数据库上下文类给数据库传入原生SQL来影响数据库模式。
LINQ to Entities详解
到目前为止,我们已经学会了如何使用Code First方式来创建实体数据模型,也学会了使用EF进行领域建模,执行模型验证以及控制数据库连接参数。一旦数据建模完成,接下来就是要对这些模型进行各种操作了,通常有以下两种方式:
- LINQ to Entities
- Entity SQL
本系列教程只讲LINQ to Entities,Entity SQL就是通过在EF中执行SQL,大家可以自行研究。
1、什么是LINQ to Entities
LINQ,全称是Language-INtegrated Query(集成语言查询),是.NET语言中查询数据的一种技术。LINQ to Entities 是一种机制,它促进了使用LINQ对概念模型的查询。
因为LINQ是声明式语言,它让我们聚焦于我们需要什么数据而不是应该如何检索数据。LINQ to Entities在实体数据模型之上提供了一个很好的抽象,所以我们可以使用LINQ来指定检索什么数据,然后LINQ to Entities provider会处理访问数据库事宜,并为我们取到必要的数据。
当我们使用LINQ to Entities对实体数据模型执行LINQ查询时,这些LINQ查询会首先被编译以决定我们需要获取什么数据,然后执行编译后的语句,从应用程序的角度看,最终会返回.NET理解的CLR对象。
上图展示了LINQ to Entities依赖EntityClient
才能够使用EF的概念数据模型,接下来我们看下LINQ to SQL如何执行该查询并给应用程序返回结果:
- 应用程序创建一个LINQ查询。
- LINQ to Entities会将该LINQ查询转换成
EntityClient
命令。 EntityClient
命令然后使用EF和实体数据模型将这些命令转换成SQL查询。- 然后会使用底层的ADO.NET provider将该SQL查询传入数据库。
- 该查询然后在数据库中执行。
- 执行结果返回给EF。
- EF然后将返回的结果转成CLR类型,比如领域实体。
EntityClient
使用项目,并返回必要的结果给应用程序。
EntityClient
对象寄居在System.Data.EntityClient
命名空间中,我们不必显式创建该对象,我们只需要使用命名空间,然后LINQ to Entities会处理剩下的事情。
如果我们对多种类型的数据库使用LINQ to Entities,那么我们只需要为该数据库使用正确的ADO.NET provider,然后EntityClient
就会使用这个provider对任何数据库的LINQ查询无缝执行。
2、使用LINQ to Entities操作实体
编写LINQ查询的方式有两种:
- 查询语法
- 方法语法
选择哪种语法完全取决你的习惯,两种语法的性能是一样的。查询语法相对更容易理解,但是灵活性稍差;相反,方法语法理解起来有点困难,但是提供了更强大的灵活性。使用方法语法可以进行链接多个查询,因此在单个语句中可以实现最大的结果。
下面以一个简单的例子来理解一下这两种方法的区别。创建一个控制台应用,名称为“Donators_CRUD_Demo”,该demo也用于下面的CRUD一节。
领域实体模型定义如下:
public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } }
数据库上下文定义如下:
public class DonatorsContext : DbContext { public DonatorsContext() : base("name=EFCodeFirst") { } public virtual DbSet<Donator> Donators { get; set; } }
定义好连接字符串之后,如果使用该实体数据模型通过执行LINQ查询来获取Donator数据,那么可以在数据库上下文类的Donators集合上操作。下面我们用两种方法来实现“找出打赏了50元的打赏者”。
(1) 查询语法
//1.查询语法 var donators = from donator in db.Donators where donator.Amount == 50 select donator;
(2) 方法语法
//2.方法语法 var donators = db.Donators.Where(d => d.Amount == 50m);
完整的Main方法如下:
static void Main(string[] args) { using (var db = new DonatorsContext()) { //1.查询语法 //var donators = from donator in db.Donators where donator.Amount == 50 select donator; //2.方法语法 var donators = db.Donators.Where(d => d.Amount == 50m); Console.WriteLine("Id\\t姓名\\t金额\\t打赏日期"); foreach (var donator in donators) { Console.WriteLine("{0}\\t{1}\\t{2}\\t{3}", donator.Id, donator.Name, donator.Amount, donator.DonateDate.ToShortDateString()); } } Console.WriteLine("Operation completed!"); Console.Read(); }
两种方法的LINQ查询我们都是使用了var
隐式类型变量将LINQ查询的结果存储在了donators变量中。使用LINQ to Entities,我们可以使用隐式类型变量作为输出结果,编译器可以由该隐式变量基于LINQ查询推断出输出类型。一般而言,输出类型是IQueryable<T>
类型,我们的例子中应该是IQueryable<Donator>
。当然我们也可以明确指定返回的类型为IQueryable<Donator>
或者IEnumerable<Donator>
。
重点理解
当使用LINQ to Entities时,理解何时使用
IEnumerable
和IQueryable
很重要。如果使用了IEnumerable
,查询会立即执行,如果使用了IQueryable
,直到应用程序请求查询结果的枚举时才会执行查询,也就是查询延迟执行了,延迟到的时间点是枚举查询结果时。
如何决定使用IEnumerable
还是IQueryable
呢?使用IQueryable
会让你有机会创建一个使用多条语句的复杂LINQ查询,而不需要每条查询语句都对数据库执行查询。该查询只有在最终的LINQ查询要求枚举时才会执行。
3、LINQ操作
为了方便展示,我们需要再创建一张表,因此,我们需要再定义一个实体类,并且要修改之前的实体类,如下所示:
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; } }
public class Province { public Province() { Donators = new Collection<Donator>(); } public int Id { get; set; } [StringLength(225)] public string ProvinceName { get; set; } public virtual ICollection<Donator> Donators { get; set; } }
从上面定义的POCO类,我们不难发现,这两个实体之间是一对多的关系,一个省份可能会有多个打赏者,至于为何这么定义,上一篇已经提到了,这篇不再啰嗦。Main方法添加了一句代码Database.SetInitializer(new DropCreateDatabaseIfModelChanges<DonatorsContext>());
,运行程序,会生成新的数据库,然后插入以下数据(数据纯粹是为了演示,不具真实性):
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)
(1) 执行简单的查询
平时我们会经常需要从某张表中查询所有数据的集合,如这里查询所有打赏者的集合:
//查询语法 //var donators = from donator in context.Donators select donator; //方法语法 var donators = context.Donators;
下面是该LINQ查询生成的SQL:
SELECT [t0].[Id], [t0].[Name], [t0].[Amount], [t0].[DonateDate], [t0].[Province_Id]
FROM [Donators] AS [t0]
LINQPad是一款练习LINQ to Entities出色的工具。在LINQPad中,我们已经在DbContext或ObjectContext内部了,不需要再实例化数据库上下文了,我们可以使用LINQ to Entities查询数据库。我们也可以使用LINQPad查看生成的SQL查询了。
LINQPad多余的就不介绍了,看下图,点击图片下载并学习。
下图为LINQPad将linq语法转换成了SQL。
(2) 使用导航属性
如果实体间存在一种关系,那么这个关系是通过它们各自实体的导航属性进行暴露的。在上面的例子中,省份Province
实体有一个Donators集合属性用于返回该省份的所有打赏者,而在打赏者Donator
实体中,也有一个Province属性用于跟踪该打赏者属于哪个省份。导航属性简化了从一个实体到和它相关的实体,下面我们看一下如何使用导航属性获取与其相关的实体数据。
比如,我们想要获取“山东省的所有打赏者”:
//查询语法 var donators = from province in context.Provinces where province.ProvinceName == "山东省" from donator in province.Donators select donator; //查询语法 var donators = context.Provinces.Where(province => province.ProvinceName == "山东省").SelectMany(province => province.Donators);
最终的查询结果都是一样的:
反过来,如果我们想要获取打赏者“雪茄”的省份:
//查询语法 var provices = from donator in context.Donators where donator.Name == "雪茄" select donator.Province; //方法语法 //var provices = context.Donators.Where(donator => donator.Name == "雪茄").Select(donator => donator.Province);
(3) 过滤数据
实际上之前已经介绍了,根据某些条件过滤数据,可以在LINQ查询中使用Where
。比如上面我们查询了山东省的所有打赏者,这里我们过滤出打赏金额在10~20元之间的打赏者:
//查询语法 /* var donators = from donator in context.Donators where donator.Amount >= 10M && donator.Amount <= 20M select donator; */ //方法语法 var donators = context.Donators.Where(donator => donator.Amount >= 10 && donator.Amount <= 20);
最终查询的结果如下:
生成的SQL语句在这里不在贴出来了,大家自己通过LINQPad或者其他工具自己去看吧!只要知道EF会帮助我们自动将LINQ查询转换成合适的SQL语句就可以了。
(4) LINQ投影
如果不指定投影的话,那么默认就是选择该实体或与之相关实体的所有字段,LINQ投影就是返回这些实体属性的子集或者返回一个包含了多个实体的某些属性的对象。
投影一般用在应用程序中的VIewModel(视图模型),我们可以从LINQ查询中直接返回一个视图模型。比如,我们想要查出“所有省的所有打赏者”:
class Program { static void Main(string[] args) { using (var context = new DonatorsContext()) { //查询语法 /* var provinces = from province in context.Provinces select new { Province = province, Donators = province.Donators }; */ //方法语法 var provinces = context.Provinces.Select(province => new { Province = province, Donators = province.Donators }); foreach (var province in provinces) { foreach (var donator in province.Donators) { Console.WriteLine("{0}\\t{1}", province.Province.ProvinceName, donator.Name); } } } Console.WriteLine("Operation completed!"); Console.Read(); } }
执行结果如下:
当然,如果我们已经定义了一个包含了Province
和DonatorList
属性的类型(比如视图模型),那么也可以直接返回该类型,下面只给出方法语法(查询语法大家可自行写出)的写法:
public class DonatorsWithProvinceViewModel { public string Province { get; set; } public ICollection<Donator> DonatorList { get; set; } }
//方法语法 var provinces = context.Provinces.Select(province => new DonatorsWithProvinceViewModel() { Province = province.ProvinceName, DonatorList = province.Donators });
在
IQueryable<T>
中处理结果也会提升性能,因为直到要查询的结果进行枚举时才会执行生成的SQL。
(5) 分组Group
分组的重要性相必大家都知道,这个肯定是要掌握的!下面就看看两种方法的写法。
//查询语法 /* var donatorsWithProvince = from donator in context.Donators group donator by donator.Province.ProvinceName into groupedProvince select new { ProvinceName = groupedProvince.Key, Donators = groupedProvince }; */ //方法语法 var donatorsWithProvince = context.Donators.GroupBy(donator => donator.Province.ProvinceName).Select(groupedDonators => new { ProvinceName = groupedDonators.Key, Donators = groupedDonators }); foreach (var donatorWithProvince in donatorsWithProvince) { Console.WriteLine("{0}\\t{1}", donatorWithProvince.ProvinceName, donatorWithProvince.Donators.Count()); }
稍微解释一下吧,上面的代码会根据省份名称进行分组,最终以匿名对象的投影返回。结果中的ProvinceName就是分组时用到的字段,Donators属性包含了通过ProvinceName找到的Donator集合。
执行结果如下:
(6) 排序Ordering
对特定的列进行升序或降序排列也是经常使用的操作。比如我们按照打赏金额升序排序。
//查询语法 var orderedDonators = from donator in context.Donators orderby donator.Amount descending select donator; //方法语法 //var orderedDonators = context.Donators.OrderByDescending(item => item.Amount); foreach (var orderedDonator in orderedDonators) { Console.WriteLine("{0}\\t{1}", orderedDonator.Name, orderedDonator.Amount); }
升序查询执行结果:
只要删除掉descending关键字就是升序排序了,默认排序方式是升序。
(7) 聚合操作
使用LINQ to Entities可以执行下面的聚合操作:
- Count-数量
- Sum-求和
- Min-最小值
- Max-最大值
- Average-平均值
下面我找出山东省打赏者的数量:
using (var context = new DonatorsContext()) { //查询语法 var count1 = (from donator in context.Donators where donator.Province.ProvinceName == "山东省" select donator).Count(); //方法语法 var count2 = context.Donators.Count(item => item.Province.ProvinceName == "山东省"8天掌握EF的Code First开发9.6 翻译系列:数据注解之Index特性EF 6 Code-First系列
20.1翻译系列:EF 6中自动数据迁移技术EF 6 Code-First系列
EF中三大开发模式之DB First,Model First,Code First以及在Production Environment中的抉择