EF6:创建存储过程。使用Fluent API或DBMigrations?

时间:2016-05-19 15:36:54

标签: entity-framework stored-procedures code-first

我首先使用EF6代码来创建我的数据库。我理解语法,DbContext和模型构建器。我使用LINQ进行了几次详尽的查询,一切正常。

但是现在我需要做一些使用linq在一个查询中无法完成的事情。我需要使用存储过程执行Merge语句。

我已经看到了几个关于如何创建存储过程的问题,例如: Create Stored Procedures using Entity Framework Code First?

大多数答案都在谈论为DbMigrations创建派生类并重写Up()函数。我理解我应该在Up函数中编写什么来确保创建存储过程。

  

但是我应该怎么做才能在数据库创建过程中调用Up函数?

我应该在DbContext.OnModelCreating中做些什么吗?

我认为我不应该实例化DbMigrations的子类并调用Up()。

上面提到的链接是关于“打开包管理器控件”。那是什么?或者,在从较旧版本迁移到较新版本时,您是否真的使用此方法?

1 个答案:

答案 0 :(得分:9)

经过一番调查后,我发现如何确保在创建数据库时创建存储过程。我发现了两种方法,每种方法各有优缺点。因此我描述了它们。对不起,如果这个答案相当长。

这里描述的两种方法是:

  • 创建一个DataBase Initializer ,一个实现IDataBaseInitializer的类。这可能是从DropCreateDatabaseIfModelChanges或类似的派生的类。覆盖Seed函数并使用context.Database.ExecuteSqlCommand(...)在此函数中创建存储过程。
  • 使用实体框架迁移创建存储过程。

第一种方法更简单。无论何时创建数据库,都会调用Seed并创建存储过程。但是,此方法的缺点是,只要存储过程的参数的名称或类型发生更改,就不会在运行时检测到这种情况。

DbMigration方法使用lambda表达式匹配存储过程的参数,因此只要参数的类型或名称发生更改,编译器就会检测远程过程的定义是否与参数匹配。

我将描述这两种方法。这两个例子都有相同的简单Hello World!过程和带有大量参数的大合并程序。

  

合并声明的定义并不重要。什么   确实是否已经有匹配的记录   属性,如果是这样,它会增加现有成本的成本。如果没有   创建记录并使用成本初始化成本。这是一个   使用linq语句和IQueryable的典型示例是不够的。   使用linq,必须检索记录,更新它并调用   SaveChanges,问题(1)在   与此同时,其他人可能已经添加了一个值,并且(2)它需要   至少两次往返。因此需要存储过程。

方法IDatabaseInitializer

在您的项目中,您可以为要访问的数据库表创建实体类和一个派生自DbContext的类,其中包含DbSet属性。

例如:

public class UsageCosts
{
    public int Id {get; set; }
    public DateTime InvoicePeriod { get; set; }
    public long CustomerContractId { get; set; }
    public string TypeA { get; set; }
    public string TypeB { get; set; }
    public decimal VatValue { get; set; }

    // the value to invoice
    public decimal PurchaseCosts { get; set; }
    public decimal RetailCosts { get; set; }
}

public class DemoContext : DbContext
{
    public DemoContext(string nameOrConnectionString) : base(nameOrConnectionString) {}

    public DbSet<UsageCosts> UsageCosts { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        // add entity framework fluent api statements here          
    }
}

除了你的数据库类,创建一个数据库初始化程序,它有一个函数Seed,将在创建数据库时调用。

internal class DataBaseInitializer : DropCreateDatabaseIfModelChanges<DemoContext>
{
    protected override void Seed(DemoContext context)
    {
        base.Seed(context);

        // create stored procedures here
        this.CreateStoredProcedureHelloWorld(context)
        this.CreateStoredProcedureUpdateUsageCosts(context)
    }

显示如何创建存储过程(Hello World!)的简单示例

    private void CreateStoredProcedureHelloWorld(DemoContext context)
    {
        context.Database.ExecuteSqlCommand("create procedure HelloWorld as begin Select 'Hello World' end;");
    }

使用输入参数创建存储过程:

