第六章:控制数据库位置,创建过程和种子数据

前面我们已经提到默认规则和配置可以用于影响模型和数据库构架。本章你会看到如何使用Code First来控制数据库。

你将会学到Code First默认配置如何选择数据库,也会掌握如何修改默认规则或指定上下文使用真正的数据库。我们覆盖的主题将包括指向其他数据库引擎,部署应用程序,执行数据库有关的任何等。

你也可以学习到数据库初始化器可以用于控制数据库生成过程,添加种子数据到数据库中等。这在进行自动测试的场景时特别有用。

控制数据库位置

到目前为止我们都是引用了Code First的默认规则来选择应用程序的数据库目标。默认情况,Code First会使用localhost\SQLEXPRESS作为目标数据库引擎,并使用context类的全名作为数据库名(即命名空间+类名)。如果不符合要求你需要覆写默认规则然后告知Code First想要连接到哪个数据库。你可以Code First的连接工厂来选择数据库,修改或替换默认规则。可选择地,你也可以使用DbContext构造器或应用程序配置文件来告知Code First对某个特定的上下文使用指定的数据库。

小贴士:Code First可以在SQL Azure上使用与任何本地数据库相同的方法进行创建和初始化工作。你可在 http://www.windowsazure.com/zh-cn/develop/overview/ 找到相关的文章。目前数据库供应商正在努力修改他们的产品已提供对Code First的支持。在使用Code First与第三方数据库引擎工作前一定要检查是否支持。

使用配置文件控制数据库位置

控制数据库连接最简单也最可靠的方法是使用配置文件。配置文件可以帮你绕过所有与数据库位置相关的约定,并能指定到你想使用的确切数据库。这种方法是非常有用的,如果你想改变你的上下文的连接字符串指向一个生产数据库,为您部署应用程序。

默认情况下,您添加到您的配置文件的连接字符串应该与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>

小贴士:对熟悉使用EDMX文件创建连接字符串的人,请注意这不是一个EntityConnection String 而是一个简单的数据库连接字符串。使用Code First,你不需要引用元数据文件或System.Data.Entity Client 名称空间。

修改Main方法,调用InsertDestination方法,如代码6-2所示。

Example 6-2. Main method modified to call InsertDestination

static void Main() {
    InsertDestination();
}

运行应用程序,你会发现,BreakAwayConfigFile数据库实例已在您的本地SQL Express上创建了(图6-1)。

image

Code First使用配置文件中的BreakAwayContext连接字符串匹配名为BreakAwayContext的上下文。因为在配置文件中发现了一个条目,就不再使用约定来定位数据库。连接字符串项,也已经被命名为DataAccess.BreakAwayContext,这是上下文的全名。

使用DbContext的构造器控制数据库名称

你已经看到如何通过配置文件中的连接字符串设置上下文,现在让我们来看看使用代码来控制数据库连接的方法。到目前为止,您只使用过DbContext的默认构造函数,还有一些其他可用的构造函数可供使用。其中大部分是更高级的方法,这将在这本书中进行介绍,有两个构造函数允许你影响连接到的数据库。

小贴士:如果您添加了一个连接字符串到您的配置文件,如在上一节所示,开始本节之前,一定要去掉它。记住,配置文件压倒一切,包括在本节的功能。

DbContext有一个构造函数使用一个字符串参数来控制数据库的名称。如果您使用此构造器,您提供的值将被用来代替context的全名。添加到BreakAwayContext构造函数接受一个数据库名称的字符串值,并把它传递给基构造器(例6-3)。请注意,您也必须要加入默认的构造器,以确保所有现有的代码从前面的章节继续工作。

Example 6-3. Database name constructor added to context

public BreakAwayContext() {}
public BreakAwayContext(string databaseName) : base(databaseName) {}

修改Main方法调用 SpecifyDatabaseName方法 (Example 6-4).

static void Main() {
    SpecifyDatabaseName();
}
private static void SpecifyDatabaseName() {
    using(var context = new BreakAwayContext("BreakAwayStringConstructor")) {
        context.Destinations.Add(new Destination {
            Name = "Tasmania"
        });
        context.SaveChanges();
    }
}

新方法使用的构造器,就是你刚刚添加的,用于指定数据库名称。使用的此名称,就不是上下文的全名了。运行应用程序,你会看到一个名为BreakAwayStringConstructor数据库已在您的本地SQL Express实例中创建。

使用DbContext构造器控制连接字符串

在本章的前面,你看到,你可以通过在配置文件中加入与你的上下文的名称相同的连接字符串指定一个数据库。如果您使用的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") {}

重用数据库连接

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) {}

