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

Har*_*lse 7 stored-procedures entity-framework code-first

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

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

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

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

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

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

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

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

Har*_*lse 8

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

这里描述的两种方法是:

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

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

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

我将描述这两种方法.这两个例子都有相同的简单Hello World!过程和一个包含大量参数的大型Merge过程.

合并声明的定义并不重要.它的作用是检查是否已有匹配多个属性的记录,如果是,则会增加现有成本的成本.如果不是,它会创建记录并使用成本初始化成本.这是一个典型的例子,使用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          
    }
}
Run Code Online (Sandbox Code Playgroud)

除了数据库类之外,还要创建一个数据库初始化程序,它有一个函数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)
    }
Run Code Online (Sandbox Code Playgroud)

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

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

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

    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());
    }
}
Run Code Online (Sandbox Code Playgroud)

hello world示例可以在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控制台可以使用命令Enable-Migrations启用迁移
  • 使用NuGet包管理器控制台添加一个迁移,并给若名,例如InitialCreation使用命令添加迁移InitialCreation

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

  • 使用函数Seed()从DbMigratinConfiguration派生的配置
  • InitialCreation从DbMigration派生,函数Up()(和函数Down().在这个Up中你会看到一个或多个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);
Run Code Online (Sandbox Code Playgroud)

"Hello World"存储过程创建如下:

    base.CreateStoredProcedure("dbo.HelloWorld3", "begin Select 'Hello World' end;");
Run Code Online (Sandbox Code Playgroud)

带输入参数的存储过程:

    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;");
}
Run Code Online (Sandbox Code Playgroud)

为完整性:远程过程调用

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();
}
Run Code Online (Sandbox Code Playgroud)

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