Chapter6
Controlling Database Location,Creation Process, and Seed Data
第6章
控制数据库位置,创建过程和种子数据
In previous chapters you have seen how convention and configuration can be used to affect the model and the resulting database schema. In this chapter you will see how the convention and configuration concept applies to the database that is used by Code First.
You’ll learn how Code First conventions select a database and how you can alter this convention or specify the exact database that your context should use. The topics we cover will help you target other database providers, deploy your application, and perform many other database-related tasks.
You’ll also discover how database initializers can be used to control the database creation process and insert seed data into the database. This can be particularly useful when writing automated scenario tests.
前面我们已经提到默认规则和配置可以用于影响模型和数据库构架。本章你会看到如何使用Code First来控制数据库。
你将会学到Code First默认配置如何选择数据库,也会掌握如何修改默认规则或指定上下文使用真正的数据库。我们覆盖的主题将包括指向其他数据库引擎,部署应用程序,执行数据库有关的任何等。
你也可以学习到数据库初始化器可以用于控制数据库生成过程,添加种子数据到数据库中等。这在进行自动测试的场景时特别有用。
Controlling the Database Location
控制数据库位置
So far you have relied on the Code First convention to select which database the application targets. By default, Code First has created the database on localhostSQLEXPRESS using the fully qualified name of your context class for the database name (i.e.,the namespace plus the class name). There will be times when this won’t be appropriate and you need to override the convention and tell Code First which database to connect to. You can modify or replace the convention used to select a database using Code First connection factories. Alternatively, you can just tell Code First exactly which database to use for a particular context, using the DbContext constructors or your application configuration file.
到目前为止我们都是引用了Code First的默认规则来选择应用程序的数据库目标。默认情况,Code First会使用localhostSQLEXPRESS作为目标数据库引擎,并使用context类的全名作为数据库名(即命名空间+类名)。如果不符合要求你需要覆写默认规则然后告知Code First想要连接到哪个数据库。你可以Code First的连接工厂来选择数据库,修改或替换默认规则。可选择地,你也可以使用DbContext构造器或应用程序配置文件来告知Code First对某个特定的上下文使用指定的数据库。
Code First database creation and initialization works with SQL Azure in the same way that it works with any local database. You can see this in action in “Tutorial: Developing a Windows Azure Data Application Using Code First and SQL Azure”. Vendors have begun modifying their database providers to support Code First as well. Be sure to check for this support before trying to use Code First with one of the third-party providers.
小贴士:Code First可以在SQL Azure上使用与任何本地数据库相同的方法进行创建和初始化工作。你可在http://www.windowsazure.com/zh-cn/develop/overview/找到相关的文章。目前数据库供应商正在努力修改他们的产品已提供对Code First的支持。在使用Code First与第三方数据库引擎工作前一定要检查是否支持。
Controlling Database Location with a Configuration File
使用配置文件控制数据库位置
The easiest and most definitive way to control the database that your context connects to is via a configuration file. Using the configuration file allows you to bypass all database location–related conventions and specify the exact database you want to use. This approach is particularly useful if you want to change the connection string of your context to point to a production database as you deploy your application.
By default, the connection string that you add to your configuration file should have the same name as your context. The name of the connection string can be either just the type name or the fully qualified type name. In “Controlling Connection String Name with DbContext Constructor” on page 132, you will see how to use a connection string with a name that does not match your context name. Add an App.config file to your BreakAwayConsole application with a BreakAwayContext connection string, as shown in Example 6-1.
控制数据库连接最简单也最可靠的方法是使用配置文件。配置文件可以帮你绕过所有与数据库位置相关的约定,并能指定到你想使用的确切数据库。这种方法是非常有用的,如果你想改变你的上下文的连接字符串指向一个生产数据库,为您部署应用程序。
默认情况下,您添加到您的配置文件的连接字符串应该与context有相同的名称。连接字符串的名称可以是类型名称或完全限定的类型名称。在后面“使用DbContext构造器控制连接字符串名称”,你会看到如何让连接字符串的名称不匹配上下文的名称。添加一个App.config文件到BreakAwayConsole应用程序,内中包含BreakAwayContext的连接字符串,如例6-1所示。
Example 6-1. Connection string specified in App.config
<?xml version="1.0"?> <configuration> <connectionStrings> <add name="BreakAwayContext" providerName="System.Data.SqlClient" connectionString="Server=.SQLEXPRESS; Database=BreakAwayConfigFile; Trusted_Connection=true" /> </connectionStrings> </configuration>
For those familiar with creating connection strings when your application uses an EDMX file, notice that this is not an EntityConnection String but simply a database connection string. With Code First, you have no need to reference metadata files or the System.Data.Entity Client namespace.、
小贴士:对熟悉使用EDMX文件创建连接字符串的人,请注意这不是一个EntityConnection String 而是一个简单的数据库连接字符串。使用Code First,你不需要引用元数据文件或System.Data.Entity Client 名称空间。
Modify the Main method so that it calls the InsertDestination method, as shown in Example 6-2.
修改Main方法,调用InsertDestination方法,如代码6-2所示。
Example 6-2. Main method modified to call InsertDestination
static void Main() { InsertDestination(); }
Run the application, and you will notice that a BreakAwayConfigFile database has been created in your local SQL Express instance (Figure 6-1).
运行应用程序,你会发现,BreakAwayConfigFile数据库实例已在您的本地SQL Express上创建了(图6-1)。
Code First matched the BreakAwayContext name of your context with the BreakAwayContext connection string in the configuration file. Because an entry was found in the configuration file, the convention for locating a database was not used. The connection string entry could also have been named DataAccess.BreakAwayContext, which is the fully qualified name of the context.
Code First使用配置文件中的BreakAwayContext连接字符串匹配名为BreakAwayContext的上下文。因为在配置文件中发现了一个条目,就不再使用约定来定位数据库。连接字符串项,也已经被命名为DataAccess.BreakAwayContext,这是上下文的全名。
Controlling Database Name with DbContext Constructor
使用DbContext的构造器控制数据库名称
You’ve seen how to set the connection string that your context will use via the configuration file; now let’s look at some ways to control the database connection from code. So far you have just used the default constructor on DbContext, but there are also a number of other constructors available. Most of these are for more advanced scenarios, which will be covered later in this book, but there are two constructors that allow you to affect the database being connected to.
你已经看到如何通过配置文件中的连接字符串设置上下文,现在让我们来看看使用代码来控制数据库连接的方法。到目前为止,您只使用过DbContext的默认构造函数,还有一些其他可用的构造函数可供使用。其中大部分是更高级的方法,这将在这本书中进行介绍,有两个构造函数允许你影响连接到的数据库。
If you added a connection string to your configuration file, as shown in the previous section, be sure to remove it before starting this section. Remember that the configuration file overrides everything, including the features you will see in this section.
小贴士:如果您添加了一个连接字符串到您的配置文件,如在上一节所示,开始本节之前,一定要去掉它。记住,配置文件压倒一切,包括在本节的功能。
DbContext includes a constructor that takes a single string parameter to control the database name. If you use this constructor, the value you supply will be used in place of the fully qualified context name. Add a constructor to BreakAwayContext that accepts a string value for the database name and passes it to the base constructor (Example 6-3). Notice that you are also adding a default constructor to ensure that all the existing code from previous chapters continues to work.
DbContext有一个构造函数使用一个字符串参数来控制数据库的名称。如果您使用此构造器,您提供的值将被用来代替context的全名。添加到BreakAwayContext构造函数接受一个数据库名称的字符串值,并把它传递给基构造器(例6-3)。请注意,您也必须要加入默认的构造器,以确保所有现有的代码从前面的章节继续工作。
Example 6-3. Database name constructor added to context
public BreakAwayContext() { } public BreakAwayContext(string databaseName) : base(databaseName) { }
Modify the Main method to call a new SpecifyDatabaseName method (Example 6-4).
修改Main方法调用 SpecifyDatabaseName方法 (Example 6-4).
Example 6-4. SpecifyDatabaseName method added to application
static void Main() { SpecifyDatabaseName(); } private static void SpecifyDatabaseName() { using (var context = new BreakAwayContext("BreakAwayStringConstructor")) { context.Destinations.Add(new Destination { Name = "Tasmania" }); context.SaveChanges(); } }
This new method uses the constructor you just added to specify a database name. This name is used instead of the fully qualified name of your context. Run the application and you will see that a database named BreakAwayStringConstructor has been created in your local SQL Express instance.
新方法使用的构造器,就是你刚刚添加的,用于指定数据库名称。使用的此名称,就不是上下文的全名了。运行应用程序,你会看到一个名为BreakAwayStringConstructor数据库已在您的本地SQL Express实例中创建。
Controlling Connection String Name with DbContext Constructor
使用DbContext构造器控制连接字符串
Earlier in this chapter, you saw that you are able to specify a database to use in the configuration file by adding a connection string with the same name as your context. If you use the DbContext constructor that accepts a database name, Entity ramework
will look for a connection string whose name matches the database name. In other words, with the default constructor, Entity Framework will look for a connection string named BreakAwayContext, but with the constructor used in Example 6-4, it will expect a connection string named BreakAwayStringConstructor.
You can also force the context to get its connection string from the configuration file by supplying name=[connection string name] to this constructor. This way, you don’t need to rely on name matching, since you are explicitly providing a connection string name. If no connection string is found with the specified name, an exception is thrown.
Example 6-5 shows how you can modify the default constructor of breakAwayContext to ensure that the connection string is always loaded from the configuration file.
在本章的前面,你看到,你可以通过在配置文件中加入与你的上下文的名称相同的连接字符串指定一个数据库。如果您使用的DbContext构造函数接受一个数据库名,EF框架就会寻找一个与连接字符串的名称相匹配的数据库的名称。换句话说,默认的构造函数,实体框架会寻找名为BreakAwayContext的连接字符串,而使用与示例6-4中使用的构造函数,它会期望一个名为BreakAwayStringConstructor的连接字符串。
您还可以强制上下文从配置文件中所提供的name= [connection string name]获取连接字符串。这样,你就不需要依靠名称匹配,因为你明确地提供了一个连接字符串。如果没有找到有具体指定名称的连接字符串,就会抛出一个异常。
例6-5显示了如何修改breakAwayContext默认构造器,以确保连接字符串始终是从配置文件加载。
Example 6-5. Constructor defining which connection string should be loaded from App.config
public BreakAwayContext() :base("name=BreakAwayContext") { }
Reusing Database Connections
重用数据库连接
DbContext has another constructor that allows you to supply a DbConnection instance.This can be useful if you have other application logic that works with a DbConnection or if you want to reuse the same connection across multiple contexts. To see this in action, add another constructor to BreakAwayContext that accepts a DbConnection and then passes the DbConnection to the base constructor, as shown in Example 6-6. You’ll also notice that you need to specify a value for the contextOwnsConnection. This argument controls whether or not the context should take ownership of the connection. If set to true, the connection will get disposed along with the context. If set to false,your code will need to take care of disposing the connection.
DbContext另一个构造器,允许您提供一个DbConnection的实例。这可能是有用的,如果你有其他的应用程序逻辑与DbConnection相关,或者如果你想重用在多个环境下的同一个连接。要看到这种行为,添加另一个构造器BreakAwayContext接受一个DbConnection,然后通过DbConnection基构造器传递值,如例6-6所示。你还会发现,你需要指定一个contextOwnsConnection的值。此参数控制context是否拥有连接的所有权。如果设置为true,连接将会随上下文一起被释放。如果设置为false,您的代码将需要关注连接的释放问题。
小贴士:添加这个构造器你需要引用System.Data.Common名称空间。
Example 6-6. DbConnection constructor added to context
public BreakAwayContext(DbConnection connection) : base(connection, contextOwnsConnection: false) { }
Modify the Main method to call a new ReuseDbConnection method, as shown in
Example 6-7.
修改Main方法调用新的ReuseDbConnection方法,如Example 6-7所示:
小贴士:你需要引用System.Data.SqlClient名称空间,因为此代码使用SqlConnection类型。
Example 6-7. ReuseDbConnection method added to application
static void Main() { ReuseDbConnection(); } private static void ReuseDbConnection() { var cstr = @"Server=.SQLEXPRESS; Database=BreakAwayDbConnectionConstructor; Trusted_Connection=true"; using (var connection = new SqlConnection(cstr)) { using (var context = new BreakAwayContext(connection)) { context.Destinations.Add(new Destination { Name = "Hawaii" }); context.SaveChanges(); } using (var context = new BreakAwayContext(connection)) { foreach (var destination in context.Destinations) { Console.WriteLine(destination.Name); } } } }
The ReuseDbConnection constructs a SqlConnection and then reuses it to construct two separate BreakAwayContext instances. In the example, the SqlConnection is just constructed from a connection string that is defined in code. However, Code First isn’t concerned with where you got the connection. You could be getting this connection string from a resource file. You may also be using some existing components that give you an existing DbConnection instance.
ReuseDbConnection构造一个SqlConnection,然后重用它来构造两个单独的BreakAwayContext实例。在这个例子中,在SqlConnection是在代码中定义的连接字符串。然而,Code First并不关心你否获得连接。您可以从资源文件该连接字符串。您也可以使用一些现有的组件,让您获得现有的DbConnection的实例。
Controlling Database Location with Connection Factories
使用连接工厂控制数据库位置
One final option for controlling the database that is used is by swapping out the convention that Code First is using. The convention that Code First uses is available via Database.DefaultConnectionFactory. Connection factories implement the IDbConnectionFactory interface and are responsible for taking the name of a context and creating a DbConnection pointing to the database to be used. Entity Framework includes two connection factory implementations and you can also create your own.
控制所使用的数据库的一个最后的选择是通过更换Code First使用默认约一。Code First使用约定是通过Database.DefaultConnectionFactory来进行。连接工厂实现IDbConnectionFactory接口,并负责上下文的命名,并指明为要使用的数据库创建一个DbConnection。EF框架包含两个连接工厂实现,你也可以创建自己的。
Working with SqlConnectionFactory
使用SqlConnectionFactory
The default connection factory for Code First is SqlConnectionFactory. This connection factory will use the SQL Client (System.Data.SqlClient) database provider to connect to a database. The default behavior will select a database on localhostSQLEXPRESS using the fully qualified name of the context type as the database name. Integrated authentication will be used for authenticating with the database server.
You can override parts of this convention by specifying segments of the connection string that are to be set for any connection it creates. These segments are supplied to the constructor of SqlConnectionFactory using the same syntax used in connection strings. For example, if you wanted to use a different database server, you can specify the Server segment of the connection string:
Code First的默认连接工厂是SqlConnectionFactory。此连接工厂将使用SQL Client(System.Data.SqlClient的)数据库引擎连接到数据库。默认的行为,将选择在localhost SQLEXPRESS创建数据库,并使用上下文类型的完全限定名作为数据库的名称。集成身份验证,将用于与数据库服务器进行身份验证。
你可以通过指定的连接字符串段,来覆写默认规则。这些片段使用SqlConnectionFactory构造函数相同的语法,在连接字符串中使用。例如,如果你想使用不同的数据库服务器,您可以指定服务器段的连接字符串:
Database.DefaultConnectionFactory = new SqlConnectionFactory("Server=MyDatabaseServer");
Alternatively, you may want to use different credentials to connect to the database server:
可选地,你也可使用不同的验证方式来连接数据库服务器:
Database.DefaultConnectionFactory = new SqlConnectionFactory("User=MyUserName;Password=MyPassWord;");
Working with SqlCeConnectionFactory
使用SqlCeConnectionFactory
Entity Framework also includes SqlCeConnectionFactory, which uses SQL Compact Client to connect to SQL Server Compact Edition databases. By default the database file name matches the fully qualified name of the context class and is created in the |ApplicationData| directory.
EF框架还包括SqlCeConnectionFactory,它使用SQL Compact Client 连接到SQL ServerCompact Edition数据库。默认情况下,数据库文件名匹配上下文类的完全限定名,创建在| ApplicationData|目录。(对可执行程序而言,| ApplicationData|位于应用程序所在目录,对web应用程序,伴于网站根目录下的的App_Data子目录内。
Installing SQL Server Compact Edition
Before using SQL Server Compact Edition, you need to install the runtime. The runtime is available as an installer or via NuGet. Install the SqlServerCompact NuGet package to your BreakAwayConsole project. You can install the NuGet package by right-clicking on the References folder in your BreakAwayConsole project and selecting “Add Library Package Reference….” Select “Online” from the left menu and then search for “SqlServerCompact.”
小贴士:安装SQL Server Compact Edition
在使用SQL Server Compact Edition前,需要进行安装。可以通过NuGet来进行安装。安装SqlServerCompact 的NuGet包的到你的BreakAwayConsole项目的方法是:右键单击项目并选择:Add Library Package Reference….,从弹出的对话框中选择Online并查找SqlServerCompact.
Modify the Main method, as shown in Example 6-8, to set the SqlCeConnectionFactory, and then call the InsertDestination method you created back in Chapter 2. The connection factories are included in the System.Data.Entity.Infrastructure namespace, so you will need to add a using for this. Be sure to read the rest of this section before running the code.
修改Main方法,如代码6-8所示,设置SqlCeConnectionFactory,然后调用InsertDestination方法(第2章创建)。连接工厂包含在System.Data.Entity.Infrastructure名称空间,需要添加对其的引用。在运行代码前请读完本节。
Example 6-8. Changing the default connection factory
static void Main() { Database.SetInitializer( new DropCreateDatabaseIfModelChanges<BreakAwayContext>()); Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); InsertDestination(); }
Notice that you need to specify a string that identifies the database provider to use (known as the provider invariant name). This string is chosen by the provider writer to uniquely identify the provider. Most providers keep the same identifier between versions, but SQL Compact uses a different identifier for each version. This is because SQL Compact providers are not backwards-compatible (you can’t use, for example,the 4.0 provider to connect to a 3.5 database). The SqlCeConnectionFactory needs to know what version of the provider to use, so it requires you to supply this string.
If you want to test out this code, you will need to make a small change to your model. 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.
Once you’ve made this change, you can run the application and you will notice that a DataAccess.BreakawayContext.sdf file is created in the output directory of your application (Figure 6-2). Now that you’ve seen SQL Compact in action, go ahead and reenable the configuration to make Trip.Identifier database-generated.
请注意,您需要指定一个字符串,标识数据库引擎(称为provider invariant name)。这个字符串是数据库供应商提供的唯一标识。大多数供应商保持不同版本之间使用相同的标识符,但SQL Compact为每个版本使用不同的标识符。这是因为SQL Compact数据库引擎并不向后兼容的,例如,您不能使用4.0引擎连接到一个3.5数据库。SqlCeConnectionFactory需要知道provider使用的版本,所以它需要你提供这个字符串。
如果你想测试一下这个代码,你需要到你的模型一个小的变化。早在第3章,我们配置Trip.Identifier为数据库生成的key。标识符是一个GUID属性,在SQL Server下没有任何问题。 SQL Compact,不能产生的GUID列的值。如果你想运行该应用程序,删除或注释掉Data Annotations或Fluent API配置的Trip.Identifier(作为数据库生成列)。
一旦你做出了这种变化,你可以运行该应用程序,你会发现一个DataAccess.BreakawayContext.sdf文件是在您的应用程序的输出目录(图6-2)创建。现在,你已经看到SQL默认规则的行为,继续前进,重新启用以前的配置,使Trip.Identifier能够在数据库里生成。
Writing a custom connection factory
写一个定制的连接工厂
So far you have seen the connection factories that are included in Entity Framework, but you can also write your own by implementing the IDbConnectionFactory Interface.The interface is simple and contains a single CreateConnection method that accepts the context name and returns a DbConnection.
In this section, you’ll build a custom connection factory that is very similar to SqlConnectionFactory, except it will just use the context class name, rather than its fully qualified name for the database. You’ll also build this custom factory so that it will remove the word Context if it’s found in the context name.
Start by adding a CustomConnectionFactory class to your DataAccess project (Example 6-9).
到目前为止,您已经看到,连接工厂已经包含EF框架中,但你也可以通过实现IDbConnectionFactory接口来创建自已的连接工厂。
这个接口很简单,包含了一个单一的创建连接的方法,它接受上下文的名称,并返回一个DbConnection。
在本节中,您将构建一个自定义的连接工厂,与SqlConnectionFactory非常相似,但它将只使用上下文类的名称,而不是使用全名作为数据库的名称。您还可以定制这个工厂,当其发现名称中包含Context字符串时将删除它。
加入一个CustomConnectionFactory类到DataAccess项目(代码6-9)。
Example 6-9. Custom connection factory implementation
using System.Data.Common; using System.Data.Entity.Infrastructure; using System.Data.SqlClient; using System.Linq; namespace DataAccess { public class CustomConnectionFactory : IDbConnectionFactory { public DbConnection CreateConnection( string nameOrConnectionString) { var name = nameOrConnectionString .Split('.').Last() .Replace("Context", string.Empty); var builder = new SqlConnectionStringBuilder { DataSource = @".SQLEXPRESS", InitialCatalog = name, IntegratedSecurity = true, MultipleActiveResultSets = true }; return new SqlConnection(builder.ToString()); } } }
The CustomConnectionFactory implementation uses the Split method to take the section of the context name after the final period to use for the database name. It then replaces any instances of the Context word with an empty string. Then it uses SqlConnection StringBuilder to create a connection string that is then used to construct a SqlConnection.
With this method in place, you can modify the Main method to make use of the custom connection factory you just created (Example 6-10). You do so by setting the Custom ConnectionFactory as the DefaultConnectionFactory before other code, which will be using a context.
CustomConnectionFactory使用Split方法取得上下文的名称的最后一段(以“.”划分)作为数据库名称。然后,它将Context字符串替换为空字符串字(如果有的话)。然后,它使用SqlConnection的StringBuilder创建一个连接字符串,将其用于构造一个SqlConnection。
有了这个方法,你可以修改Main方法,使用刚刚创建的自定义连接工厂(例6-10)。这样就DefaultConnectionFactory或其他代码之前,让上下文设置使用自定义的ConnectionFactory。
Example 6-10. Default connection factory set to new custom factory
static void Main() { Database.SetInitializer( new DropCreateDatabaseIfModelChanges<BreakAwayContext>()); Database.DefaultConnectionFactory = new CustomConnectionFactory(); InsertDestination(); }
Run the application and you will see that a new “BreakAway” database is created on the local SQL Express instance (Figure 6-3). The custom factory you just created has removed the namespace from the database name and also stripped the word “Context” from the end.
运行程序你会在SQL Express实例中发现一个新“BreakAway”数据库创建了。定制工厂已经替你将数据库名的名称空间和后缀Context删除。
Working with Database Initialization
数据库初始化
In Chapter 2, you saw that an initializer can be set for a context type using the Database.SetInitializer method. The initializer you set allowed the database to be dropped and recreated whenever the model changed:
在第2章,你已经学习到可以使用Database.SetInitializer方法来为上下文类型设置初始化。设置初始化器可以清除并在模型变化时重建数据库:
Database.SetInitializer( new DropCreateDatabaseIfModelChanges<BreakAwayContext>());
Initialization involves two main steps. First, the model is created in memory using the Code First conventions and configuration discussed in previous chapters. Second, the database that will be used to store data is initialized using the database initializer that has been set. By default, this initialization will use the model that Code First calculated to create a database schema for you. Initialization will occur one time per application instance; in .NET Framework applications, the application instance is also referred to as an AppDomain. Initialization is triggered the first time that the context is used. Initialization occurs lazily, so creating an instance of the context is not enough to cause initialization to happen. An operation that requires the model must be performed, such as querying or adding entities.
The initialization process is thread-safe, so multiple threads in the same AppDomain can use the same context type. DbContext itself is not threadsafe, so a given instance of the context type must only be used in a single thread.
初始化包括两个主要步骤。首先,使用Code First在内存中根据默认规则和配置创建模型。其次,使用已设置的数据库初始化器将用于存储数据的数据库初始化。默认情况下,这个初始化将使用Code First创建一个数据库架构的模型。初始化会发生在每一个.NET Framework应用程序的实例上。应用程序的实例也被称为一个AppDomain。当上下文被使用时,初始化第一次被引发。初始化是延迟加载的,所以创建一个实例的是不完全满足初始化发生的条件的。必须执行对模型的操作,如查询或添加实体才会发生。
初始化过程是线程安全的,所以在同一AppDomain中的多个线程可以使用相同的上下文类型。 DbContext本身不是线程安全的,因此,必须只能在一个单独的线程中使用一个给定的上下文类型实例。
Controlling When Database Initialization Occurs
在数据库初始化产生时进行控制
There are situations where you may want to control when initialization occurs, rather than leaving it to happen automatically the first time your context is used in an application instance. Initialization can be triggered using the DbContext.Database.Initialize method. This method takes a single boolean parameter named force. Supplying false will cause the initialization to occur only if it hasn’t yet been triggered in the current AppDomain. Remember that running the initializer once per AppDomain is the default behavior. Setting force to true will cause the initialization process to run even if it has already occurred in the current AppDomain. Because the context also triggers initialization, this code needs to run prior to the context being used in the AppDomain.
Why would you want to manually trigger database initialization? You may want to
manually trigger initialization so that any errors that occur during model creation and database initialization can be caught and processed in a single place. Another reason to force initialization to occur would be to front-load the cost of creating a large and/or complex model.
Let’s see this in action. Modify the Main method, adding in code to force database initialization to occur, and handle any exceptions that occur as a result of building the model (Example 6-11).
有的情况下,您可能希望控制初始化的发生,而不是让它自动发生在应用程序实例中第一次使用上下文对象时。初始化可以使用DbContext.Database.Initialize方法触发。这个方法接受一个名为force的布尔参数。该参数为false将导致初始化只发生在尚未在当前AppDomain触发的情况。请记住,每个AppDomain运行初始化一次,就会执行默认行为一次。force设置为true时将会使初始化过程运行,即使它已经在当前AppDomain发生。因为上下文也触发初始化,此代码需要运行在上下文被AppDomain使用之前。
为什么你想手动触发数据库初始化?您可能需要通过手动触发初始化,使模型的创建和数据库初始化过程中发生的任何错误可以被捕获,并在一个地方处理。强制初始化发生的另一个原因是为了前端加载大型和/或复杂的模型。
让我们来看看这个行为。修改Main方法,在代码中加入强制数据库初始化的配置,处理模型构建时发生的任何异常(代码6-11)。
Example 6-11. Main method updated to process initialization errors
static void Main() { Database.SetInitializer( new DropCreateDatabaseIfModelChanges<BreakAwayContext>()); using (var context = new BreakAwayContext()) { try { context.Database.Initialize(force: false); } catch (Exception ex) { Console.WriteLine("Initialization Failed..."); Console.WriteLine(ex.Message); } } }
Now we’ll make a change that will cause initialization to fail by asking Code First to map a numeric property to a string column. Doing this will cause the model creation process to fail before Code First even tries to create the database schema.
Modify Activity and add in a Column annotation that specifies a varchar data type to be used for the ActivityId property (Example 6-12).
现在我们做些更改以使用初始化失败。这个错误发生在Code First映射一个数值属性到字符串列中。这样做会使模型创建失败发生在试图创建数据库构架之前。
修改Activity类加入一个Data Annotations的Column特性标记指定AcitivityId属性使用varchar数据类型。(代码6-12)
Example 6-12. ActivityId mapped to an incompatible database type
public class Activity { [Column(TypeName = "varchar")] public int ActivityId { get; set; } [Required, MaxLength(50)] public string Name { get; set; } public List<Trip> Trips { get; set; } }
Run the application and the program will display the exception informing us that the data type that was specified is not valid because of the invalid cast:
运行程序将显示异常信息,表示不能将整形数据映射到varchar类型:
Initialization Failed...
Schema specified is not valid. Errors:
(122,12) : error 2019: Member Mapping specified is not valid. The type 'Edm.Int32[Nullable=False,DefaultValue=]' of member 'ActivityId' in type 'DataAccess.Activity' is not compatible with 'SqlServer.varchar[Nullable=False,DefaultValue=,MaxLength=8000,Unicode=False,FixedLength=False,StoreGeneratedPattern=Identity]' of member 'ActivityId' in type 'CodeFirstDatabaseSchema.Activity'.
(146,10) : error 2019: Member Mapping specified is not valid. The type 'Edm.Int32[Nul-lable=False,DefaultValue=]' of member 'ActivityId' in type 'DataAccess.Activity' is not compatible with 'SqlServer.varchar[Nullable=False,DefaultValue=,MaxLength=8000,Unicode=False,FixedLength=False]' of member 'Activity_ActivityId' in type 'CodeFirstDatabaseSchema.ActivityTrip'.
Remove the annotation you just added to DestinationId and run the application again.This time there will be no error.
移除刚刚添加到DestinationId上的特性标记,再次运行程序。这次就没有问题了。
Switching Off Database Initialization Completely
关闭数据库初始化功能
Of course, not every scenario calls for the database to be automatically initialized, and Entity Framework caters to these situations, too. For example, if you are mapping to an existing database, you probably want Code First to error if it can’t connect to the database, rather than trying to magically create one for you. You can switch off initialization by passing null to Database.SetInitializer:
Database.SetInitializer(null);
When the initializer is set to null, DbContext.Database.Initialize can still be used to force model creation to occur.
当然,并不是所有场景都需要自动调用初始化,EF框架满足所有情况。例如,如果你映射到一个现有的数据库,可能在不能连接到数据库时需要让Code First发生错误而不是魔法般地创建一下。你可以通过传递一个null参数到Database.SetInitializer来关闭初始化功能:
Database.SetInitializer(null); 当初始化器被设置为null后,DbContext.Database.Initialize 仍可用于模型的创建过程.
Database Initializers Included in Entity Framework
将数据库初始化器包含在EF框架
You’ll notice that Database.SetInitializer accepts an instance of IDatabaseInitializer<TContext>. There are three implementations of this interface included in Entity Framework. These implementations are abstract, so you can derive from them and customize the behavior. We’ll walk through creating your own implementation a little later on.
CreateDatabaseIfNotExists
This is the default initializer that is set for all contexts unless Database.SetInitializer is used to specify an alternative initializer. This is the safest initializer, as the database will never be dropped automatically, causing data loss. We saw in Chapter 2 that if the model is changed from when the database was created, an exception is thrown during initialization:
你会发现,Database.SetInitializer接受IDatabaseInitializer<TContext>的一个实例。在EF框架中有三个针对此接口的实现。这些实现是抽象的,所以你可以从其中派生或自定义行为。后面我们将引导您逐步创建自己的实现。
CreateDatabaseIfNotExists
除非Database.SetInitializer指定了替代的初始化器,所有上下文都会被设置给默认初始化器。这是最安全的初始化,数据库将永远不会被自动删除,造成数据丢失。我们看到在第2章,如果模型是从数据库时创建后发生的改变,在初始化期间会抛出异常:
The model backing the “BreakAwayContext” context has changed since the data-
base was created. Either manually delete/update the database, or call Database.SetI
nitializer with an IDatabaseInitializer instance. For example, the DropCreateDa
tabaseIfModelChanges strategy will automatically delete and recreate the database,
and optionally seed it with new data.
Because this is the default initializer, you shouldn’t need to set it, but if you find a need to you can use the following code:
由于默认初始化器的存在,如果需要执行下列代码,你不需要做任何设置:
Database.SetInitializer( new CreateDatabaseIfNotExists<BreakAwayContext>());
DropCreateDatabaseWhenModelChanges
You’ve seen this initializer used throughout the previous chapters to make sure the database always matches the current model. If Code First detects that the model and database do not match, the database will be dropped and recreated so that it matches the current model. This is useful during development, but you obviously wouldn’t want to use this when deploying your application, as it will result in data loss. We’ve already seen the code required to set this initializer:
从前面几章你已经看到要确保数据库总是匹配当前的模型.如果Code First检测到二者不匹配,数据库就人被删除并且重新创建以便可以满足匹配关系.在开发时这很有用,但是显然不在在程序部署中使用,这样数的会丢失.我们已经看到这样的设置初始化的代码:
Database.SetInitializer( new DropCreateDatabaseIfModelChanges<BreakAwayContext>());
DropCreateDatabaseAlways
This initializer will drop and recreate the database regardless of whether the model matches the database or not. At first glance, you may wonder why you would ever want to do that. If you are writing integration tests that exercise your whole application stack, it can be useful to have a way to reset the database to a well-known state before running a test. Modify the Main method as shown in Example 6-13 to
run some code that could represent a test that uses your application to insert a single Destination.
这一初始化器将不管模型与数据库匹配与否都会删除和重建数据库.你可能会疑惑为什么要这么做.如果你集成测试的整个应用程序,就会需要在运行测试前将数据库重置到一个已知的状态.修改Main方法(代码6-13),运行一些代表测试的代码,这会在应用程序中插入一个Destinaion.
Example 6-13. Implementation of a pseudo integration test
static void Main() { Database.SetInitializer( new DropCreateDatabaseAlways<BreakAwayContext>()); RunTest(); } private static void RunTest() { using (var context = new BreakAwayContext()) { context.Destinations.Add(new Destination { Name = "Fiji" }); context.SaveChanges(); } using (var context = new BreakAwayContext()) { if (context.Destinations.Count() == 1) { Console.WriteLine( "Test Passed: 1 destination saved to database"); } else { Console.WriteLine( "Test Failed: {0} destinations saved to database", context.Destinations.Count()); } } }
Because the initializer is set to drop and recreate the database each time, you know that the database will be empty before the test starts. You won’t always want the database to be empty before running integration tests, and we’ll look at seeding data a little later on. Go ahead and run the application, and we will see that the test passes. So far we have just executed a single test, but normally there would be multiple tests required to test an application. Update the Main method so that it runs the same test twice in a row (Example 6-14).
由于初始化器被设置为每次都删除并重建,你会知道在测试开始前数据库是空的.你不用总是去考虑在运行测试前数据库是否为空,后而我们会看到放置一些种子数据在里面的例子.运行程序,我们会看到测试通过.就目前为止我们只执行一个单一的测试,通常一个应用程序里面需要进行多个测试.更新Main方法以便使其可以在一行里运行两次测试(代码6-14):
Example 6-14. Main updated to run the test twice
static void Main(string[] args) { Database.SetInitializer( new DropCreateDatabaseAlways<BreakAwayContext>()); RunTest(); RunTest(); }
Run the application, and you will see that the first execution of the test method will succeed but the second one will fail, stating that there are two destinations in the database. The second test is failing because the data from the first execution is still in the database. This is happening because AppDomain only runs the initializer once by default.
Earlier in this chapter, you learned that you can use Database.Initialize to force initialization to occur, even if has already happened in the current AppDomain. Modify the RunTest method to include a call to Database.Initialize with force set to true to ensure the database is reset before each test (Example 6-15). Run the application again and you will see both tests now pass. The database is getting dropped and recreated in the well-known state before each execution.
运行程序,你会看到第一个方法通过而第二个失败,表明数据库中有两个destinations.第二个测试失败的原因是第一次执行的结果已经在数据库中了.进一步的原因是AppDomaing默认情况每次程序运行只执行一次初始化.
本章前面介绍可以使用Database.Initialize强制初始化,不管当前的AppDomain是否已经初始化过.修改RunTest方法包含一个调用Database.Initialize强制初始化的方法确保每次测试前都会重置数据库(代码6-15),再次运行程序你会发现测方式现在通过了在每次测试执行前.数据库先删除又以已知的状态进行重建.
Example 6-15. RunTest updated to force initialization
static void RunTest() { using (var context = new BreakAwayContext()) { context.Database.Initialize(force: true); context.Destinations.Add(new Destination { Name = "Fiji" }); context.SaveChanges(); } using (var context = new BreakAwayContext()) { if (context.Destinations.Count() == 1) { Console.WriteLine( "Test Passed: 1 destination saved to database"); } else { Console.WriteLine( "Test Failed: {0} destinations saved to database", context.Destinations.Count()); } } }
Dropping and recreating the database is an easy way to start each test with a well-known state, but it can be expensive if you are running a lot of integration tests. Consider using System.Transactions.TransactionScope as a way to avoid changes being permanently saved to the database during each test.
删除和重建数据库是将数据库状态保持在一个已知状态的很容易的方法,但是如果运行一系集成的测试,系统开销过大.考虑使用System.Transactions.TransactionScope作为避免在每一次测试时永久存储对数据库的修改.
Creating a Custom Database Initializer
创建一个定制的数据库初始化器
So far, you have used the initializers that are included in the Entity Framework API. There may be times when the initialization logic that you want doesn’t align with any of the included initializers. Fortunately Database.SetInitializer accepts the IDatabaseInitializer interface, which you can implement to provide your own logic.
到目前为止,我们一直在使用EF框架中包含的初始化器.有时不想按照已有的初始化器的逻辑进行工作.Database.SetInitializer 接受IDatabaseInitializer 接口,你可以通过实现这个接口来定制逻辑.
As well as writing your own custom initializers, you can also find initializers that other people have created. One example of this is available in the EFCodeFirst.CreateTablesOnly NuGet package. This initializer will allow you to drop and create the tables in an existing database, rather than dropping and creating the actual database itself. This is particularly useful if you are targeting a hosted database where you don’t have permission to drop or create the entire database.
小贴士:除了自已写定制的初始化器,也可以引用别人创建的.有一个例子EFCodeFirst.CreateTablesOnly NuGet 包.这个初始化器允许你在已经存在的数据库进行删除和创建操作,而不是删除和创建数据库实体本身.当你将数据库指向一个宿主数据库而又没有权限删除和创建整个数据库时特别有用.
There could be any number of reasons you want to implement your own initializer. We are going to look at a simple scenario where the developer will be prompted before the database is dropped and recreated. The Database property exposes a variety of methods to interact with the database such as checking to see if it exists, creating it, or dropping it. The three initializers that are included in the API contain logic that leverages these methods. You can combine the methods in logic in your own class. That’s what you'll do in this next example. Add the PromptForDropCreateDatabaseWhenModelChages class to your DataAccess project (Example 6-16).
你想实现自己的初始化器的原因可能有很多。我们来看一个简单的场景:在数据库删除并重新创建之前给开发者一个提示。Database属性暴露了各种方法与数据库进行交互,可以实现检查是否存在,是否创建,或是否删除等功能。API中包含的初始化器包含的逻辑利用了这些方法。在你自己的类,你也可以将这些方法整合在逻辑里。这就是下面这个例子要做的。添加PromptForDropCreateDatabaseWhenModelChages类到您的DataAccess项目(例6-16)。
Example 6-16. Custom initializer
using System; using System.Data.Entity; namespace DataAccess { public class PromptForDropCreateDatabaseWhenModelChages<TContext> : IDatabaseInitializer<TContext> where TContext : DbContext { public void InitializeDatabase(TContext context) { // If the database exists and matches the model // there is nothing to do var exists = context.Database.Exists(); if (exists && context.Database.CompatibleWithModel(true)) { return; } // If the database exists and doesn't match the model // then prompt for input if (exists) { Console.WriteLine ("Existing database doesn't match the model!"); Console.Write ("Do you want to drop and create the database? (Y/N): "); var res = Console.ReadKey(); Console.WriteLine(); if (!String.Equals( "Y", res.KeyChar.ToString(), StringComparison.OrdinalIgnoreCase)) { return; } context.Database.Delete(); } // Database either didn't exist or it didn't match // the model and the user chose to delete it context.Database.Create(); } } }
The PromptForDropCreateDatabaseWhenModelChages class implements a single InitializeDatabase method. First, it checks if the database exists and matches the current model. If it does, there is nothing else to be done and the initializer returns. If the database exists but doesn’t match the current model, you will be prompted to see if you want to drop and create the database. If you decide not to recreate the database, the initializer returns and Entity Framework will attempt to run against the existing database schema. If you do decide to recreate the database, the existing database is dropped. The final line of code simply creates the database and is only reached if the database didn’t exist or if we chose to recreate the database.
The custom initializer now needs to be registered with the Entity Framework; modify the Main method to take care of this (Example 6-17). You’ll notice that we’re also updating Main so that it calls the InsertDestination method that we wrote back in Chapter 2.
PromptForDropCreateDatabaseWhenModelChages类实现单一的InitializeDatabase方法。首先,它检查数据库是否存在以及是否与当前的模型相匹配。如果是这样,什么也不做,初始化器返回。如果该数据库存在,但不匹配当前的模型,会提示你是否想删除和创建数据库。如果您决定不重新创建数据库,初始化器返顺,EF框架将尝试按现有的数据库模式再次运行。如果您决定重新创建数据库,现有的数据库将被删除。最后一行代码简单地创建数据库中,只会在数据库不存在,或者我们选择重新创建数据库才会得到执行。
自定义的初始器在需要在EF框架内注册;修改Main方法(例6-17)。你会注意到Main方法调用了我们在第2章更新的InsertDestination方法:
Example 6-17. Custom initializer registered in Main
static void Main() { Database.SetInitializer(new PromptForDropCreateDatabaseWhenModelChages<BreakAwayContext>()); InsertDestination(); }
Let’s go ahead and change the model so that it no longer matches the database. Modify the Destination class by adding a MaxLength annotation to the Name property:
我们对模型作些修改使其不再与数据库匹配.修改Destinaton类中的Name属性,在其上附加一个Data Annotations标记:MaxLength.
[MaxLength(200)] public string Name { get; set; }
Now run the application, and you will be prompted, asking if you want to drop and Create the database. Answer no (N) to tell our custom initializer to leave the database alone this time. You’ll notice that the application still completes successfully. This is because the changes you made don’t prevent Entity Framework from being able to use the current model to access the out-of-date database schema. Entity Framework expects that Destination names should be 200 characters or less. Since the database didn’t change, it doesn’t enforce max length, so it’s happy with the insert statement that Entity Framework is sending to the database.
Now let’s make a change that will affect the insert statement. Modify the Destination class to include a new TravelWarnings property:
现在运行的应用程序,将提示您,询问您是否要删除并创建数据库。答否(N),告诉我们的自定义初始化器不理会数据库。你会注意到,应用程序仍然成功完成。这是因为您所做的更改不会阻止EF框架使用当前模型访问过时的数据库架构。EF框架预期Destination Names应为200个字符或更少。由于数据库没有改变,它没有强制执行的最大长度,所以EF框架给它发送的INSERT语句可以执行。
现在,让我们做出改变,会影响INSERT语句。修改目标类,包括一个新的TravelWarnings属性:
public string TravelWarnings { get; set; }
Run the application again. As before, you’ll be prompted, asking if you want to drop and create the database. Select not to recreate the database again, and this time you will get a DbUpdateException. You’ll need to drill through the inner exceptions to find the actual cause of the error (Figure 6-4).
The inner exception of the top-level exception is an UpdateException, and the inner exception of that is a SqlException. The SqlException finally has the message that explains what happened: “Invalid column name 'TravelWarnings'.” The problem is that Entity Framework is trying to execute the SQL shown in Example 6-18, but the TravelWarnings column doesn’t exist in the database.
再次运行应用程序。像以前一样,你会被提示,询问您是否要删除并创建数据库。选择不创建数据库,这个时候你会得到一个DbUpdateException。你需要通过内部异常链去找到错误的真正原因(图6-4)。
顶层异常的内部异常是UpdateException,该内部异常是一个SQLException。最后的SQLException的消息,说明发生了什么:“无效的列名称”TravelWarnings,“问题发生的原因是EF框架试图执行示例6-18中所示的SQL语句,但TravelWarnings列在数据库中不存在。
Example 6-18. Invalid SQL being executed
insert [dbo].[Destinations]([Name], [Country], [Description], [TravelWarnings], [Photo]) values (@0, @1, @2, null, null) select [DestinationId] from [dbo].[Destinations] where @@ROWCOUNT > 0 and [DestinationId] = scope_identity()
Run the application again, but this time select to drop and recreate the database when prompted. The application will now execute successfully.
再次运行程序,这次选择删除并重建数据库,程序成功执行.
Setting Database Initializers from a Configuration File
在配置文件中设置数据库初始化器
Setting initializers in code is an easy way to get started while developing, but when it’s time to deploy your application, you probably want to have an easier way to set them without modifying code. It’s highly unlikely you want to deploy your application with the DropCreateDatabaseIfModelChanges initializer set in production! Add an appSettings section to the config file of your BreakAwayConsole project that includes the initializer setting shown in Example 6-19.
在代码中设置的初始化是一种简单的方法,但部署用程序时,您可能希望有一个更简单的方式设置,而无需修改代码。想要应用程序部署设置DropCreateDatabaseIfModelChanges的初始化器,这是极不可能的!将appSettings节添加到BreakAwayConsole项目的配置文件中,其中包括了初始化器的设置,见示例6-19中所示。
Example 6-19. Initializer set in configuration file
<?xml version="1.0"?> <configuration> <appSettings> <add key="DatabaseInitializerForType DataAccess.BreakAwayContext, DataAccess" value="System.Data.Entity.DropCreateDatabaseIfModelChanges`1 [[DataAccess.BreakAwayContext, DataAccess]], EntityFramework" /> </appSettings> </configuration>
小贴士:代码示例有一行断裂的代码,在实际的App.config文件中应该删除.value值必须在同一行才能工作.
There is a lot going on in the line of configuration, so let’s break down how it is structured. The key section always starts off with DatabaseInitializerForType followed by a space, then the assembly qualified name of the context that the initializer is being set for. In our case that is DataAccess.BreakAwayContext, DataAccess, which simply means the DataAccess.BreakAwayContext type that is defined in the DataAccess assembly. The value section is the assembly qualified name of the database initializer to be used. It looks complex because we are using a generic type; we are setting DropCreateDatabaseIfModelChanges<BreakAwayContext> defined in the EntityFramework assembly.
Also modify the Main method so that it no longer sets an initializer in code:
还应有很多配置行,我们打破配置结构来分别研究。关键部分开始于DatabaseInitializerForType,后跟一个空格,然后配置正确的上下文名称以便初始化器能够为其设置。在我们的例子就是DataAccess.BreakAwayContext,DataAccess,仅仅意味着DataAccess.BreakAwayContext类型的定义是在DataAccess程序集。Value部分是配置数据库初始化器要使用名称。它看起来复杂,因为我们使用泛型类型,我们使用了EF框架程序集中定义的DropCreateDatabaseIfModelChanges<BreakAwayContext> 方法来进行设置。
还需要修改Main方法,以便它不再设置在代码中的初始化:
static void Main() { InsertDestination(); }
Now make a change to the model so that you can test that the entry in our configuration file is being used. Modify the Destination class to include a new ClimateInfo property:
现在对模型作些改变以便测试配置文件是否得到应用.修改Destination类包含一个新的ClimateInfor属性:
public string ClimateInfo { get; set; }
Run the application, and you will see that the database gets dropped and recreated with the new ClimateInfo column.
运行程序,你会看到数据库被删除重建,新增ClimateInfo列,
Now if you want to deploy your application, you may want to change the initializer to CreateDatabaseIfNotExists so that you never incur automatic data loss. You may also be working with a DBA who is going to create the database for you. If the database is being created outside of the Code First workflow, you will want to switch off database initialization altogether. You can do that by changing the configuration file to specify Disabled for the initializer (Example 6-20).
现在,如果你要部署你的应用程序,你可能变更初始化器为CreateDatabaseIfNotExists,以便永远不会导致数据丢失。您也可能工作在别人为您创建的DBA上,如果数据库在Code First工作流之外创建,你会想禁用数据库的初始化。你可以通过改变配置文件来指定初始化器的禁用(代码6-20).
Example 6-20. Initializer disabled in configuration file
<?xml version="1.0"?> <configuration> <appSettings> <add key="DatabaseInitializerForType DataAccess.BreakAwayContext, DataAccess" value="Disabled" /> </appSettings> </configuration>
Now that we’ve explored setting database initializers in a config file, be sure to remove any settings that you have added.
现在我探索了有关在配置文件设置初始化器的方法,请移除已经添加的任何设置.
Using Database Initializers to Seed Data
数据库数据库初始化器添加种子数据
In this chapter, you have seen how database initializers can be used to control how and when Code First creates the database. So far, the database that Code First creates has always been empty, but there are situations where you may want Code First to create your database with some seed data. You may have some lookup tables that have a predefined set of data, such as Gender or Country. You may just want some sample data in your database while you are working locally so that you can see how your application behaves.
Another scenario where seed data can be useful is running integration tests. In the previous section, we wrote a test that relied on an empty database; now let’s write one that relies on a database containing some well-known data.
Let’s start by writing the test you are going to run. Modify the Main method to run a test that verifies there is a Destination entry for “Great Barrier Reef” in our database (Example 6-21). Be sure you have removed any settings you added to the config file in the previous section.
在本章中,你已经看到数据库的初始化可以被用来控制Code First何时以及如何创建数据库。到目前为止,Code First创建的数据库一直是空的,但也有一些需要Code First创建一些种子数据的情况。您可能有一些预定义的数据,如性别或国家的查找表。或者你可能只是想在本地工作时,在数据库中放一些示例数据,从而可以看到应用程序的行为。
种子数据可以用另一种情况是运行集成测试。在上一节中,我们写了一个测试,依靠的是一个空的数据库,现在让我们进行一个依赖于包含一些已知数据的数据库的测试。
让我们开始写要运行的测试。修改Main方法来运行测试,以验证“Great Barrier Reef”是否为数据库中的Destination条目(例6-21).确保您已经删除在上一节添加到任何设置的配置文件。
Example 6-21. Implementation of pseudo test reliant on seed data
static void Main() { Database.SetInitializer( new DropCreateDatabaseAlways<BreakAwayContext>()); GreatBarrierReefTest(); } static void GreatBarrierReefTest() { using (var context = new BreakAwayContext()) { var reef = from destination in context.Destinations where destination.Name == "Great Barrier Reef" select destination; if (reef.Count() == 1) { Console.WriteLine( "Test Passed: 1 'Great Barrier Reef' destination found"); } else { Console.WriteLine( "Test Failed: {0} 'Great Barrier Reef' destinations found", context.Destinations.Count()); } } }
Run the application, and you will see that the test fails, stating that there are no entries for “Great Barrier Reef” in the database. This makes sense, because you set the DropCreateDatabaseAlways initializer, which will create and empty the database for us.
What the test really needs is a variation of DropCreateDatabaseAlways that will insert some seed data after it has created the database. The three initializers that are included in the Entity Framework are not sealed, meaning you can create your own initializer that derives from one of the included ones. All three of the included initializers also include a Seed method that is virtual (Overridable in Visual Basic), meaning it can be overridden. The seed method has an empty implementation, but the initializers will call it at the appropriate time to insert seed data that you provide.
To check out this feature, add a DropCreateBreakAwayWithSeedData class to your DataAccess project. The key to providing the seed data is to override the initializer’s Seed method, as shown in Example 6-22.
运行应用程序,你会看到测试失败,说明“Great Barrier Reef”在数据库中没有任何条目与之匹配。这是有道理的,因为你设置了DropCreateDatabaseAlways初始化,这将创建和清空数据库。
测试真正需要的是,在创建了数据库后,将插入一些种子数据,能够DropCreateDatabaseAlways的变化来实现。包括在EF框架中的三个初始化器不是sealed的,这意味着你可以通过派生其中之一来创建自己的初始化器。所有三个初始化器都包括一个名为Seed的abstract方法(在Visual Basic中为Overridable),这意味着它可以被覆写。Seed方法有一个空的实现,但是,初始化器可以在适当的时候插入您提供的种子数据。
要检查此功能,您的DataAccess项目添加DropCreateBreakAwayWithSeedData类。提供种子数据的关键是覆写初始化种子的方法,如例6-22所示。
Example 6-22. Initializer with seed data implemented
using System.Data.Entity; using Model; namespace DataAccess { public class DropCreateBreakAwayWithSeedData : DropCreateDatabaseAlways<BreakAwayContext> { protected override void Seed(BreakAwayContext context) { context.Destinations.Add(new Destination { Name = "Great Barrier Reef" }); context.Destinations.Add(new Destination { Name = "Grand Canyon" }); } } }
Notice that there is no call to context.SaveChanges() at the end of the Seed method in Example 6-24. The base Seed method will call that for you after the code in your custom method has been executed. If you let Visual Studio’s editor auto-implement the override method for you, it will include a call to base.Seed(context). You can leave that in if you like, but be sure to let it be the last line of code in the method.
小贴士:注意我们在这里没有调用SaveChanges方法.Seed的基方法会在定制方法之后调用.如果你让VS的编辑器自动实现覆写方法,就会包括一个对base.Seed(context)的调用.你可以不去管他,但是记住要将这行代码放在方法的最后一行.
Now that you have created an initializer that will insert seed data, it needs to be registered with Entity Framework so that it will be used. This is achieved in same way that we registered the included initializers earlier—via the Database.SetInitializer
method.
现在你已经创建了一个能够插入种子数据的初始化器,它需要在EF框架中注册后才能被使用.这可以以我们在前面包含初始化器相同的方式进行--通过Database.SetInitializer方法
Modify the Main method so that DropCreateBreakAwayWithSeedData is registered (Example 6-23).
修改Main方法以注册DropCreateBreakAwayWithSeedData类:
Example 6-23. Initializer with seed data is registered
static void Main() { Database.SetInitializer(new DropCreateBreakAwayWithSeedData()); GreatBarrierReefTest(); }
Run the application again, and the test will pass this time because Code First is now using DropCreateBreakAwayWithSeedData to initialize the database. Because this initializer derives from DropCreateDatabaseAlways, it will drop the database and recreate and empty one. The Seed method that you overrode will then be called and the seed data you specified is inserted into the newly created database each time.
再次运行应用程序,本次测试通过,因为Code First现在使用DropCreateBreakAwayWithSeedData初始化数据库。由于此初始化派生自DropCreateDatabaseAlways,它会删除数据库并重新创建一个空数据库。覆写的Seed方法,随后被调用,您指定的种子数据插入到了新创建的数据库里。
The Seed method in Example 6-24 is a great first look at seeding the database but somewhat simplistic. You can insert various types of data and related data as well. For an example of an efficient LINQ method used to insert entire graphs of related data in Seed, check out my blog post, Seeding a Database with Code First.
代码6-24中的Seed方法或许有些简单化,但这让我们很好地观察了这个方法的创建过程。可以插入各类数据及相关数据。例如使用一个有效率的LINQ方法利用Code First检查我的博客,将相关文章的全部图片作为种子插入数据库。
Using Database Initialization to Further Affect Database Schema
使用数据库初始化进一步影响数据库构架
In addition to seeding a database when Code First creates it, you may want to affect the database in ways that can’t be done with configurations or data seeding. For example, you may want to create an Index on the Name field of the Lodgings table to speed up searches by name.
You can achieve this by calling the DbContext.Database.ExecuteSqlCommand method along with the SQL to create the index inside the Seed method. Example 6-24 shows the modified Seed method that forces this Index to be created before the data is inserted.
除了使用Code First在数据库中创建种子数据以外,你也可不使用配置或种子数据达到相同目的.你如,你可以想创建Lodgings表中Name字段的索引以加快使用name查询的速度.
你可以通过调用DbContext.Database.ExecuteSqlCommand 方法来达到目的,这个方法会在Seed方法内部构造创建索引的SQL语句.代码6-24显示了对Seed方法的修改,强制数据插入时创建索引.
Example 6-24. Using the ExecuteSqlCommand to add an Index to the database
protected override void Seed(BreakAwayContext context) { context.Database.ExecuteSqlCommand ("CREATE INDEX IX_Lodgings_Name ON Lodgings (Name)"); context.Destinations.Add(new Destination { Name = "Great Barrier Reef" }); context.Destinations.Add(new Destination { Name = "Grand Canyon" }); }
Summary
小结
In this chapter you saw how Code First interacts with the database by default, and how you can override this default behavior. You’ve learned how to control the database that Code First connects to and how that database is initialized. You’ve also seen how database initializers can be used in scenario tests to insert seed data into the database as it is initialized.
Throughout this book, you have seen how Code First creates a model based on your
domain classes and configuration. You’ve then seen how Code First locates and initializes the database that the model will be used to access. In the next chapter, you will learn about some advanced concepts that you probably won’t use regularly, but you may find useful from time to time.
在这一章中,你看到了默认情况Code First如何与数据库进行交互,也学习到如何覆写此默认行为。你已经学会了如何控制数据库,Code First连接到数据库时是如何初始化的。您还学到如何将数据库的初始化用于情景测试中,如何在初始化时插入种子数据。
在这本书中,你已经看到Code First根据您的域类和配置创建了一个模型,然后,你也看到Code First是如何定义和初始化被模型用来访问的数据库。在下一章中,您将学习一些不太常用的先进的理念,但这些理念有时会很有用。