修改Main方法调用新的ReuseDbConnection方法,如Example 6-7所示:

小贴士:你需要引用System.Data.SqlClient名称空间,因为此代码使用SqlConnection类型。

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);
            }
        }
    }
}

ReuseDbConnection构造一个SqlConnection,然后重用它来构造两个单独的BreakAwayContext实例。在这个例子中,在SqlConnection是在代码中定义的连接字符串。然而,Code First并不关心你否获得连接。您可以从资源文件该连接字符串。您也可以使用一些现有的组件,让您获得现有的DbConnection的实例。

使用连接工厂控制数据库位置

控制所使用的数据库的一个最后的选择是通过更换Code First使用默认约一。Code First使用约定是通过Database.DefaultConnectionFactory来进行。连接工厂实现IDbConnectionFactory接口,并负责上下文的命名,并指明为要使用的数据库创建一个DbConnection。EF框架包含两个连接工厂实现,你也可以创建自己的。

使用SqlConnectionFactory

Code First的默认连接工厂是SqlConnectionFactory。此连接工厂将使用SQL Client(System.Data.SqlClient的)数据库引擎连接到数据库。默认的行为,将选择在localhost\ SQLEXPRESS创建数据库,并使用上下文类型的完全限定名作为数据库的名称。集成身份验证,将用于与数据库服务器进行身份验证。 你可以通过指定的连接字符串段,来覆写默认规则。这些片段使用SqlConnectionFactory构造函数相同的语法,在连接字符串中使用。例如,如果你想使用不同的数据库服务器,您可以指定服务器段的连接字符串:

Database.DefaultConnectionFactory = new SqlConnectionFactory("Server=MyDatabaseServer");

可选地,你也可使用不同的验证方式来连接数据库服务器:

Database.DefaultConnectionFactory = new SqlConnectionFactory("User=MyUserName;Password=MyPassWord;");

使用SqlCeConnectionFactory

EF框架还包括SqlCeConnectionFactory,它使用SQL Compact Client 连接到SQL ServerCompact Edition数据库。默认情况下,数据库文件名匹配上下文类的完全限定名,创建在| ApplicationData|目录。(对可执行程序而言,| ApplicationData|位于应用程序所在目录,对web应用程序,伴于网站根目录下的的App_Data子目录内。

小贴士:安装SQL Server Compact Edition

在使用SQL Server Compact Edition前,需要进行安装。可以通过NuGet来进行安装。安装SqlServerCompact 的NuGet包的到你的BreakAwayConsole项目的方法是:右键单击项目并选择:Add Library Package Reference….,从弹出的对话框中选择Online并查找SqlServerCompact.

修改Main方法,如代码6-8所示,设置SqlCeConnectionFactory,然后调用InsertDestination方法(第2章创建)。连接工厂包含在System.Data.Entity.Infrastructure名称空间,需要添加对其的引用。在运行代码前请读完本节。

static void Main() {
    Database.SetInitializer(
    new DropCreateDatabaseIfModelChanges < BreakAwayContext > ());
    Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
    InsertDestination();
}

请注意,您需要指定一个字符串,标识数据库引擎(称为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能够在数据库里生成。 image

写一个定制的连接工厂

到目前为止,您已经看到,连接工厂已经包含EF框架中,但你也可以通过实现IDbConnectionFactory接口来创建自已的连接工厂。 这个接口很简单,包含了一个单一的创建连接的方法,它接受上下文的名称,并返回一个DbConnection。 在本节中,您将构建一个自定义的连接工厂,与SqlConnectionFactory非常相似,但它将只使用上下文类的名称,而不是使用全名作为数据库的名称。您还可以定制这个工厂,当其发现名称中包含Context字符串时将删除它。 加入一个CustomConnectionFactory类到DataAccess项目(代码6-9)。

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());
        }
    }
}

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();
}

运行程序你会在SQL Express实例中发现一个新“BreakAway”数据库创建了。定制工厂已经替你将数据库名的名称空间和后缀Context删除。

image

数据库初始化

在第2章,你已经学习到可以使用Database.SetInitializer方法来为上下文类型设置初始化。设置初始化器可以清除并在模型变化时重建数据库:

Database.SetInitializer(
new DropCreateDatabaseIfModelChanges < BreakAwayContext > ());

