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

Posted

技术标签:

【中文标题】EF6:创建存储过程。使用 Fluent API 或 DBMigrations?【英文标题】:EF6: Create stored procedure. Use Fluent API or DBMigrations? 【发布时间】:2016-05-19 15:36:53 【问题描述】:

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

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

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

大多数答案都是关于为 DbMigrations 创建派生类并覆盖 Up() 函数。我明白我应该在 Up 函数中写什么以确保创建存储过程。

但是我应该怎么做才能在创建数据库期间调用这个 Up 函数呢?

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

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

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

【问题讨论】:

【参考方案1】:

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

这里描述的两种方法是:

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

第一种方法更简单。每当创建数据库时,就会调用 Seed 并创建存储过程。但是这种方法的缺点是,每当存储过程的参数名称或类型发生变化时,直到运行时才被检测到。

DbMigration 方法使用 lambda 表达式匹配存储过程的参数,因此每当参数的类型或名称发生变化时,编译器都会检测远程过程的定义是否与参数匹配。

我将描述这两种方法。这两个示例都有相同的简单 Hello World!过程和一个带有很多参数的大型 Merge 过程。

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

方法 IDatabaseInitializer

在您的项目中,您为要访问的数据库表创建实体类和具有 DbSet 属性的类派生形式 DbContext。

例如:

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 ***

StringBuilder 的方法也可以在 *** 上的某个地方找到,可惜我找不到。

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

DbMigration 方法

有关迁移,请参阅:

MSDN: Enabling Migrations Creating and Calling Stored Procedure from Entity Framework 6 Code First

这个想法是让 Visual Studio 包管理器创建一个具有 Up() 函数的 DbManager 派生类。每当数据库向上迁移到派生类的版本时,都会调用此函数。

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

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

要确保调用 Up() 函数,最简单的方法是让 Visual Studio 执行此操作。

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

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

配置派生自带有函数 Seed() 的 DbMigratinConfiguration InitialCreation 从 DbMigration 派生,带有函数 Up()(和函数 Down()。在这个 Up 中,您将看到一个或多个 CreateTable 函数

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

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

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

这让我们有机会在 InitialCaeation.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);

“Hello World”存储过程的创建如下:

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

记住Down()方法:

    public override void Down()
    
        this.DropStoredProcedure("dbo.update2");
    

为了完整性:远程过程调用

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 发生变化时,编译器就可以检查名称和属性类型。

【讨论】:

以上是关于EF6:创建存储过程。使用 Fluent API 或 DBMigrations?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Entity Framework 6.1 fluent API 创建唯一索引

EF 6.X 中的实体框架代码优先 Fluent API 默认值

MVC5加EF6 执行存储过程 获取表

一对一或零对一实体框架代码优先 Fluent Api

使用 Fluent API 创建的 Entity Framework Core 2 一对多

如何在 EF Core 中通过 Fluent Api 创建加密列