    private void CreateStoredProcedureUpdateUsageCosts(DemoContext context)
    {
        var x = new StringBuilder();
        x.AppendLine(@"create procedure updateusagecosts");
        x.AppendLine(@"@InvoicePeriod datetime,");
        x.AppendLine(@"@CustomerContractId bigint,");
        x.AppendLine(@"@TypeA nvarChar(80),");
        x.AppendLine(@"@TypeB nvarChar(80),");
        x.AppendLine(@"@VatValue decimal(18, 2),");
        x.AppendLine(@"@PurchaseCosts decimal(18, 2),");
        x.AppendLine(@"@RetailCosts decimal(18, 2)");
        x.AppendLine(@"as");
        x.AppendLine(@"begin");
        x.AppendLine(@"Merge [usagecosts]");
        x.AppendLine(@"Using (Select @InvoicePeriod as invoicePeriod,");
        x.AppendLine(@"              @CustomerContractId as customercontractId,");
        x.AppendLine(@"              @TypeA as typeA,");
        x.AppendLine(@"              @TypeB as typeB,");
        x.AppendLine(@"              @VatValue as vatvalue)");
        x.AppendLine(@"              As tmp ");
        x.AppendLine(@"On ([usagecosts].[invoiceperiod] = tmp.invoiceperiod");
        x.AppendLine(@"AND [usagecosts].[customercontractId] = tmp.customercontractid");
        x.AppendLine(@"AND [usagecosts].[typeA] = tmp.typeA");
        x.AppendLine(@"AND [usagecosts].[typeB] = tmp.typeB");
        x.AppendLine(@"AND [usagecosts].[vatvalue] = tmp.Vatvalue)");
        x.AppendLine(@"When Matched Then ");
        x.AppendLine(@"    Update Set [usagecosts].[purchasecosts] = [usagecosts].[purchasecosts] + @purchasecosts,");
        x.AppendLine(@"               [usagecosts].[retailcosts] = [usagecosts].[retailcosts] + @retailcosts");
        x.AppendLine(@"When Not Matched Then");
        x.AppendLine(@"    Insert (InvoicePeriod, CustomerContractId, typea, typeb, vatvalue, purchasecosts, retailcosts)");
        x.AppendLine(@"    Values (@invoiceperiod, @CustomerContractId, @TypeA, @TypeB, @VatValue, @PurchaseCosts, @RetailCosts);");
        x.AppendLine(@"end");
        context.Database.ExecuteSqlCommand(x.ToString());
    }
}

The hello world example can be found here on StackOverflow

使用StringBuilder的方法也可以在StackOverflow的某个地方找到,但是我无法找到它。

在创建数据库期间,调用DatabaseInitializer.Seed(...)。这里命令上下文执行SQL语句。该语句是一个字符串。 这就是为什么编译器不会注意到函数参数名称或类型的变化。

DbMigration方法

对于迁移,请参阅:

我们的想法是让visual studio包管理器创建一个具有Up()函数的DbManager派生类。只要将数据库向上迁移到派生类的版本,就会调用此函数。

在Up()中,您可以调用基类DbMigration.CreateStoredProcedure。这个方法的好处是从实体类型到参数的转换是使用委托(使用lambda表达式)完成的,因此在编译时检查:属性是否仍然存在并且它们是否具有正确的类型?

唉,从DbMigration构造派生类是不够的,并从Seed()函数中调用Up()函数。

为了确保调用Up()函数,最简单的方法是让visual studio执行此操作。

  • 创建项目
  • 为实体框架添加Nuget包
  • 使用实体类的DbSet属性创建实体类和DbContext
  • 在visual studio中,通过“工具”菜单启动Nuget包管理器控制台
  • 使用Nuget Package Manager控制台使用命令启用 - 迁移
  • 启用迁移
  • 使用Nuget Package Manager控制台添加一个迁移,并使用 add-Migration InitialCreation
  • 命令为名称命名,例如 InitialCreation