初始化包括两个主要步骤。首先,使用Code First在内存中根据默认规则和配置创建模型。其次,使用已设置的数据库初始化器将用于存储数据的数据库初始化。默认情况下,这个初始化将使用Code First创建一个数据库架构的模型。初始化会发生在每一个.NET Framework应用程序的实例上。应用程序的实例也被称为一个AppDomain。当上下文被使用时,初始化第一次被引发。初始化是延迟加载的,所以创建一个实例的是不完全满足初始化发生的条件的。必须执行对模型的操作,如查询或添加实体才会发生。 初始化过程是线程安全的,所以在同一AppDomain中的多个线程可以使用相同的上下文类型。 DbContext本身不是线程安全的,因此,必须只能在一个单独的线程中使用一个给定的上下文类型实例。

在数据库初始化产生时进行控制

有的情况下,您可能希望控制初始化的发生,而不是让它自动发生在应用程序实例中第一次使用上下文对象时。初始化可以使用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);
        }
    }
}

现在我们做些更改以使用初始化失败。这个错误发生在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;
    }
}

运行程序将显示异常信息,表示不能将整形数据映射到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'.

移除刚刚添加到DestinationId上的特性标记,再次运行程序。这次就没有问题了。

关闭数据库初始化功能

当然,并不是所有场景都需要自动调用初始化,EF框架满足所有情况。例如,如果你映射到一个现有的数据库,可能在不能连接到数据库时需要让Code First发生错误而不是魔法般地创建一下。你可以通过传递一个null参数到Database.SetInitializer来关闭初始化功能:

Database.SetInitializer(null);当初始化器被设置为null后,
DbContext.Database.Initialize仍可用于模型的创建过程.

将数据库初始化器包含在EF框架

你会发现,Database.SetInitializer接受IDatabaseInitializer<TContext>的一个实例。在EF框架中有三个针对此接口的实现。这些实现是抽象的,所以你可以从其中派生或自定义行为。后面我们将引导您逐步创建自己的实现。 CreateDatabaseIfNotExists 除非Database.SetInitializer指定了替代的初始化器,所有上下文都会被设置给默认初始化器。这是最安全的初始化,数据库将永远不会被自动删除,造成数据丢失。我们看到在第2章,如果模型是从数据库时创建后发生的改变,在初始化期间会抛出异常:

由于默认初始化器的存在,如果需要执行下列代码,你不需要做任何设置:

Database.SetInitializer(
new CreateDatabaseIfNotExists < BreakAwayContext > ());

从前面几章你已经看到要确保数据库总是匹配当前的模型.如果Code First检测到二者不匹配,数据库就人被删除并且重新创建以便可以满足匹配关系.在开发时这很有用,但是显然不在在程序部署中使用,这样数的会丢失.我们已经看到这样的设置初始化的代码:

Database.SetInitializer(
new DropCreateDatabaseIfModelChanges < BreakAwayContext > ());

这一初始化器将不管模型与数据库匹配与否都会删除和重建数据库.你可能会疑惑为什么要这么做.如果你集成测试的整个应用程序,就会需要在运行测试前将数据库重置到一个已知的状态.修改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());
        }
    }
}

由于初始化器被设置为每次都删除并重建,你会知道在测试开始前数据库是空的.你不用总是去考虑在运行测试前数据库是否为空,后而我们会看到放置一些种子数据在里面的例子.运行程序,我们会看到测试通过.就目前为止我们只执行一个单一的测试,通常一个应用程序里面需要进行多个测试.更新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();
}

运行程序,你会看到第一个方法通过而第二个失败,表明数据库中有两个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());
        }
    }
}

删除和重建数据库是将数据库状态保持在一个已知状态的很容易的方法,但是如果运行一系集成的测试,系统开销过大.考虑使用System.Transactions.TransactionScope作为避免在每一次测试时永久存储对数据库的修改.

Creating a Custom Database Initializer

创建一个定制的数据库初始化器

到目前为止,我们一直在使用EF框架中包含的初始化器.有时不想按照已有的初始化器的逻辑进行工作.Database.SetInitializer 接受IDatabaseInitializer 接口,你可以通过实现这个接口来定制逻辑.

小贴士:除了自已写定制的初始化器,也可以引用别人创建的.有一个例子EFCodeFirst.CreateTablesOnly NuGet 包.这个初始化器允许你在已经存在的数据库进行删除和创建操作,而不是删除和创建数据库实体本身.当你将数据库指向一个宿主数据库而又没有权限删除和创建整个数据库时特别有用.

你想实现自己的初始化器的原因可能有很多。我们来看一个简单的场景:在数据库删除并重新创建之前给开发者一个提示。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();
        }
    }
}

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();
}

我们对模型作些修改使其不再与数据库匹配.修改Destinaton类中的Name属性,在其上附加一个Data Annotations标记:MaxLength.

[MaxLength(200)]
public string Name {
    get;
    set;
}

