本文出自8天掌握EF的Code First开发系列,经过自己的实践整理出来。
本篇目录
- 创建控制台项目
- 根据.Net中的类来创建数据库
- 简单的CRUD操作
- 数据库模式更改介绍
- 本章小结
本人的实验环境是VS 2012,windows 7,MSSQL Server 2008 R2。
创建控制台项目
1. 新建控制台应用项目
2. 通过NuGet安装Entity Framework 6
根据.Net中的类来创建数据库
上面的步骤之后,我们就可以开始写代码了。在写代码之前,你要始终记得,每个类就是相应的数据表中的一行数据,该类的属性对应的是这行数据的列。为了表示赞助者们对我的支持,我决定用他们的数据进行举例。
1. 创建实体数据模型
我们现在创建一个捐赠者类Donator:
namespace EFCodeFirst { public class Donator { public int DonatorId { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonatorDate { get; set; } } }
我们需要定义和期望的数据库类型相匹配的属性。上面的例子中,.Net中的int类型会映射到SQL Server中的int类型,string类型会映射到Nvarchar(max)类型,decimal和Datetime也和SQL Server中的一样。大多数时候,我们不需要关心这些细节,我们只需要编写能够表示数据的模型类就行了,然后使用标准的.Net类型定义属性,其他的就让EF自己计算出保存数据所需要的RDBMS类型。
2. 创建数据库上下文
接下来我们创建数据库上下文,它是数据库的抽象。目前,我们只有一张表Donator,因而要给该数据库上下文定义一个属性来代表这张表。再者,一张表中一般肯定不止一条数据行,所以我们必须定义一个集合属性,EF使用DbSet来实现这个目的。
namespace EFCodeFirst { public class Context : DbContext { public Context() :base("name = EFCodeFirst") { } public DbSet<Donator> Donators { get; set; } } }
在这里,DbContext 是所有基于 EF 的上下文基类,通过它可以访问到数据库中的所有表。上面的代码中调用了父类的构造函数,并且传入了一个键值对,键是 name ,值是 EFCodeFrist ,这个键值对是定义在应用程序的配置文件中的,取决于你的应用程序类型,可能是 app.config 或者 web.config 。在我们的控制台应用程序中就是 app.config。
在 app.config 文件的 configuration 的节点下(不要在第一个节点下,否则报错)添加:
<connectionStrings> <add name="EFCodeFirst" connectionString="server=DTSZOPAULHUANG\SQLEXPRESS;Database=EFCodeFirst;Integrated Security=SSPI" providerName="System.Data.SqlClient"/> </connectionStrings>
接下来就应该是创建数据库了,创建数据库的方式有两种:
- 在后面的博客中我们会通过数据库迁移来实现数据库的创建,原理就是数据库会在第一次查询,更新或插入操作时创建。
- 通过EF数据库的API来创建。
这里我们先通过第二种方法来创建数据库。首先我们必须确保数据库中没有和我们现在要创建的数据库同名的数据库存在,否则会提示错误。当然,我们也可以通过EF的API访问数据库来创建数据库。
namespace EFCodeFirst { class Program { static void Main(string[] args) { using (var context = new Context()) { context.Database.CreateIfNotExists(); } Console.WriteLine("Database 创建成功"); Console.ReadKey(); } } }
最后,我们只需要确保我们的连接字符串没有问题。现在,运行程序,打开SSMS(当然也可以在VS的服务器资源管理器查看)进行确认即可。
可以很清楚地看到,数据表名是自定义数据库上下文的属性,而表中的列是数据模型的属性。此外,注意一下列的类型。EF默认将DonatorId作为了主键,string类型的Name在数据库中的类型是nvarchar(max),这些都是在使用EF时必须注意的命名规范(或者约定)。
简单的CRUD操作
首先,大脑中时刻要有这张图,这张图也是很重要的概念:
1. 创建记录——Create
你可以这样认为,将对象添加到集合中就相当于将数据行插入到数据库的相应的表中。我们使用 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<Donator> { new Donator { Name = "陈志康", Amount = 50, DonatorDate = new DateTime(2016, 4, 7) }, new Donator { Name = "海风", Amount = 5, DonatorDate = new DateTime(2016, 4, 8) }, new Donator { Name = "醉千秋", Amount = 18.8m, DonatorDate = new DateTime(2016, 4, 15) } }; context.Donators.AddRange(donators); context.SaveChanges(); } Console.ReadKey(); } }
这里需要注意两点:
- 不需要给 DonatorId 属性赋值,因为它对应到 SQL Server 表中的主键列,它的值是自动生成的,当 SaveChanges 执行之后,打个断点就能看到返回的 DonatorId 已经有值了。
- Context 的实例用了 using 语句包装起来,这是因为 DbContext 实现了 IDisposabl e接口。Dbcontext 还包含了 DbConnection 的实例,该实例指向了具有特定连接字符串的数据库。在EF中合适地释放数据库连接和 ADO.NET 中同等重要。
执行结果与在VS中查看数据是否生成:
2. 查询记录——Retrieve
查询时也是直接通过DbSet进行查询的。
private static void Search() { using (var context = new Context()) { 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.DonatorDate.ToShortDateString()); } } }
如果像下面那样打一个断点,你会看到一个结果视图,点击那个类似刷新的图标会看到查询的结果,这个东西道出了 EF 中很重要的一个概念: 延迟查询。但是此时还没有真正查询数据库,只有当LINQ的查询结果被访问或者枚举时才会将查询命令发送到数据库。EF 是基于 Dbset 实现的 IQueryable 接口来处理延迟查询的。
最后,我使用了一个foreach循环将结果枚举出来,这样就执行了SQL,结果如下:
3. 更新记录——Update
在 SQL 中,更新需要使用 Update 命令。而在 EF 中,我们要找到 DbSet 集合中要更新的对象,然后更改其属性,最后调用 SaveChanges 方法即可。下面将赞助者的名称进行修改。
private static void Update() { using (var context = new Context()) { var donator = context.Donators.FirstOrDefault(item => item.Name.Equals("醉千秋")); if (donator != null) { donator.Name = "醉、千秋"; context.SaveChanges(); } } }
这里我们使用了 FirstOrDefault() 扩展方法来判断序列中是否存在特定的元素,如果存在则修改目标对象的 Name 属性,之后调用 SaveChanges 方法。当然这里也可以使用 First、Single 或者 SingleOrDefault 方法都可以。这四个方法之间的区别请参考这里
最后检测数据库操作成功的结果:
4. 删除记录——Delete
接下来要删除记录,先把剩下的打赏者数据全部放到数据库,然后再在最后加入一条测试数据,如下:
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‘)
要删除一条数据,就先要找到这条数据,删除代码如下:
private static void Delete() { using (var context = new Context()) { //根据Name找到要删除的测试数据 var toBeDeletedDonator = context.Donators.Single(d => d.Name == "待打赏"); if (toBeDeletedDonator != null) { //如果满足条件,就将该对象使用Remove方法标记为Deleted context.Donators.Remove(toBeDeletedDonator); //最后持久化到数据库 context.SaveChanges(); } } }
注意:上面使用了 First 方法来查找待删除的数据,但是如果在数据库中没有满足条件的记录存在,则会抛出异常,所以使用 First 方法时要注意。
数据库模式更改介绍
如果你修改了Donator类或者又添加了新的DbSet属性(即添加了新表),在操作的过程中你可能会遇到一些异常。现在,我想再添加一张表 PayWays ,用来存储打赏者的打赏方式,比如微信,支付宝,QQ红包等。
定义 PayWay 类, 包含两个属性,以后可能会增加属性和 Donator 表关联:
public class PayWay { public int Id { get; set; } public string Name { get; set; } }
修改 Context 类,使之包含 PayWays 数据集合。
public class Context : DbContext { public Context() :base("name = EFCodeFirst") { } public DbSet<Donator> Donators { get; set; } public DbSet<PayWay> PayWays { get; set; } }
我们这里更加明显地可以看到,数据库上下文代表了整个数据库,它包含多个表,每张表都成为了Context类的一个属性。
现在,如果我们运行程序,并循环枚举支付方式会出现什么情况呢?
意思是:自从数据库创建以来,模型背后的数据库上下文‘Context’已经发生了变化,也就是当初的数据库和现在的上下文对不上了。呵呵!当然对不上了,数据库上下文被我更改了,而数据库没改啊!
在后面我会介绍数据库迁移和对已存在的数据库进行迁移,但是我们现在也要解决这个当前问题。这就引入了初始化器(Initializer)的概念。初始化器会在首次实例化过程期间或者EF首次访问数据库时运行。EF中需要关心的初始化器有三种:
- CreateDatabaseIfNotExists<TContext>
- DropCreateDatabaseIfModelChanges<TContext>
- DropCreateDatabaseAlways<TContext>
这三种初始化器看其名字都很好理解,CreateDatabaseIfNotExists<TContext> 指如果数据库不存在则创建,DropCreateDatabaseIfModelChanges<TContext> 指如果模型改变了(包括模型类的更改以及上下文中集合属性的添加和移除)就销毁之前的数据库再创建数据库,DropCreateDatabaseAlways<TContext> 总是销毁再重新创建数据库,如果没有指定的话默认使用第一个初始化器。
现在我们使用第二个初始化器,要使用该初始化器,我们应该在实例化数据库上下文之前就要让 EF 知道。我们可以使用 EF 的 API 中 Database 类的 SetInitialize r静态方法。在我们的控制台应用中,我们应该将它放在Main方法的第一行:
static void Main(string[] args) { using (var context = new Context()) { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>()); var payWays = context.PayWays; Console.WriteLine("姓名\t\t"); foreach (var payWay in payWays) { Console.WriteLine("{0}\t\t", payWay.Name); } } Console.ReadKey(); }
运行程序,但是很不幸,还是有异常抛出。
这是因为数据库在其他的应用程序中是打开的,如SQL Server Management Studio。此时,只要关闭其他的应用程序就可以了。关闭 SQL Server Management Studio 再运行程序。
程序运行正确且数据库结构发生了变化
需要注意的是,因为上面的初始化器会销毁之前的数据库,因此之前累积的所有数据也都丢失了。很显然,这种用法不适合生产环境,但是我们学习EF或者项目早期是很方便的。另外一个有趣的功能是,初始化器允许我们在目标数据库创建之后运行其他代码,可以通过重写Seed方法即可。该方法需要一个数据库上下文的实例参数:
Database.SetInitializer(new DatabaseInitializer());
public class DatabaseInitializer : DropCreateDatabaseIfModelChanges<Context> { protected override void Seed(Context context) { context.PayWays.AddRange(new List<PayWay> { 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 。之后,让程序跑了起来。然后我们发现数据库中产生了和该属性同名的数据表。数据库的创建过程使用了很多约定,包括表名和主键的确定。