您会注意到您的项目中添加了几个类。

    使用函数Seed()从DbMigratinConfiguration派生的
  • 配置
  • InitialCreation 从DbMigration派生,使用函数Up()(和函数Down()。在此向上,您将看到一个或多个CreateTable函数

如果您仍然拥有上一个示例中所述的数据库播种器类,并且您使用DataBase.SetInitializer对其进行初始化,那么每当需要重新创建数据库时,都会调用各种Up()和Seed()函数按以下顺序:

  • 配置构造函数
  • InitialCreation.Up()
  • DatabaseSeeder.Seed()

由于某种原因,未调用Configuration.Seed()。

这使我们有机会在InitialCraeation.Up()

中创建存储过程
public override void Up()
{
    CreateTable("dbo.UsageCosts",
        c => new
            {
                Id = c.Int(nullable: false, identity: true),
                InvoicePeriod = c.DateTime(nullable: false),
                CustomerContractId = c.Long(nullable: false),
                TypeA = c.String(),
                TypeB = c.String(),
                VatValue = c.Decimal(nullable: false, precision: 18, scale: 2),
                PurchaseCosts = c.Decimal(nullable: false, precision: 18, scale: 2),
                RetailCosts = c.Decimal(nullable: false, precision: 18, scale: 2),
            })
        .PrimaryKey(t => t.Id);

&#34; Hello World&#34;存储过程创建如下:

    base.CreateStoredProcedure("dbo.HelloWorld3", "begin Select 'Hello World' end;");

带输入参数的存储过程:

    base.CreateStoredProcedure("dbo.update2", p => new
    {
        InvoicePeriod = p.DateTime(),
        CustomerContractId = p.Long(),
        TypeA = p.String(maxLength: 80),
        TypeB = p.String(maxLength: 80),
        VatValue = p.Decimal(10, 8),
        PurchaseCosts = p.Decimal(10, 8),
        RetailCosts = p.Decimal(10, 8),
    },
    @"begin
        Merge [usagecosts]
        Using (Select
            @InvoicePeriod as invoicePeriod,
            @CustomerContractId as customercontractId,
            @TypeA as typeA,
            @TypeB as typeB,
            @VatValue as vatvalue)
            As tmp 
        On ([usagecosts].[invoiceperiod] = tmp.invoiceperiod
        AND [usagecosts].[customercontractId] = tmp.customercontractid
        AND [usagecosts].[typeA] = tmp.typeA
        AND [usagecosts].[typeB] = tmp.typeB
        AND [usagecosts].[vatvalue] = tmp.Vatvalue)
    When Matched Then 
        Update Set [usagecosts].[purchasecosts] = [usagecosts].[purchasecosts] + @purchasecosts, [usagecosts].[retailcosts] = [usagecosts].[retailcosts] + @retailcosts
    When Not Matched Then
        Insert (InvoicePeriod, CustomerContractId, typea, typeb, vatvalue, purchasecosts, retailcosts)
        Values (@invoiceperiod, @CustomerContractId, @TypeA, @TypeB, @VatValue, @PurchaseCosts, @RetailCosts);
     end;");
}

完整性:远程过程调用

using (var dbContext = new DemoContext())
{
    object[] functionParameters = new object[]
    {
        new SqlParameter(@"InvoicePeriod", usageCosts.InvoicePeriod),
        new SqlParameter(@"CustomerContractId", usageCosts.CustomerContractId),
        new SqlParameter(@"TypeA", usageCosts.TypeA),
        new SqlParameter(@"TypeB", usageCosts.TypeB),
        new SqlParameter(@"VatValue", usageCosts.VatValue),
        new SqlParameter(@"PurchaseCosts", 20M),
        new SqlParameter(@"RetailCosts", 30M),
    };
    string sqlCommand = String.Format(@"Exec {0} @InvoicePeriod, @CustomerContractId, @TypeA, @TypeB, @VatValue, @PurchaseCosts, @RetailCosts", functionName);
    dbContext.Database.ExecuteSqlCommand(sqlCommand, functionParameters);
    dbContext.SaveChanges();
}

在我看来,最好把它放在DbSet的扩展方法中。每当UsageCosts更改时,编译器都可以检查名称和属性类型。