现在运行的应用程序,将提示您,询问您是否要删除并创建数据库。答否(N),告诉我们的自定义初始化器不理会数据库。你会注意到,应用程序仍然成功完成。这是因为您所做的更改不会阻止EF框架使用当前模型访问过时的数据库架构。EF框架预期Destination Names应为200个字符或更少。由于数据库没有改变,它没有强制执行的最大长度,所以EF框架给它发送的INSERT语句可以执行。 现在,让我们做出改变,会影响INSERT语句。修改目标类,包括一个新的TravelWarnings属性:

public string TravelWarnings {
    get;
    set;
}

再次运行应用程序。像以前一样,你会被提示,询问您是否要删除并创建数据库。选择不创建数据库,这个时候你会得到一个DbUpdateException。你需要通过内部异常链去找到错误的真正原因(图6-4)。 顶层异常的内部异常是UpdateException,该内部异常是一个SQLException。最后的SQLException的消息,说明发生了什么:“无效的列名称”TravelWarnings,“问题发生的原因是EF框架试图执行示例6-18中所示的SQL语句,但TravelWarnings列在数据库中不存在。

image

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()

再次运行程序,这次选择删除并重建数据库,程序成功执行.

在配置文件中设置数据库初始化器

在代码中设置的初始化是一种简单的方法,但部署用程序时,您可能希望有一个更简单的方式设置,而无需修改代码。想要应用程序部署设置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值必须在同一行才能工作.

还应有很多配置行,我们打破配置结构来分别研究。关键部分开始于DatabaseInitializerForType,后跟一个空格,然后配置正确的上下文名称以便初始化器能够为其设置。在我们的例子就是DataAccess.BreakAwayContext,DataAccess,仅仅意味着DataAccess.BreakAwayContext类型的定义是在DataAccess程序集。Value部分是配置数据库初始化器要使用名称。它看起来复杂,因为我们使用泛型类型,我们使用了EF框架程序集中定义的DropCreateDatabaseIfModelChanges<BreakAwayContext> 方法来进行设置。 还需要修改Main方法,以便它不再设置在代码中的初始化:

static void Main() {
    InsertDestination();
}

现在对模型作些改变以便测试配置文件是否得到应用.修改Destination类包含一个新的ClimateInfor属性:

public string ClimateInfo {
    get;
    set;
}

运行程序,你会看到数据库被删除重建,新增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>

现在我探索了有关在配置文件设置初始化器的方法,请移除已经添加的任何设置.

数据库数据库初始化器添加种子数据

在本章中,你已经看到数据库的初始化可以被用来控制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());
        }
    }
}

运行应用程序,你会看到测试失败,说明“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"
            });
        }
    }
}

小贴士:注意我们在这里没有调用SaveChanges方法.Seed的基方法会在定制方法之后调用.如果你让VS的编辑器自动实现覆写方法,就会包括一个对base.Seed(context)的调用.你可以不去管他,但是记住要将这行代码放在方法的最后一行.

现在你已经创建了一个能够插入种子数据的初始化器,它需要在EF框架中注册后才能被使用.这可以以我们在前面包含初始化器相同的方式进行--通过Database.SetInitializer方法

修改Main方法以注册DropCreateBreakAwayWithSeedData类: Example 6-23. Initializer with seed data is registered

static void Main() {
    Database.SetInitializer(new DropCreateBreakAwayWithSeedData());
    GreatBarrierReefTest();
}

再次运行应用程序,本次测试通过,因为Code First现在使用DropCreateBreakAwayWithSeedData初始化数据库。由于此初始化派生自DropCreateDatabaseAlways,它会删除数据库并重新创建一个空数据库。覆写的Seed方法,随后被调用,您指定的种子数据插入到了新创建的数据库里。

代码6-24中的Seed方法或许有些简单化,但这让我们很好地观察了这个方法的创建过程。可以插入各类数据及相关数据。例如使用一个有效率的LINQ方法利用Code First检查我的博客,将相关文章的全部图片作为种子插入数据库。

使用数据库初始化进一步影响数据库构架

除了使用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"
    });
}

小结

在这一章中,你看到了默认情况Code First如何与数据库进行交互,也学习到如何覆写此默认行为。你已经学会了如何控制数据库,Code First连接到数据库时是如何初始化的。您还学到如何将数据库的初始化用于情景测试中,如何在初始化时插入种子数据。 在这本书中,你已经看到Code First根据您的域类和配置创建了一个模型,然后,你也看到Code First是如何定义和初始化被模型用来访问的数据库。在下一章中,您将学习一些不太常用的先进的理念,但这些理念有时会很有用。