Chapter7 Advanced Concepts 第7章 高級概念 The Code First modeling functionality that you have seen so far should be enough to get you up and running with mo ...
Chapter7
Advanced Concepts
第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。這個方法依賴於你暴露的構造器,這個構造器接受一個單一的實體連接參