第7章
高级概念
The Code First modeling functionality that you have seen so far should be enough to get you up and running with most applications. However, Code First also includes some more advanced functionality that you may require as your needs advance. Throughout this book you’ve seen Code First’s conventions in action, but if there are one or more conventions you don’t like, Code First allows you to remove them. You may also want to get rid of that EdmMetadata table Code First is adding to your database. Code First caches its model by default, and it’s possible to override that behavior to solve problems like targeting multiple database providers in the same application in stance. This chapter will cover these topics and more.
到目前为止你已经看到Code First的建模功能应足以让你和运行大多数应用。然而,Code First还包括一些更高级的功能,您可能有需要。在这本书中,您已经看到Code First的默认行为,但如果有一个或多个你不喜欢的约定,Code First允许你删除它们。您可能还希望得到摆脱EdmMetadata表添加到您的数据库。默认情况下Code First需要缓存模型,您可以覆写这种行为来解决类似在一个应用程序中指向多个数据库的这种情况。本章将涵盖更多的这些主题。
Mapping to Nontable Database Objects
映射到非表数据库对象
So far you have used Code First to map to tables, whether you are generating a database or mapping to tables in an existing database. But databases support many other types of objects, including stored procedures and views.
As of Entity Framework 4.2, Code First only has built-in support for tables, meaning that it is only capable of generating schemas that contain tables. Therefore, if you are using Code First to generate your database, you are restricted to tables.
However, if you are mapping to an existing database, you may have views, stored procedures, and other objects in the database you are mapping to. Let’s take a look at how we can interact with those.
您已经学习了使用Code First映射到表,无论是生成一个数据库还是映射到一个现有的数据库中的表,都是针对有表的数据库进行。但数据库支持许多其他类型的对象,包括存储过程和视图。
对EF框架4.2而言,Code First只内置表支持,这意味着它只能生产生包含表的构架。因此,如果您
使用Code First生成数据库,就被限制在使用表工作上面。
不过,如果你是映射到一个现有的数据库,可能有视图,存储过程,并映射到数据库中的其他对象。让我们看看如何与这些对象进行交互。
You have the option of manually editing the database schema after Code First has created it. If you do manually edit the database to include nontable objects, you can apply the same techniques discussed in this section.
The Entity Framework team has indicated that they plan to add support for mapping to other database objects in future releases.
小贴士:你可以选择在Code First生成数据库构架后手工编辑数据库来包含非表对象。在本节中你就可以使用这样的技术。
EF框架开发团队声称他们已经计划在未来的版本技持非表数据库对象的映射。
Mapping to Updatable Views
映射到可更新的视图
In some cases you may want to simply map an entity to a view rather than a table. For example, you may be mapping to a database that has a very large and confusing schema.To simplify things, the database might contain a view that exposes the data for your entity with more comprehensible column names. If the view is updatable, you can use the Entity Framework to insert, update, and delete data as well as selecting it. Fortunately, most databases, including SQL Server, use the same SQL syntax for interacting with views as they do for tables. This means you can simply “lie” to Code First and tell it that the view is a table. You do this by using the same configuration you use for naming tables.
在某些情况下,你可能想简单地将实体映射到视图,而不是表。例如,您可能需要映射到一个数据库,它有一个非常大的和混乱的构架。为了简单,数据库可能包含一个视图,使用相比实体更容易理解的列名暴露数据。如果视图是可更新的,您可以使用EF框架来插入,更新和删除数据以及选择数据。幸运的是,大多数数据库,包括SQL Server,使用相同的SQL语法与视图交互,就好像与表格进行交互一样。这意味着你可以简单地“欺骗”Code First,并告诉它那个视图就是一个表。您可以通过使用命名表中相同的配置的方法来控制它。
小贴士:不懂可更新视图?请到MSDN中的 SQL Server’s “CREATE VIEW (Transact-SQL)”去看看,会有所帮助。
For example, perhaps you want the Destination data to come from an updateable view called my_destination_view rather than a table. You can use the Table annotation to specify the view name:
例如,假设有一个Destination数据是来自于一个可更新的视图,名为my_destination_view。你可以使用Table标记为其指定视图名称:
[Table("my_detination_view")]
public class Destination
Alternatively, you can use the ToTable method from the Fluent API to map to the view:
你也可能使用Fluent API的ToTable方法来映射到视图:
modelBuilder.Entity<Destination>().ToTable("my_detination_view");
Using Views to Populate Objects
使用视图填充对象
Not all scenarios call for mapping an entity directly to an updateable view. You may find yourself wanting to leave a class mapped to a table but to have the ability to use a view to retrieve a set of those classes in a particular scenario. For example, let’s assume that you want to leave Destination mapped to the Destinations table, but in one area of your application you want to load all the destinations from the TopTenDestinations view. You can use the SqlQuery method on DbSet to load entities based on some SQL that you write:
并非所有的情况下都可调用实体直接映射到一个可更新的视图。在某些特殊场景里,如果一个类已经映射到一个表,而需要视图能够来调用一组这样的类。举个例子,假设你已经让Destination表映射到了Destinations表,而在您的应用程序的一个区域里需要从TopTenDestination视图中加载所有的destinations。您就可以使用DbSet的SqlQuery方法来加载你写的一些以SQL为基础的实体:
var destinations = context.Destinations
.SqlQuery("SELECT * FROM dbo.TopTenDestinations");
In the above code we are using a SQL statement that bypasses Entity Framework to get back the desired Destination objects. The good thing is that once those objects are retrieved from the database, they are treated exactly the same as objects that were loaded any other way. This means you still get change tracking, lazy loading, and other DbContext features for the Destination objects that were loaded.
在上面的代码中,我们使用一个SQL语句,绕过实体框架取回所需的目标对象。好消息是,一旦这些对象从数据库中检索,它们被视为与其他方式加载的对象相同。这意味着你仍然能为加载的目标对象获得变化跟踪,延迟加载,和其他DbContext功能。
The SqlQuery method relies on an exact match between the column names in the result set of the query you wrote and the names of the properties in your object. Because the Destination class contains DestinationId, Name, and other properties, the view must return columns with these same names. If the view does not have the same column names as the properties on your class, you will need to alias the columns in your select statement.
For example, let’s say that your TopTenDestinations view uses Id instead of DestinationId for the primary key name. In SQL Server, you can use the AS word to alias the Id column from the view as the DestinationId column that Entity Framework
is expecting, as you can see in Example 7-1.
SqlQuery函数的方法依赖于在查询结果集的列名和对象属性名的精确匹配。由于目标类包含DestinationId,Name和其他属性,视图必须返回与其相同名称的列。如果视图没有与类属性相同的列名,需要在SELECT语句中的为列设置别名。
例如,TopTenDestinations视图使用Id而不是DestinationId作为主键的名称。在SQL Server中,可以使用AS关键字为Id列指定DestinationId列的别名,满足@EF的要求,代码7-1对此进行了解释:
Example 7-1. Querying a database view from a DbSet
var destinations = context.Destinations
.SqlQuery(@"SELECT
Id AS DestinationId,
Name,
Country,
Description,
Photo
FROM dbo.TopTenDestinations");
Note that the column-to-property name matching does not take any mapping into
account. For example, if you had mapped the DestinationId property to a column
called Id in the Destinations table, the SqlQuery method would not use this mapping. The SqlQuery method always attempts the column-to-property matching based on property name. Therefore, the column in the result set would still need to be called DestinationId.
请注意,列与属性的名称匹配并未考虑任何到帐户的映射。例如,如果你映射DestinationId属性到Desintaions表中的Id列,SqlQuery方法不会使用这种映射。 sqlquery函数的方法总是尝试其于属性名来匹配列-属性关系。因此,仍然需要在结果集中将此列称为DestinationId。
Using Views to Populate Nonmodel Objects
使用视图来填充非模型对象
The two techniques we have looked at so far allow you to use a view to populate a set of objects that are part of your model. Once these objects are created, they are tracked by the context and any changes will be written back to the database. You may find yourself wanting to get the results of a view back into a read-only set of objects. The results of the view may combine data from multiple tables and therefore can’t be mapped directly to an entity that is part of your model.
For example, you may have a view called DestinationSummaryView that combines data from the Destinations and Lodgings tables. This view may have DestinationId, Name, LodgingCount, and ResortCount columns. These columns don’t match any of the entities in the BAGA model, but it would be great to be able to get the results back into a purpose-built object that you can then use in your application.
我们已经了解了两个技术,都可以使用视图来填充一个对象,成为模型的一部分。一旦这些对象被创建,将会被上下文跟踪,任何更改都会写回数据库。您可能会发现视图的结果返回的是只读的对象集。视图的结果可能会将多个表中的数据结合起来,因此不能直接映射到一个作为模型一部分的实体中。
例如,你可能有一个视图,命名为DestinationSummaryView,组合了Destinations和Lodgings表的数据。这个视图可能有DestinationId,Name,LodgingCount,和ResortCount列。这些列不匹配任何在BAGA模型内的实体,但它能够将结果返回一个专用的对象,你可以在程序中*使用。
The DestinationSummary class might look something like Example 7-2.
DestinationSummary类类似代码7-2所示:
Example 7-2. DestinationSummary implementation
public class DestinationSummary
{
public int DestinationId { get; set; }
public string Name { get; set; }
public int LodgingCount { get; set; }
public int ResortCount { get; set; }
}
Because the class isn’t part of the BAGA model, you can’t use a DbSet to query for
results. Instead, you use the SqlQuery method on DbContext.Database as follows:
由于此类不是BAGA模型的一部分,不能使用DbSet来查询结果。相反,需要使用SqlQuery方法查询DbContext.Database:
var summary = context.Database.SqlQuery<DestinationSummary>(
"SELECT * FROM dbo.DestinationSummaryView");
In response, Entity Framework will run the SQL that you supplied to access the DestinationSummaryView view. It will then take these results and try to match the column names up with the property names of the DestinationSummary class that you specified in the generic argument of SqlQuery. Because the column and property names match, we will get the results of the query in a collection of DestinationSummary objects.
Because we didn’t go through a DbSet as we did in Example 7-1, the DestinationSummary objects that are created are not tracked by the context. Therefore, if you change any of the properties, Entity Framework will not pay any attention to those changes any time SaveChanges is called.
作为回应,EF框架将运行由您提供的SQL访问DestinationSummaryView视图。然后将这些结果尝试使用DestinationSummary类的属性名匹配您在SqlQuery方法的范型参数指定的列名。因为列名和属性名相匹配,我们可以从DestinationSummary对象的集合中得到的查询结果。
在例7-1中,我们并没有通过DbSet创建DestinationSummary对象,因此不会被上下文跟踪到。因此,如果你改变任何属性,EF框架将不会关注任何在调用SaveChanges时发生的任何变化。
Working with Stored Procedures
使用存储过程
Code First does not have any support for mapping Insert, Update, and Delete statements for your classes directly to stored procedures, as you are able to do in the designer.
Code First并不支持直接通过存储过程来映射插入,更新和删除命令,但是你可以在设计器中使用这些命令。
小贴士:EF框架开发团队已经接到了大量的用户要求支持此功能,声明在后续版本中可能会加入支持。
Using the same techniques you just saw for working with views, you can also use stored procedures to fetch results from the database. For example, let’s say you have a Get TopTenDestinations stored procedure that takes a single parameter to specify in which country to look for destinations. You can use the SqlQuery method on DbSet to execute this procedure:
使用你刚刚在视图中同样的技术,也可使用存储过程从数据库中生成结果。例如,有一个名为TopTenDestinations 的存储过程通过一个单一的参数来指定在哪个国家内查找目的地。你可以使用DbSet的SqlQuery方法来生成这个过程:
var country = "Australia";
var destinations = context.Destinations
.SqlQuery("dbo.GetTopTenDestinations @p0", country);
Notice that SqlQuery accepts parameters. See the sidebar “SqlQuery Parameters to
Prevent SQL Injection” on page 157 for more information.
注意到SqlQuery接受一个参数,见后面“使用SqlQuery参数避免SQL注入”获取更多信息。
As you saw above with views, you can also use the DbContext.Database.SqlQuery method to get back results from stored procedures that don’t match an entity in your model. Let’s assume you have a GetDestinationSummary stored procedure and you want to get the results in a collection of the DestinationSummary class you saw back in Example 7-2. Let’s also say this stored procedure takes two parameters—one for the country and the other for some keywords:
与视图类似,你可以使用DbContext.Database.SqlQuery方法获得存储过程返回的数据,这些存储过程并不与模型中的实体相匹配。假设有一个GetDestinationSummary存储过程,想要获取的结果是DestinationSummary类的集合(代码7-2)。现在给存储过程提供两个参数,一个为country,另一个为某个关键词:
var country = "Australia";
var keyWords = "Beach, Sun";
var destinations = context.Database.SqlQuery<DestinationSummary>(
"dbo.GetDestinationSummary @p0, @p1", country, keyWords);
In the above code, you can see that we’re using index-based naming for parameters. As noted in the sidebar, Entity Framework will wrap these parameters up as DbParameter objects for you to avoid any SQL injection issues. The column names in the result returned by the stored procedure will be matched with the property names on DestinationSummary. Because DestinationSummary isn’t part of the BAGA model, the results are not tracked and any changes will not be pushed back to the database.
在上述代码中,我们为参数使用了索引名。@EF将会封装这些参数为DbParameter对象以避免任何SQL注入问题。由存储过程返回的列名将会匹配DestinationSummary的属性名。由于DestinationSummary并不是BAGA模型的一部分,结果不会被跟踪,任何更改也不会返回到数据库中。
SqlQuery Parameters to Prevent SQL Injection
The SqlQuery method allows you to specify parameters. Entity Framework will take
care of wrapping these into DbParameter objects to help prevent against SQL injection attacks. You use a @p prefix for parameters followed by an integer index. Entity Framework will then match these indexes up with the list of parameters you provide after the query string. As with the view-based example you saw earlier, the results of the query are tracked by the context and behave the same as results of any other query.
小贴士:使用SqlQuery参数防止SQL注入
SqlQuery方法允许指定参数。EF框架将封装参数为DbParameter对象以防止SQL注入式攻击。使用对参数@p前缀后附一个整数索引号来表达。EF框架将以这些索引号匹配在查询字符串中提供的参数值。正如你在前面看到的基于视图的案例,查询结果可以被上下文所跟踪,像其他查询结果一样使用。
Removing Conventions
删除默认规则
In previous chapters you have seen that Code First includes a set of conventions that help build your model. You’ve seen how you can supplement or override what the conventions do using Data Annotations or the Fluent API. One other option you have is to switch off one or more of the default conventions.
Each Code First convention is implemented as a class in the System.Data.Entity.ModelConfiguration.Conventions namespace. Code First currently only allows you to remove one or more of the included conventions.
从前面的章节中可以看到,Code First包含了一系列的约定帮助你创建模型。你也看到如何设置或覆写默认规则,可以使用Data Annoations,也可使用@FA。还有一个选项可以关闭一个或多个默认约定。
每个Code First的默认约定都被配置为一个类,这个类位于System.Data.Entity.ModelConfiguration.Conventions 名称空间。Code First目前只允许移除一个多个包含的默认规则。
The ability to write your own conventions was included in a preview of Code First. However, the Entity Framework team removed this functionality because they felt that they didn’t have time to polish the design and get to the appropriate quality level without holding up the muchawaited release of Code First. It’s likely this feature will become available again in a future release.
小贴士:Code First的预览版本支持编写自己的“约定”代码。但是,EF框架团队在正式版取消了此功能,因为他们认为他们没有时间去完善并不是太过令信期待的功能。很可能在未来的版本会再次提供此功能。
A full list of the Code First conventions that can be removed and a description of what each convention does is available at http://msdn.microsoft.com/en-us/library/
gg696316(v=VS.103).aspx. The complete list of conventions is also shown in
Figure 7-1
有关类列表取自微软网站,http://msdn.microsoft.com/zh-cn/library/gg696316(v=VS.103).aspx,目前尚无中文版本。
While you can remove any of the conventions listed in Figure 7-1, we’ll use just one
OneToManyCascadeDelete—to demonstrate how to go about this process. This convention adds a cascade delete rule to all required relationships.
我们可以移除上述列表中的任何一个默认规则。这里我们只尝试对OneToManyCascadeDelete进行移除作为展示。这个默认规则对所有必须的关系创建级联删除。
While you could just override the cascade behavior for every required relationship, if you have a lot of relationships, it may make more sense just to disable the convention altogether.
Switching off conventions is done in the OnModelCreating method on your context via the DbModelBuilder.Conventions.Remove method. Add the following line of code to OnModelCreating in your BreakAwayContext class:
当然可以覆写每个必须关系的级联行为。但是如有很多关系,可能需要同时禁用默认规则。
关闭默认规则在context的OnModelCreating方法内进行,通过调用DbModelBuilder.Conventions.Remove方法就可以实现。在BreakAwayContext类中的OnmodelCrating方法加入如下代码:
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
The model contains a required relationship between Lodging and Destination. Up until now, Code First has been automatically adding a cascade delete rule to this relationship.With the new code in place, run the application so that the database gets recreated and this cascade delete will be removed from this relationship in the model and in the database (Figure 7-2). It will also disappear from any other required relationships that may exist.
在模型中有一个必须的关系:Lodging与Destination.当前,Code First自动为这个关系添加了一个级联删除。使用新代码后,运行程序,级联删除就从模型和数据库中被移除了(见图7-2)。任何其他必须关系的级联删除都被去除了。
After switching off the conventions, you may decide that you want to reintroduce cas
cade delete behavior on some of the relationships. You can do this using the Fluent API as described back in Chapter 4.
In the BAGA model, it makes sense for us to have cascade delete enabled on required relationships, so go ahead and re-enable the OneToManyCascadeDeleteConvention convention by removing the modelBuilder.Conventions.Remove call we just added.
关闭默认约定后,你可能还想在许多关系中重新引入级联删除行为。你可以使用Fluent API来进行设置。(见第4章)
在BAGA模型中,需要在必须的关系中具有级联删除功能,请删除上述代码,恢复
OneToManyCascadeDeleteConvention默认规则。
Taking Control of Model Caching
控制模型缓存
Throughout this book you have seen how Code First takes care of a lot of things for you, but that you can take control of them and change the behavior when needed. Model caching is no exception; in fact, you likely had no idea that Code First was caching a model for you up to this point. After scanning your classes and applying conventions and configuration, Code First keeps an in-memory version of your model around so that it can be reused in the application instance. This is the reason that the OnModelCreating method is only hit once for each DbContext in an application instance. In this section, you will learn more about what model caching is, when you might need to override the conventions, and how you go about doing that.
在这本书中,你已经看到Code First管理了很多东西,但是,你可以在需要控制它,改变它的行为。模型缓存也不例外,事实上,你可能没想到,Code First为你进行了模型的缓存。在扫描类并使用默认约定与配置后,Code First就会使模型的一个版本保持在内存中,以便可以被应用程序的实例重用。这是因为应用程序实例的每一个Dbcontext只调用一次OnModelCreating方法。在本节中,您将学习模型缓存是什么,如果需要重写约定,应如何去做。
Understanding Model Caching
理解模型缓存
In earlier chapters, you have seen that Code First will automatically discover and build a model based on the DbSet properties that you expose on your context. The model creation process involves taking that set of entity types, running the Code First conventions on them, and then applying any additional configuration that you specified via Data Annotations or the Fluent API. This process isn’t cheap on resources and can take some time, especially if your model is large and/or complex. To avoid incurring this cost every time you create an instance of your context, Code First runs this process once and then caches the final model for your context type. Model caching occurs at the AppDomain level.
You can see model caching in action by monitoring when the OnModelCreating method is called on your context. Add a line to the OnModelCreating method that will write to the console whenever it is called:
在前面的章节中,你已经看到,Code First会自动发现和建立一个模型,这个模型基于你在context中暴露的DbSet属性而创建。模型创建过程中涉及到提取实体类型设置,配置Code First约定以及启用任何由@DA或@FA实施的附加配置。这个过程很耗费资源,可能会需要一些时间,尤其是在模型很大和/或复杂的时候。为了避免每次创建上下文的一个实例所需的开销过大,Code First每运行一次这个过程,就会缓存上下文类型的最后一个模型。模型缓存发生在AppDomain级别。
通过在context中调用OnModelCreating方法可以看到模型缓存行为,在OnModelCreating方法中添加一行如下的代码,只要被调用就会写入控制台:
Console.WriteLine("OnModelCreating called!");
Modify the Main method to call the InsertDestination method a number of times
(Example 7-3). You added the InsertDestination method itself back in Chapter 2.
修改Main方法调用InsertDestination方法一定次数(代码7-3)。InsertDestination方法见第2章。
Example 7-3. Main updated to use the context multiple times
static void Main()
{
Database.SetInitializer(
new DropCreateDatabaseIfModelChanges<BreakAwayContext>());
InsertDestination();
InsertDestination();
InsertDestination();
}
After running the application again, you will see that although the code constructs and uses three separate instances of BreakAwayContext, the OnModelCreating method is only called once. This is because Code First only calls OnModelCreating while creating the model for the first context instance; after that, the final model is cached and is reused for the following uses of BreakAwayContext.
运行程序,你会发现尽管代码构造和使用了三个BreakAwayContext实例,OnModelCrating方法只被调用了一次。这是因为Code First只有当第一次context实例创建模型时和进行调用;在那以后,最后一个模型就被加入缓存,在后面的BreakAwayContext需要时被重用。
Overriding Default Model Caching
覆写默认的模型缓存
There aren’t many situations where you need to take control of model caching. Provided that the model for a given context type stays the same for every instance of that context with an AppDomain, the default behavior is going to work as expected. Using the default behavior is also going to give you the best performance, because model creation will only occur one time.
There are some situations where the model for a given context type may vary between instances in the same AppDomain. One example would be using a multitenant database. A multitenant database involves having the same model replicated multiple times in the same physical database. For example, you may have a model that is used to store blog posts and a website that displays them. Your website might contain a personal and a work blog that both use the same model. In the database you could have the tables used to store the data for this model replicated in two separate schemas. The tables for your work blog may live in the work schema (work.Posts, work.Comments, etc.) and the tables for your personal blog might live in the personal schema (personal.Posts, personal.Comments, etc.). Each of these sets of tables is known as a tenant. Database schemas are just one way to distinguish between tenants; there are many other patterns, such as table prefixes.
If your application needs to access multiple tenants from the same AppDomain, the mapping between classes and tables is going to be different depending on what tenant you are targeting. Different mapping means a different model, which in turn means the default model caching won’t work for you.
有许多情况需要控制模式缓存。只要AppDomain中的每一个实例中上下文类型的模型保持相同,默认行为就会按预期方式工作。使用默认行为,可以获得最好的性能,因为模型的创建将只发生一次。
有某些情况下,给定的上下文类型的模型,在同一AppDomain中实例之间可能会有所不同。一个例子是使用多租赁数据库。多租赁数据库会涉及到在同一物理数据库具有相同的模式的实例会复制多闪。例如,可能有一个模型用于存存储blog文章,同时也用于在网站上予以显示。您的网站可能包含个人和工作博客,都使用相同的模型。在数据库中你可能将这一模型复制存付两个单独构架中的表中。工作博客使用的表可能位于work构架中(如work.Posts,work.Comments等),而个人博客可能位于personal构架中(如personal.Posts,personal.Comments等)。这些表被称为租客。数据库模式只是一种区分租户的方法;还有许多其他的模式,如表前缀,。
如果您的应用程序需要访问相同的AppDomain多个租户,类和表之间的映射将是不同的,取决于你的目标租户。不同的映射意味着不同的模型,这反过来又意味着默认模型缓存不会为你工作。
Another example would be using the same context to target the same model on different database providers in the same AppDomain. Different database providers means different data types for the columns in the database, which in turn means a different model. Let’s take a look at this scenario and how to handle model caching.
Add the TargetMultipleProviders method shown in Example 7-4. This method uses
the same context to access a SQL Server and SQL Server Compact Edition database.
另一个例子是同一AppDomain内,使用相同的上下文指向不同数据库引擎的相同模型。不同的数据库引擎意味着数据库列中有不同的数据类型,而这又意味着不同的模型。让我们看看在这种情况下,如何处理模型缓存。
添加例7-4中所示的TargetMultipleProviders方法。此方法使用
同样的上下文访问SQL Server和SQL Server Compact Edition数据库。
You will need the SQL Server Compact Edition runtime installed to complete this section. If you have completed Chapter 6, you have already installed the runtime. If not, see “Installing SQL Server Compact Edition” on page 135. You may also remember that in Chapter 6 we had to change our model to target SQL Compact. If you want to test out this code, you will need to make the same change again here. Back in Chapter 3, we configured Trip.Identifier to be a database-generated key.
Identifier is a GUID property, and SQL Server had no problem generating values for us. SQL Compact, however, isn’t able to generate values for GUID columns. If you want to run the application, remove either the Data Annotation or Fluent API call that configures Trip.Identifier as database-generated.
小贴士:
完成本节需要安装SQL ServerCompact Edition运行时。如果您在第6章,已经安装了运行时,请继续。如果没有,请参阅前述的“安装SQL Server Compact Edition”。您可能还记得,在第6章中,我们不得不改变我们的模型,指向SQL Compact Edition。如果你想测试一下这个代码,你需要在这里再次作出同样的变化。早在第3章,我们配置数据库生成的键Trip.Identifier标识符是一个GUID属性,使用在SQL Server上没有任何问题。 SQL Compact不能产生的GUID列的值。的如果你想运行应用程序,删除数据库生成Trip.Identifier的Data Annoations或Fluent API配置代码。
Example 7-4. Reusing a context to target multiple providers
static void Main(string[] args)
{
Database.SetInitializer(new
DropCreateDatabaseIfModelChanges<BreakAwayContext>());
TargetMultipleProviders();
}
private static void TargetMultipleProviders()
{
var sqlString = @"Server=.\SQLEXPRESS;
Database=DataAccess.BreakAwayContext;
Trusted_Connection=true";
using (var connection = new SqlConnection(sqlString))
{
using (var context = new BreakAwayContext(connection))
{
context.Destinations.Add(new Destination { Name = "Hawaii" });
context.SaveChanges();
}
}
var sqlCeString =
@"Data Source=|AppData|\DataAccess.BreakAwayContext.sdf";
using (var connection = new SqlCeConnection(sqlCeString))
{
using (var context = new BreakAwayContext(connection))
{
context.Destinations.Add(new Destination { Name = "Hawaii" });
context.SaveChanges();
}
}
}
Run the application. You will get an exception when trying to use the context instance that targets SQL Server Compact Edition. This will be a NotSupportedException stating that, “Using the same DbCompiledModel to create contexts against different types of database servers is not supported. Instead, create a separate DbCompiledModel for each type of server being used.”
To use the same context type with different models in the same AppDomain, you need to externally build a DbCompiledModel for each model and then use these to construct the different context instances. DbContext exposes a set of constructors that allow you to supply the model to be used, along with connection information. Add a constructor to the BreakAwayContext class that allows a DbCompiledModel and a DbConnection to be supplied:
运行程序,在试图使用上下文实例指向SQL Server Compact Edition时会得到一个异常。这是一个NotSupportedException,言明:“不支持在不同类型的数据服务中使用同一DbCompiledModel创建contexts.可以为每种服务器创建一个单独的context类型”。
为了在同一AppDomain中使用同一context指向不同的模型,你需要为每个模型构建一个DbCompiledModel,然后使用这个模型构建不同的上下文实例。DbContext暴露了一系构造器允许你支持模型这样使用。添加一个构造器到BreakAwayContext类中,允许支持DbCompiledModel 和DbConnection:
public BreakAwayContext(DbConnection connection,
DbCompiledModel model)
: base(connection, model, contextOwnsConnection: false)
{ }
The code in Example 7-5 shows an updated TargetMultipleProviders method that demonstrates how this constructor can now be used to target different database providers, using a different model for each.
代码7-5显示了一个更新的TargetMultipleProviders方法展示如何利用这个构造器来指向不同的数据库引擎,为各自使用不同的模型:
Example 7-5. Code updated to work with multiple providers
private static void TargetMultipleProviders()
{
var sql_model = GetBuilder().Build(
new DbProviderInfo("System.Data.SqlClient", "2008"))
.Compile();
var ce_model = GetBuilder().Build(
new DbProviderInfo("System.Data.SqlServerCe.4.0", "4.0"))
.Compile(); var sql_cstr = @"Server=.\SQLEXPRESS;
Database=DataAccess.BreakAwayContext;
Trusted_Connection=true"; using (var connection = new SqlConnection(sql_cstr))
{
using (var context =
new BreakAwayContext(connection, sql_model))
{
context.Destinations.Add(new Destination { Name = "Hawaii" });
context.SaveChanges();
}
} var ce_cstr =
@"Data Source=|DataDirectory|\DataAccess.BreakAwayContext.sdf";
using (var connection = new SqlCeConnection(ce_cstr))
{
using (var context = new BreakAwayContext(connection, ce_model))
{
context.Database.Initialize(force: true);
context.Destinations.Add(new Destination { Name = "Hawaii" });
context.SaveChanges();
}
}
}
private static DbModelBuilder GetBuilder()
{
var builder = new DbModelBuilder();
builder.Entity<EdmMetadata>().ToTable("EdmMetadata");
builder.Entity<Activity>();
builder.Entity<Destination>();
builder.Entity<Hostel>();
builder.Entity<InternetSpecial>();
builder.Entity<Lodging>();
builder.Entity<Person>();
builder.Entity<PersonPhoto>();
builder.Entity<Reservation>();
builder.Entity<Resort>();
builder.Entity<Trip>();
builder.ComplexType<Address>();
builder.ComplexType<Measurement>();
builder.ComplexType<PersonalInfo>();
return builder;
}
Let’s walk through what the code in the TargetMultipleProviders method is doing. The GetBuilder method is responsible for creating a DbModelBuilder and registering all your classes with the builder. The code in the example registers each class using the DbModelBuilder.Entity and DbModelBuilder.ComplexType methods. This approach will work if you have been using Data Annotations to configure your classes. If you have been using the Fluent API, you should copy the code from your OnModelCreating method to replace this code. Note that you also need to include the EdmMetadata class and map it to the EdmMetadata table; this allows Code First to detect when the model and database go out of sync. When DbContext is responsible for building the model, it will take care of adding this class for you.
我们来看看TargetMultipleProviders方法中的代码做了什么。 GetBuilder方法负责创建一个DbModelBuilder,使用builder注册所有的类。示例代码中的每个类都使用DbModelBuilder.Entity和DbModelBuilder.ComplexType方法进行了注册。这种方法将正常工作在已经使用Data Annoations配置过的类上。如果使用Fluent API,应该从OnModelCreating方法复制的代码来替换这部分代码。请注意,还需要包括EdmMetadata类,并将其映射到EdmMetadata表中,这使Code First可以到检测到模型和数据库是否同步。当由DbContext负责创建模型时,会对此类自动维护。
The next step is to build and compile the model for the two providers that are going to be targeted. In the example, the invariant name and manifest token for the database provider are supplied to the Build method. As an alternative, there is another overload of Build that accepts a DbConnection to get the provider information from.
下一步要为两个需要指向的数据库引擎建立和编译模型。在这个例子中,不变的名称和数据库引擎令牌都提供给Build方法。作为一种替代方法,Builder的另一个重载,接受一个DbConnection获得数据库引擎信息。
With the compiled models created, they can now be used to access the two different databases. Remember that database initialization only occurs once per AppDomain, so only the first database to be used will be initialized automatically. The call to Database.Initialize on the context targeting the second database ensures that the second database is also initialized.
In the end, the new Destination is added to two different databases using the same set of classes and configurations to define duplicate models. Now that we’re done using SQL Compact, go ahead and re-enable the configuration to make Trip.Identifier database-generated.
在编译过的模型创建后,他们现在可以用来访问两个不同的数据库。请记住,数据库初始化只在每个AppDomain发生一次,所以只有第一个要使用的数据库将自动初始化。调用Database.Initialize是针对第二个数据库的,确保第二个数的也得到初始化。
最后,新的Destination被添加到两个不同的数据库中,使用的是相同的类和相同的配置设置定义了重复的模型。现在,我们使用过SQL Compact了,重新启用以往配置,以使Trip.Identifier键由数据库生成。
Remember that building and compiling the model are expensive operations. The resulting compiled model should be cached and reused for all context instances that target the same model.
小贴士:记住构建和编译模型是高开销的操作。结果编译模型应被缓存并且在同一方面的所有context实例中被重用。
Working with the EdmMetadata Table
使用EdmMetadata表
Back in Chapter 2, you learned that, by default, Code First adds an EdmMetadata table to your database. There are some advantages in allowing Code First to have this table in the database, but you also have the option of removing it. In this section, you will see how to remove the EdmMetadata table from your database. You’ll also learn about the implications of removing it.
The EdmMetadata table serves a single purpose, and that is to store a snapshot of the model that was used to create the database. Having the snapshot allows Code First to check whether the current model matches the current database or not. The snapshot is stored by taking a SHA256 hash of the database portion of the model. You can see in Figure 7-3 that the EdmMetadata table always contains a single row with the hash stored in it.
早在第2章中,您就学习到,默认情况下,Code First会将EdmMetadata表添加到您的数据库。允许Code First在数据库中创建这个表有一定的好处,但您也可以选择删除它。在本节中,你会看到如何从数据库中删除EdmMetadata表。您还可以了解删除它的影响。
EdmMetadata表的存在只有一个目的,那就是存储被用来创建数据库的模型快照。快照允许Code First检查当前模型是否匹配当前数据库。快照以model数据库部分的SHA256哈希形式来存储。你可以看到在图7-3 EdmMetadata表总是包含一个存储有哈希值的单列。
Coding Against EdmMetadata
使用EdmMetadata编码
Code First uses the EdmMetadata table in the included database initializers, but you can also interact with it programmatically using the EdmMetadata class in the EntityFramework API. Modify the Main method to call a new UseEdmMetadataTable method, shown in Example 7-6, to experiment with this class:
Code First在包含的数据库初始化器中使用EdmMetadata表,但是你也可以使用编程方式与其交互,通过在@EFAPI中EdmMetadata类来实现。修改Main方法,调用一个新的UseEdmMetadataTable方法,如代码7-6所示:
Example 7-6. The UseEdmMetadata method
Example 7-6. The UseEdmMetadata method
static void Main()
{
Database.SetInitializer(
new DropCreateDatabaseIfModelChanges<BreakAwayContext>());
UseEdmMetadataTable();
}
private static void UseEdmMetadataTable()
{
using (var context = new BreakAwayContext())
{
var modelHash = EdmMetadata.TryGetModelHash(context);
Console.WriteLine("Current Model Hash: {0}", modelHash);
var databaseHash =
context.Set<EdmMetadata>().Single().ModelHash;
Console.WriteLine("Current Database Hash: {0}", databaseHash);
var compatible =
context.Database.CompatibleWithModel(throwIfNoMetadata: true);
Console.WriteLine("Model Compatible With Database?: {0}",
compatible);
}
}
This code starts by using the static EdmMetadata.TryGetModelHash method to find the hash for the current model. This method will always work for Code First models, but if you attempt to use it with a model created using the designer, it will return null. The EdmMetadata class is included as part of your model, so you can use your DbContext to interact with it.
The second section of code creates a DbSet for the EdmMetadata class and then asks for the single row of data so that it can read the hash value from it. Finally, there is a DbContext.Database.bvgt65 method that makes it simple to check if the model and database match. This is the method that the database initializers included in the Entity Framework make use of. Specifying true for the throwIfNoMetadata parameter will cause an exception to be thrown if the EdmMetadata table has been excluded from the database. Specifying false will cause the method to return false if the table is excluded. You can run the code and see that everything currently matches.
此代码开始使用静态的EdmMetadata.TryGetModelHash方法,找到当前模型的哈希值。此方法会始终工作在Code First模型中,但如果您尝试与设计器创建的模型进行工作,将返回null。 EdmMetadata类作为你的模型的一部分,所以你可以用你的DbContext与它交互。
代码的第二部分创建了为EdmMetadata类创建了DbSet,然后索取一行数据以便可以读取哈希值。最后,还有一个DbContext.Database.bvgt65方法,它简单地检查模型和数据库是否匹配。这是包含在EF框架内部的数据库初始化器的方法。如果EdmMetadata表从数据库中已排除,为throwIfNoMetadata参数指定true将导致抛出一个异常。如果排除表,指定false将导致该方法返回false。您可以运行代码并看到一切当前的匹配。
Using Code First with ObjectContext
在Code First中使用ObjectContext
Up until now, you have seen Code First being used with the DbContext API, which is the recommended API surface for working with Code First. DbContext was introduced in Entity Framework 4.1 as a lighter-weight and more productive wrapper over the existing Entity Framework components. The alternative to DbContext is the ObjectContext API, and while it is recommended to use DbContext with Code First, it is still possible to use ObjectContext. In this chapter, you will see how to build a Code First model and use it to construct an ObjectContext.
至此,Code First都在使用DbContext API进行工作,这是Code First的推荐API。DbContext是一个轻型工具,在EF框架4.1中引入,封装了现有的其他EF框架组件,更具生产力。可以使用ObjectContext代替DbContext。本章,你会看到如何使用ObjcetContext来创建Code First模型。
DbContext or ObjectContext?
DbContext is simply a wrapper over ObjectContext and associated classes. If you need some of the more advanced features that are only available from ObjectContext, you can cast DbContext to the IObjectContextAdapter interface to access the underlying ObjectContext. This approach allows you to access the functionality from ObjectCon text while still being able to write most of your code against the newer DbContext. You might consider using Code First with ObjectContext if you have existing applications that are based on ObjectContext and you are swapping from Model First or Database First to Code First.
小贴士:DbContext还是ObjectContext?
DbContext封装自ObjectContext,并对相关类进行了简化。如果你需要一些只有ObjectContext才能提供的高级功能 ,可以强制DbContext实现 IObjectContextAdapter接口访问底层的ObjectContext。这种方法允许您访问ObjectCon文本的功能,同时仍然能够对新的DbContext写你的代码。您可能会考虑使用ObjectContext的代码,首先,如果您有现有的,是基于ObjectContext和你交换首先从型号的第一或数据库代码的应用。
Similar to using a DbContext-based context, you start by creating a derived context, except this time it derives from ObjectContext and exposes ObjectSet properties instead of DbSet properties. Notice in Example 7-8 that when using an ObjectContext, you need to write a bit more code than with the DbContext. You must expose a constructor that accepts an EntityConnection. The ObjectSet properties also need to be initialized using the CreateObjectSet method; this is something DbContext takes care of for you.
类似于基于DbContext的context,你可以开始于创建一个派生的context,只不过这一次我们是派生自ObjectContext,暴露ObjectSet的属性而不是DbSet的属性。在代码7-8就使用了ObjectContext,相比DbContext需要多写一点代码。必须暴露一个构造器接受一个EntityConnection.ObjcetSet也CreaeObjectSet方法进行初始化;这些DbContext都替您完成了。
Add this new BreakAwayObjectContext class to your DataAccess project.
添加一个新的BreakAwayObjectContext类到DataAccess项目。
Example 7-8. Implementing ObjectContext
using System.Data.EntityClient;
using System.Data.Objects;
using Model;
namespace DataAccess
{
public class BreakAwayObjectContext : ObjectContext
{
public BreakAwayObjectContext(EntityConnection connection)
: base(connection)
{
this.Destinations = this.CreateObjectSet<Destination>();
this.Lodgings = this.CreateObjectSet<Lodging>();
this.Trips = this.CreateObjectSet<Trip>();
this.People = this.CreateObjectSet<Person>();
this.PersonPhotos = this.CreateObjectSet<PersonPhoto>();
}
public ObjectSet<Destination> Destinations { get; private set; }
public ObjectSet<Lodging> Lodgings { get; private set; }
public ObjectSet<Trip> Trips { get; private set; }
public ObjectSet<Person> People { get; private set; }
public ObjectSet<PersonPhoto> PersonPhotos { get; private set; }
}
}
At this point, DbContext would take care of scanning the DbSet properties and building a model based on them. But ObjectContext has no built-in support for Code First. Code First provides a method to bridge this gap—DbModelBuilder.UseObjectContext. In the following walkthrough, you’ll learn how to leverage this to create an ObjectContext from a Code First model.
DbContext能够自动扫描DbSet属性并创建一个基于这些属性的模型。但是ObjectContext没有为Code First内建支持。Code First提供了一个方法来填补这个鸿沟,这个方法就是DbModelBuilder.UseObjectContext. 下面,我们来看如何使用这个方法在Code First模型中创建ObjectContext.
Modify the Main method to make use of a new UseObjectContext method, as shown in Example 7-9.
修改Main方法使用一个新的UseObjectContext方法,如代码7-9所示:
Example 7-9. Code updated to use BreakAwayObjectContext
static void Main()
{
UseObjectContext();
}
private static void UseObjectContext()
{
var builder = GetBuilder();
var cstr = @"Server=.\SQLEXPRESS;
Database=BreakAwayObjectContext;
Trusted_Connection=true";
using (var connection = new SqlConnection(cstr))
{
var model = builder.Build(connection).Compile();
using (var context =
model.CreateObjectContext<BreakAwayObjectContext>(connection))
{
if (!context.DatabaseExists())
{
context.CreateDatabase();
}
context.Destinations.AddObject(
new Destination { Name = "Ayers Rock" });
context.SaveChanges();
}
}
You start by creating a DbModelBuilder, using the GetBuilder method we added earlier in this chapter. You then use the model builder to create a model and compile it. Note that you must supply the connection or provider information when building the model, as the provider affects the data types, etc. in the resulting model. With the model compiled, you can then use the CreateObjectContext method to construct the ObjectContext. This method relies on the constructor you exposed that accepts a single Entity Connection parameter. ObjectContext doesn’t support database initializers, so you also need to write code to check if the database exists and create it if it does not. Note that ObjectContext does not support EdmMetadata either, so there is no way to detect if the model and database are compatible.
以往我们都是先创建DbModelBuilder,使用GetBuilder方法添加上下文。然后使用模型构建器创建模型并进行编译。注意在创建模型时必须提供数据库连接或引擎信息,因为数据库引擎类型影响最终结果数据的类型。使用编译过的模型,也可使用CreateObjectContext方法来构建ObjectContext。这个方法依赖于你暴露的构造器,这个构造器接受一个单一的实体连接参数。ObjectContext不支持数据库初始化器,因此必须编写代码检查数据库是否存在并在不存在的时候去创建它。注意ObjectContext不支持EdmMetadata,因此无法检测模型和数据库是否兼容。
Summary
小结
In this chapter, you saw a variety of advanced features that Code First provides. These features are provided to make sure that you can override what Code First does by default in situations where the default behavior just doesn’t work for your scenario. Most applications won’t require you to use these features, but it’s good to have an understanding of what is available in case you ever need them.
We’ve now covered Code First from top to bottom. We started with a high-level overview of what Code First is. You then learned how Code First builds the model and how you can customize the model by using configuration. You saw how to influence which database Code First connects to and how that database is initialized. And finally in this chapter, we wrapped things up with some advanced topics.
The final chapter of this book will help prepare you for what’s coming in future releases of Code First.
本章提供了Code First提供的一些高级特征,这些特性确保在默认行为不能满足您工作场景需要时能够覆写Code First的默认设置。大多数应用程序者不需要你使用这些特征,但是理解这些特征可以作为不时之需。
到目前为止我们已经全面介绍了Code First。我们开始于一个高级别的概览。然后学习了如何使用Code First构建模型,如何使用配置定制模型。还学习了如何影响Code First连接哪一个数据库,如何让数据库初始化。最后在本章中,我们讨论了一些高级话题。本书的最后一章将帮您为未来的新版本发布作些准备。