本文共 10129 字,大约阅读时间需要 33 分钟。
上一篇,只是大概地从整体上了解了一下Entity Framework,纯粹理论,没有一点代码,但是推荐数量飙升。博主因此也感觉到了某些园友们的气息里透漏着些许火药味,确实没有啥干货啊,这个博主承认的,博主也请各位谅解,并听我给你解释解释。
下面我们正式开始今天的正题。本篇的源码:,本人的实验环境是VS 2013 Update 5,windows 10,MSSQL Server 2008。
Nuget包管理器控制台 :Install-Package EntityFramework
,也可以通过可视化窗口进行安装,如下
上面的步骤之后,我们就可以开始写代码了。在写代码之前,你要始终记得,每个类就是相应的数据表中的一行数据,该类的属性对应的是这行数据的列。为了表示赞助者们对我的支持,我决定用他们的数据进行举例。
我们现在创建一个捐赠者类Donator:
namespace FirstCodeFirstApp{ public class Donator { public int DonatorId { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } }}
我们需要定义和期望的数据库类型相匹配的属性。上面的例子中,.Net中的int类型会映射到SQL Server中的int类型,string类型会映射到所有可能的字符类型,decimal和Datetime也和SQL Server中的一样。大多数时候,我们不需要关心这些细节,我们只需要编写能够表示数据的模型类就行了,然后使用标准的.Net类型定义属性,其他的就让EF自己计算出保存数据所需要的RDBMS类型。
接下来我们创建数据库上下文,它是数据库的抽象。目前,我们只有一张表Donator,因而要给该数据库上下文定义一个属性来代表这张表。再者,一张表中一般肯定不止一条数据行,所以我们必须定义一个集合属性,EF使用DbSet
来实现这个目的。
namespace FirstCodeFirstApp{ public class Context:DbContext { public Context() : base("name=FirstCodeFirstApp") { } public DbSetDonators { get; set; } }}
在这里,DbContext是所有基于EF的上下文基类,通过它可以访问到数据库中的所有表。上面的代码中调用了父类的构造函数,并且传入了一个键值对,键是name,值是FirstCodeFirstApp,这个键值对是定义在应用程序的配置文件中的,取决于你的应用程序类型,可能是app.config或者web.config。在我们的控制台应用程序中就是app.config。
在app.config文件的configuration
的节点下(不要在第一个节点下,否则报错)添加:
接下来就应该是创建数据库了,创建数据库的方式有两种:
这里我们先通过第二种方法来创建数据库。首先我们必须确保数据库中没有和我们现在要创建的数据库同名的数据库存在,否则会提示错误。当然,我们也可以通过EF的API访问数据库来创建数据库。
namespace FirstCodeFirstApp{ class Program { static void Main(string[] args) { using (var context=new Context()) { context.Database.CreateIfNotExists();//如果数据库不存在时则创建 } Console.Write("DB has Created!");//提示DB创建成功 Console.Read(); } }}
最后,我们只需要确保我们的连接字符串没有问题。现在,运行程序,打开SSMS(当然也可以在VS的服务器资源管理器查看)进行确认即可。
可以很清楚地看到,数据表名是自定义数据库上下文的属性,而表中的列是数据模型的属性。此外,注意一下列的类型。EF默认将DonatorId
作为了主键,string类型的Name在数据库中的类型是nvarchar(max)
,这些都是在使用EF时必须注意的命名规范(或者约定)。
首先,大脑中时刻要有这张图,这张图也是很重要的概念:
你可以这样认为,将对象添加到集合中就相当于将数据行插入到数据库的相应的表中。我们使用DbSet
的Add
方法来实现新数据的添加,而DbContext
类的SaveChanges
方法会将未处理的更改提交到数据库,这是通过检测上下文中所有的对象的状态来完成的。所有的对象都驻留在上下文类的DbSet
属性中,比如,我们的例子只有一个Donators属性,那么所有的捐赠人的数据都会存储到这个泛型集合属性中。数据库上下文会跟踪DbSet
属性中的所有对象的状态,这些状态有这么几种:Deleted,Added,Modified和Unchanged。如果你想在一个表中插入多行数据,那么只需要添加该表对应的类的多个对象的实例即可,然后就使用SaveChanges
方法将更改提交到数据库,该方法是以单事务执行的。最终,所有的数据库更改都会以单个工作单元持久化。既然是事务的,那么这就允许将批量相关的更改作为单个操作提交,这样就保证了事务一致性和数据完整性。
修改Main方法如下:
class Program { static void Main(string[] args) { using (var context = new Context()) { context.Database.CreateIfNotExists();//如果数据库不存在时则创建 var donators = new List{ new Donator { Name = "陈志康", Amount = 50, DonateDate = new DateTime(2016, 4, 7) }, new Donator { Name = "海风", Amount = 5, DonateDate = new DateTime(2016, 4, 8) }, new Donator { Name = "醉千秋", Amount = 18.8m, DonateDate = new DateTime(2016, 4, 15) } }; context.Donators.AddRange(donators); context.SaveChanges(); } // Console.Write("DB has Created!");//提示DB创建成功 Console.Write("Creation Finished!");//提示创建完成 Console.Read(); } }
这里需要注意两点:
SaveChanges
执行之后,打个断点就能看到返回的DonatorId已经有值了。执行结果与在VS中查看数据是否生成:
查询时也是直接通过DbSet进行查询的。
#region 2.0 查询记录 var donators = context.Donators; Console.WriteLine("Id\t\t姓名\t\t金额\t\t赞助日期"); foreach (var donator in donators) { Console.WriteLine("{0}\t\t{1}\t\t{2}\t\t{3}", donator.DonatorId,donator.Name, donator.Amount, donator.DonateDate.ToShortDateString()); } #endregion
如果像下面那样打一个断点,你会看到一个结果视图,点击那个类似刷新的图标会看到查询的结果,这个东西道出了EF中很重要的一个概念: 延迟查询。但是此时还没有真正查询数据库,只有当LINQ的查询结果被访问或者枚举时才会将查询命令发送到数据库。EF是基于Dbset实现的IQueryable接口来处理延迟查询的。
最后,我使用了一个foreach循环将结果枚举出来,这样就执行了SQL,结果如下:
物质化:Materialization
从数据库中读取数据,通过
DbDataReader
转成.Net对象的过程。
在SQL中,更新需要使用Update命令。而在EF中,我们要找到DbSet集合中要更新的对象,然后更改其属性,最后调用SaveChanges方法即可。
下面我将赞助者“醉千秋”改为“醉、千秋”(因为他的博客园名字的确是这样的)。#region 3.0 更新记录var donators = context.Donators;if (donators.Any()){ var toBeUpdatedDonator = donators.First(d => d.Name == "醉千秋"); toBeUpdatedDonator.Name = "醉、千秋"; context.SaveChanges();}#endregion
这里我们使用了Any()
扩展方法来判断序列中是否有元素,然后使用First()
扩展方法来找到Name == "醉千秋"
的元素,最后给目标对象的Name属性赋予新值,之后调用SaveChanges
方法。
如果我们打开SQL Server Profiler,我们会看到下面的记录:
第一条记录是查询语句,找到对象;第二条是更新语句,更改属性。最后检测数据库操作成功的结果:
接下来要删除记录,我把剩下的打赏者数据全部放到数据库,然后再在最后加入一条测试数据,如下:
INSERT dbo.Donators VALUES ( N'雪茄', 10, '2016-04-08')INSERT dbo.Donators VALUES ( N'王小乙', 10, '2016-04-09')INSERT dbo.Donators VALUES ( N'键盘里的鼠标', 12, '2016-04-13')INSERT dbo.Donators VALUES ( N'smallpig', 10, '2016-04-13')INSERT dbo.Donators VALUES ( N'Darren', 5, '2016-04-15')INSERT dbo.Donators VALUES ( N'jeffrey', 10, '2016-04-15')INSERT dbo.Donators VALUES ( N'危杨益', 6.66, '2016-04-15')INSERT dbo.Donators VALUES ( N'Mr.Lan', 10, '2016-04-15')INSERT dbo.Donators VALUES ( N'周旭龙', 5, '2016-04-15')INSERT dbo.Donators VALUES ( N'403', 10.24, '2016-04-15')INSERT dbo.Donators VALUES ( N'cuibty', 8.88, '2016-04-15')INSERT dbo.Donators VALUES ( N'dennylo', 10.24, '2016-04-17')INSERT dbo.Donators VALUES ( N'lee', 5, '2016-04-18')INSERT dbo.Donators VALUES ( N'利平', 18.8, '2016-04-18')INSERT dbo.Donators VALUES ( N'听海船说', 20, '2016-04-19')INSERT dbo.Donators VALUES ( N'喝前摇一摇', 5, '2016-04-19')INSERT dbo.Donators VALUES ( N'黄大仙', 50, '2016-04-19')INSERT dbo.Donators VALUES ( N'夜未眠', 10, '2016-04-19')INSERT dbo.Donators VALUES ( N'A.L', 8.88, '2016-04-19')INSERT dbo.Donators VALUES ( N'transient', 5, '2016-04-19')INSERT dbo.Donators VALUES ( N'晓东', 6.66, '2016-04-20')INSERT dbo.Donators VALUES ( N'待打赏', 10, '2016-04-20')
要删除一条数据,就先要找到这条数据,删除代码如下:
#region 4.0 删除记录var toBeDeletedDonator = context.Donators.Single(d => d.Name == "待打赏");//根据Name找到要删除的测试数据if (toBeDeletedDonator!=null){ context.Donators.Remove(toBeDeletedDonator);//如果满足条件,就将该对象使用Remove方法标记为Deleted context.SaveChanges();//最后持久化到数据库}#endregion
如果你修改了Donator类或者又添加了新的DbSet属性(即添加了新表),在操作的过程中你可能会遇到一些异常。现在,我想再添加一张表PayWays,用来存储打赏者的打赏方式,比如微信,支付宝,QQ红包等。
先定义两个字段,以后可能会增加字段和Donator表关联:
namespace FirstCodeFirstApp{ public class PayWay { public int Id { get; set; } public string Name { get; set; } }}
在Context
类中添加这句代码public DbSet<PayWay> PayWays { get; set; }
。
我们这里更加明显地可以看到,数据库上下文代表了整个数据库,它包含多个表,每张表都成为了Context
类的一个属性。
现在,如果我们运行程序,并循环枚举支付方式会出现什么情况呢?
啊哦!抛异常了:The model backing the 'Context' context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269).
意思是:自从数据库创建以来,模型背后的数据库上下文‘Context’已经发生了变化,也就是当初的数据库和现在的上下文对不上了。呵呵!当然对不上了,数据库上下文被我更改了,而数据库没改啊!
在后面我会介绍数据库迁移和对已存在的数据库进行迁移,但是我们想在也要解决这个当前问题。这就引入了初始化器(Initializer)的概念。初始化器会在首次实例化过程期间或者EF首次访问数据库时运行。EF中需要关心的初始化器有三种:
CreateDatabaseIfNotExists<TContext>
DropCreateDatabaseIfModelChanges<TContext>
DropCreateDatabaseAlways<TContext>
这两种初始化器看其名字都很好理解,CreateDatabaseIfNotExists<TContext>
指如果数据库不存在则创建,DropCreateDatabaseIfModelChanges<TContext>
指如果模型改变了(包括模型类的更改以及上下文中集合属性的添加和移除)就销毁之前的数据库再创建数据库,DropCreateDatabaseAlways<TContext>
总是销毁再重新创建数据库,如果没有指定的话默认使用第一个初始化器。
namespace FirstCodeFirstApp{ public class Initializer:DropCreateDatabaseIfModelChanges{ }}
要使用该初始化器,我们应该在实例化数据库上下文之前就要让EF知道。我们可以使用EF的API中Database
类的SetInitializer
静态方法。在我们的控制台应用中,我们应该将它放在Main方法的第一行:
static void Main(string[] args){ Database.SetInitializer(new Initializer()); //more code}
如果我们现在运行程序,则不会抛异常了,而且数据库结构也发生变化了。
如果数据库在其他的应用程序中是打开的,如SQL Server Management Studio,那么会抛出一个不同的异常,会通知你EF无法获得一个独占的锁定以销毁数据库。此时,只要关闭其他的应用程序就可以了。
未经处理的异常: System.Data.SqlClient.SqlException: 无法删除数据库 "CodeFirstApp",因为该数据库当前正在使用。
需要注意的是,因为上面的初始化器会销毁之前的数据库,因此之前累积的所有数据也都丢失了。很显然,这种用法不适合生产环境,但是我们学习EF或者项目早期是很方便的。另外一个有趣的功能是,初始化器允许我们在目标数据库创建之后运行其他代码,可以通过重写Seed
方法即可。该方法需要一个数据库上下文的实例参数:
public class Initializer:DropCreateDatabaseIfModelChanges{ protected override void Seed(Context context) { context.PayWays.AddRange(new List { new PayWay{Name = "支付宝"}, new PayWay{Name = "微信"}, new PayWay{Name = "QQ红包"} }); }}
可以看到,在Seed
方法中,我们不需要调用SaveChanges
方法。此外,要更新生产数据库,我们可以使用EF Migrations。
如上图,Donators表中的数据都销毁了,而Seed方法给PayWays表中添加了数据。
这篇博客中,我们创建了第一个基于EF Code First的控制台应用程序。我们使用Nuget添加了EF的引用。然后我们确定要将赞助楼主的数据存储在数据库中,然后创建了一个Donator类映射到数据库中的Donators表。然后创建了数据库抽象Context类,它继承自DbContext,并在它的构造函数中指定了想要的连接字符串,同时把该连接字符串添加到应用的配置文件中。然后,我们给数据库上下文添加了一个属性Donators,它是Donator的集合,即DbSet。之后,让程序跑了起来。然后我们发现数据库中产生了和该属性同名的数据表。数据库的创建过程使用了很多约定,包括表名和主键的确定。