我们刚发布了实体框架特性第五个社区技术预览版 (简称CTP5)。 特性CTP5版包括了我们计划在2011年第一个季度以独立的软件包形式发布的新特性的预览内容,希望得到大家的反馈。特性CTP5版建立在随.NET框架4.0和Visual Studio 2010发布的现有实体框架4(简称EF4)的功能之上,是我们先前CTP版本的更新版。
本贴将对代码优先(Code First)开发作一个简介。代码优先允许你使用 C# 或 VB.Net类定义你的模型,可以使用特性(attributes)在你的类和属性上,或者使用流畅(Fluent)API,附加额外的配置。你的模型可以用来生成数据库定义或者映射到现有的数据库上。
映射到现有数据库
在CTP5版本中,我们去除了映射到现有数据库时要做额外配置的需要。如果Code First检测到它指向一个它并没有生成的现有数据库定义,那它就会“信任你”,尝试对该数据库定义使用code first的方法。将Code First指向一个现有数据库最简单的方法是加一个App/Web.config连接字符串,给它一个与你的DbContext继承类同样的名称,例如:
<connectionStrings>
<add
name="MyProductContext"
providerName="System.Data.SqlClient"
connectionString="Server=.\SQLEXPRESS;Database=Products;Trusted_Connection=true;"/>
</connectionStrings>
这个示范将演示使用 Code First 生成数据库定义,但同样的原理适用于映射到现有的数据库上,除了下面的“第8步: 设置起始化策略”并不适用于现有数据库外。
1. 安装 EF CTP5版
如果你还没安装的话,那么你需要安装实体框架特性CTP5。
2. 创建应用
为简单起见,我们将建造一个基本的console应用,使用Code First来做数据访问:
- 打开 Visual Studio 2010
- 文件 -> 新 -> 项目…
- 从左边菜单中选择 “Windows”,然后 “Console应用”
- 输入“CodeFirstSample”作其名称
- 选择 “OK”
3. 创建模型
让我们使用类来定义一个非常简单的模型,我就在Program.cs文件里定义这些类,但在实际应用中,你应该将你的类们分离成单独的文件,甚至单独的项目。
在Program.cs文件中的Program类定义下面,我定义了下面2个类:
public class Category
{
public string CategoryId { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public string CategoryId { get; set; }
public virtual Category Category { get; set; }
}
4. 创建一个上下文(Context)
开始使用类来做数据访问最简洁的方法是,定义一个继承自System.Data.Entity.DbContex的上下文类,对我的模型中的每个类呈示一个类型为DbSet<TEntity>的属性。
现在我们将开始使用CTP5版中的类型,所以我们需要加一个对CTP5程序集的引用:
- 项目 -> 添加引用…
- 选择 “.NET” 页
- 从列表中选择“EntityFramework”
- 点击 “OK”
你还需要引用现有的实体框架程序集:
- 项目 -> 添加引用…
- 选择 “.NET” 页
- 从列表中选择 “System.Data.Entity”
- 点击 “OK”
在Program.cs文件的顶部加一个using System.Data.Entity的语句:
using System.Data.Entity;
在我们刚定义的类下面加一个上下文继承类:
public class ProductContext : DbContext
{
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
}
这些就是我们开始保存和获取数据所需的全部代码,很明显,在幕后发生了很多事情,等一会儿我们来看一下其中细节,但首先我们来用用看。
5. 读写数据
在我的Program类中的Main方法里我添加了若干代码,如下:
class Program
{
static void Main(string[] args)
{
using (var db = new ProductContext())
{
// Add a food category
var food = new Category { CategoryId = "FOOD", Name = "Foods" };
db.Categories.Add(food);
int recordsAffected = db.SaveChanges();
Console.WriteLine(
"Saved {0} entities to the database, press any key to exit.",
recordsAffected);
Console.ReadKey();
}
}
}
你现在可以运行应用,会看到插入了新的Category。
我的数据在哪里?
按约定,DbContext会在localhost\SQLEXPRESS中为你创建一个数据库。数据库的名称是根据你的上下文继承类的全名来定的,在我们的情况下,数据库名为“CodeFirstSample.ProductContext”,我们将在本示范的稍后看一下改变这个行为的方法。
模型的发现
DbContext通过查看我们定义的DbSet属性,推断出包括在模型中的类。然后它会使用默认的Code Firs约定,找出主键,外键等。在CTP5版中实现的全套约定在这个约定设计博客贴子中有详细的讨论。
6. 读写更多的数据
让我们在刚编写的程序中再多加一些代码,来展示更多的功能。我们将利用DbSet类中的Find方法,根据主键查询一个实体。如果没找到匹配对象,那么Find方法会返回null。我们还将利用LINQ对在Food分类中的所有产品作查询,并将结果按name的字母顺序排列。查询使用现有的LINQ to Entities提供器,所以它支持EF4中ObjectSet/ObjectQuery同样的查询。
将我们刚编写的Main方法替换成下列代码:
class Program
{
static void Main(string[] args)
{
using (var db = new ProductContext())
{
// Use Find to locate the Food category
var food = db.Categories.Find("FOOD");
if (food == null)
{
food = new Category { CategoryId = "FOOD", Name = "Foods" };
db.Categories.Add(food);
}
// Create a new Food product
Console.Write("Please enter a name for a new food: ");
var productName = Console.ReadLine();
var product = new Product { Name = productName, Category = food };
db.Products.Add(product);
int recordsAffected = db.SaveChanges();
Console.WriteLine(
"Saved {0} entities to the database.",
recordsAffected);
// Query for all Food products using LINQ
var allFoods = from p in db.Products
where p.CategoryId == "FOOD"
orderby p.Name
select p;
Console.WriteLine("All foods in database:");
foreach (var item in allFoods)
{
Console.WriteLine(" - {0}", item.Name);
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}
7. 改变数据库名称
如果你想要改变为你自动生成的数据库的名称,有一个方法是改动你的DbContext继承的构造器,指定新的数据库的名称。
譬如说,我们想要把数据库名称改为“MyProductDatabase”,我们可以给我们的上下文继承类加一个默认构造器,将这个名字传给DbContext:
public class ProductContext : DbContext
{
public ProductContext()
: base("MyProductDatabase")
{ }
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
}
改变数据库的其他方法
还有若干种方法可以指定应该连接哪个数据库,在将来我们会在单独的贴子中做详述。
- App.config 文件中的连接字符串
在App.Config文件中创建与你的上下文同名的连接字符串。
- DbConnection
有一个构造器接受DbConnection。
- 替换默认的约定
基于上下文名称来定位数据库的这个约定是AppDomain范围的设置,你可以通过System.Data.Entity.Database.DbDatabase.DefaultConnectionFactory的静态属性来做改变。
8. 设置起始化策略
在下一节里,我们讲开始改变我们的模型,即意味着数据库定义也需要改变。目前还没有开箱即可用(‘out of the box’)的方案可以原地演变你现有的数据库定义。数据库演变是我们正研究的东西,最近一篇设计博客贴子提供了我们目前的趋向的一个例子。
但是,有机会在上下文第一次在一个AppDomain里运行时,运行一些定制的代码来初始化数据库。这在你想要插入初始数据用于做测试运行时会非常方便,在模型改变后重新创建数据库时也会非常有用。在CTP5版本中,我们包含了几个策略,你可以插接使用,但你也可以编写定制的策略。
在Program.cs的顶部,加一个 using System.Data.Entity.Database 语句:
using System.Data.Entity.Database;
在本示范中,我们只想要在模型改变后,删除和重建数据库,所以在Program类的Main方法的顶部,我加了下述代码:
DbDatabase.SetInitializer<ProductContext>( new DropCreateDatabaseIfModelChanges<ProductContext>());
9. 数据注释(Data Annotations)
到目前为止,我们仅是让EF通过默认的约定去发现模型,但有时候当我们的类并不遵循约定时,我们需要能够做进一步的配置。对此,有2个选项,在本节中,我们将看一下数据注释,Code First的流畅API将在另外一篇贴子中讨论。
让我们在模型中加一个Supplier类:
public class Supplier
{
public string SupplierCode { get; set; }
public string Name { get; set; }
}
我们还需在上下文继承类中加一个集合:
public class ProductContext : DbContext
{
public ProductContext()
: base("MyProductDatabase")
{ }
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Supplier> Suppliers { get; set; }
}
至此,如果我们运行应用的话,我们会得到一个 InvalidOperationException 异常,说是“ EntityType ‘Supplier’ has no key defined. Define the key for this EntityType.” (实体类型 ‘Supplier’ 没有定义主键,请为此实体类定义主键),因为EF无法知道SupplierCode应该是Supplier的主键。
我们将使用数据注释,先需要加一个引用:
- 项目 -> 添加引用…
- 选择“.NET” 页
- 从列表中选择“System.ComponentModel.DataAnnotations”
- 点击 “OK
在Program.cs的顶部加一个using语句:
using System.ComponentModel.DataAnnotations;
现在,我们可以给SupplierCode属性加一个注释,表示它是主键:
public class Supplier
{
[Key]
public string SupplierCode { get; set; }
public string Name { get; set; }
}
CTP5版支持的注释的完整列表如下:
- KeyAttribute
- StringLengthAttribute
- MaxLengthAttribute
- ConcurrencyCheckAttribute
- RequiredAttribute
- TimestampAttribute
- ComplexTypeAttribute
- ColumnAttribute
置于一个属性上,指定其字段名,序数和数据类型
- TableAttribute
置于一个类上,指定其数据表名和定义
- InversePropertyAttribute
置于一个导航属性上,指定代表关系另一端的属性
- ForeignKeyAttribute
置于一个导航属性上,指定代表关系外键的属性
- DatabaseGeneratedAttribute
置于一个属性上,指定数据库该如何为该属性生成值(Identity, Computed 或者 None)
- NotMappedAttribute
置于一个属性或类上,将其排除在数据库外
10. 验证
在CTP5版本中,我们引进了一个新的特性,在尝试保存到数据库前,会验证实例数据是否满足数据注释。
让我们加一个注释,指定Supplier.Name的长度必须在5到20个字符之间:
public class Supplier
{
[Key]
public string SupplierCode { get; set; }
[MinLength(5)]
[MaxLength(20)]
public string Name { get; set; }
}
在Program.cs的顶部加一个using语句:
using System.Data.Entity.Validation;
让我们改变Main方法,插入一些不合法的数据,捕捉住异常,显示任何错误的信息:
class Program
{
static void Main(string[] args)
{
DbDatabase.SetInitializer<ProductContext>( new DropCreateDatabaseIfModelChanges<ProductContext>());
using (var db = new ProductContext())
{
var supplier = new Supplier { Name = "123" };
db.Suppliers.Add(supplier);
try
{
db.SaveChanges();
}
catch (DbEntityValidationException ex)
{
foreach (var failure in ex.EntityValidationErrors)
{
Console.WriteLine( "{0} failed validation", failure.Entry.Entity.GetType());
foreach (var error in failure.ValidationErrors)
{
Console.WriteLine( "- {0} : {1}", error.PropertyName, error.ErrorMessage);
}
}
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}
如果运行应用,我们将看到显示了下列信息:
CodeFirstSample.Supplier failed validation
- Name : The field Name must be a string or array type with a minimum length of ’5′.
Press any key to exit.(CodeFirstSample.Supplier验证失败,Name : 字段Name必须是个字符串或数组类型,最少长度为5,点击任何一键退出)
结语
在本示范中,我们看了如何使用EF特性CTP5版做代码优先(Code First)开发,我们看了如何定义和配置模型,保存和获取数据,配置数据库连接,随着模型的演变更新数据库定义,在写入数据库前做数据验证。
流畅API
除了数据注释外,Code First还呈示了一个用于做配置的流畅API,相关内容在另一个博客贴子里讨论。
反馈和支持
一如以往,我们非常希望你在这个博客贴子上做评论,给我们提供任何关于代码优先(Code First)的反馈。
想得到支持的话,请使用实体框架预览产品论坛。
Rowan Miller
Program Manager
ADO.NET实体框架开发团队