Code First開發系列之數據庫遷移


返回《8天掌握EF的Code First開發》總目錄


本篇目錄


本系列的源碼本人已托管於Coding上:點擊查看,想要注冊Coding的可以點擊該連接注冊
先附上codeplex上EF的源碼:entityframework.codeplex.com,此外,本人的實驗環境是VS 2013 Update 5,windows 10,MSSQL Server 2008/2012。

這一篇,我們會學習使用EF的遷移API來記錄結構型數據庫的改變。之前,我們都是使用的數據庫初始化器銷毀然后重新創建數據庫來處理這些改變。現在,我們使用EF的遷移API沒有數據損失地完成同樣的最終結果。我們也會討論對一個已存在的數據庫集成EF的過程,而不是只允許EF從頭開始創建數據庫。最后也會介紹一些其他的功能。

本篇會覆蓋以下知識點:

  1. 在使用了EF的項目上開啟數據庫遷移
  2. 使用自動遷移
  3. 創建顯式的遷移
  4. 添加數據庫工件,例如索引
  5. 對已存在的數據庫添加遷移
  6. 使用EF的其他功能(前面的章節沒有介紹的)

開啟並運行遷移

EF是一個ORM工具,因此它使用數據庫工作,我們已經看到了目前面臨了這么一個挑戰,那就是保持RDBMS和EF實體類同步。之前,我們都是使用一個初始化器來銷毀並重建數據庫以使新的結構匹配上下文和實體。顯然,我們不能在生產環境中這樣做。因此,我們有兩種選擇:第一種是我們可以挑選其他工具(如SQL Server的SSDT)來單獨維護和升級數據庫;第二種選擇就是我們這篇要說的,當數據庫結構發生改變時,使用EF本身來更新數據庫。要使用這項技術,我們必須在項目上啟用遷移(migration)。

先前的例子,我們都是為整個應用程序和EF的實體類使用了單獨一個項目。這在真實的解決方案中沒有意義,很有可能我們要將EF的對象分離成它們自己的一個項目,這個項目一般是類庫類型的。在本篇的例子中,我們就會這么做了。

現在就開始動手了,按照之前做的例子的步驟創建一個類庫項目,取名“Data”,然后添加EF的Nuget包的引用,其次寫實體類和上下文類。最后,將該類庫項目的引用添加到應用程序的主項目中(Console項目)。項目結構如下,代碼就不貼了,大家可看源碼。

圖片

完成上面的步驟之后,下一步就是為Data項目開啟遷移了。打開Nuget包管理控制台窗口,默認項目選擇剛才創建的項目Data,然后在窗口中輸入Enable-Migrations,最后按下Enter鍵即可:

圖片

如果想要獲取PowerShell命令的詳細幫助信息,比如Enable-Migration,只需要輸入Get-Help Enable-Migrations,其他命令依次類推。我們會找到參數信息,有些參數會將遷移指向一個特定的項目或連接字符串。在我們的例子中,我們不需要指定任何參數,因為我們在控制台項目的配置文件中添加了目標連接字符串。運行該命令后,我們會看到Data項目中多了一個名叫Migrations的文件夾,該文件夾里面有一個類,它指定了遷移配置,通過泛型參數將它連接到我們的數據庫上下文類,默認生成的配置類如下所示:

internal sealed class Configuration : DbMigrationsConfiguration<Data.Context>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(Data.Context context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }

該類也有Seed方法,每當遷移應用到數據庫時都會調用該方法,這樣,開發者就可以執行各種各樣的任務,比如插入種子數據。因為該方法會在數據庫上運行多次,所以我們需要確保種子數據不重復,因而我們需要在插入數據之前檢查一下數據是否已經存在。

現在我們准備創建數據庫了,如果在本地工作,只是創建或者更新本地數據庫,那么我們在包管理器控制台中執行Update-Database命令,還有,可以使用Get-Help命令查看可以使用的參數(后面不再啰嗦這個命令)。下面,我們對-script參數感興趣,該參數很有用,因為它可以生成遷移的SQL腳本,我們可以將該腳本交給DBA或者我們自己運行。當運行Update-Database命令時,它會比較實體類、上下文創建的數據庫和物理數據庫的結構。運行該命令之后:

圖片

該錯誤是告訴我們應該啟用自動遷移生成,就像錯誤提示的那樣,將配置類中的AutomaticMigrationsEnabled = false;設置為true,生成解決方案,再次運行生成腳本的命令。我們會看到生成的腳本會在VS中打開,然后可以通過運行該腳本生成目標數據庫。當我們和DBA共事時,他需要復審我們的升級腳本,這個功能就派上用場了。如果我們只是創建本地數據庫的話,只需要運行Update-Database,無需帶任何參數。運行之后,打開數據庫,發現已經創建了新的數據庫,名為“DatabaseMigrationApp”。

圖片

生成的腳本如下:

圖片

自動遷移很容易使用,我們只需要更改代碼,然后重新運行Update-Database將發生的變化傳播到SQL Server數據庫中。要驗證自動遷移沒有數據丟失,我們手動在SSMS中添加一條數據,如下圖:

圖片

現在,我們給Donator添加一個新屬性Message(表示打賞者的留言),定義如下:

 public class Donator
 {
     public int Id { get; set; }
     [StringLength(10)]
     public string Name { get; set; }
     public decimal Amount { get; set; }
     public DateTime DonateDate { get; set; }
     public int ProvinceId { get; set; }
     public virtual Province  Province{ get; set; }

     public string Message { get; set; }
 }


執行Update-Database,我們會看到之前手動添加的數據依然保留着,而且Message列的值是NULL,這是EF自動處理的默認值,如果數值類型,默認值為0。一般來講,對於非空列,EF實際上會嘗試該類型的默認值。比如,我們要給Message屬性添加一個限制,即最大長度為50(默認的長度是MAX),然后更新數據庫,結果會報錯:

圖片

產生這個錯誤的道理很簡單,字符串長度從最大變成50,肯定會造成數據丟失的。如果你知道會造成數據丟失,還要這么做,可以在后面加參數-Force,這個參數會強制更新數據庫。或者,我們可以開啟數據丟失支持,正如EF暴露的這個設置Set AutomaticMigrationDataLossAllowed to 'true'(錯誤信息中提到的)。

我們看到,簡單的情景推薦使用自動遷移,當遷移變得復雜時,自動遷移就不那么好使了,后面會看到。

使用遷移API

假如我還要給Donator實體添加一個非空的創建時間CreationTime屬性:
public DateTime CreationTime { get; set; }
我想要這個新列的默認值為當前插入數據的日期。如果就這樣更新數據庫的話,會發現該列的值是1900/1/1。很顯然,我們並不需要這樣的值,因此,這個時候就需要我們切換到顯式遷移了。一般來說,顯式遷移比自動遷移更加靈活,雖然需要寫更多的代碼,但是對於遷移有了更多控制權,比如遷移名稱和回滾過程等等。如果我們混用了自動遷移和顯式遷移,就會把自己搞糊塗。比如,必須通過搜索項目來檢查列是否通過自動遷移或手動遷移添加了。因此,為了提供一致性和維護目的,我們需要標准化顯式遷移,此時,我們應該關閉自動遷移。

開始顯式遷移之前,先要刪除剛才SSMS中創建的數據庫,然后使用遷移配置類中 AutomaticMigrationsEnabled = false;關閉自動遷移。要創建初始化數據庫遷移,需要使用新的命令Add-Migration,如下所示:

圖片

InitialMigration只是一個遷移名稱,可以隨便起名字。這條指令會在Migrations文件夾中添加一個新類。物理文件會被命名為類似 201606100435228_InitalMigration的東西,文件名以遷移創建的時間為前綴,便於我們在文件夾中組織遷移。生成的代碼如上圖所示。

進一步看一下生成的代碼,EF自動使用DbMigration基類幫我們實現了自己的遷移,重寫了UpDown方法。Up方法將數據庫結構向前移動,例如,我們這里創建了兩張新的表;Down方法幫助我們撤銷更改,以防我們發現了軟件問題需要回滾到之前的數據庫結構。現在使用Update-Database命令更新數據庫,我們會發現數據庫中多了兩張新表。

再仔細看的話,會發現多了一張不是我們創建的表__MigrationHistory,如下所示:

圖片

__MigrationHistory這張表,顧名思義,是記錄遷移歷史的。可以看到,MigrationId對應於初次遷移的文件名,Model列包含了上下文的哈希值,ContextKey包含了上下文配置類的類名。

現在回到之前例子,我們要給Donator實體添加CreationTime屬性,然后,我們需要給這個新屬性添加新的遷移,再次使用Add-Migration指令,執行指令Add-Migration Donator_Add_CreationTime,EF生成的代碼行如下:

public partial class Donator_Add_CreationTime : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.Donators", "CreationTime", c => c.DateTime(nullable: false));
    }
    
    public override void Down()
    {
        DropColumn("dbo.Donators", "CreationTime");
    }
}


之前我們看到了AddTable方法,現在,又看到了AddColumn方法。該方法需要提供表名和列名以及列類型(由相應的.Net類型指定)。這次我們要添加一個自定義的默認值,遷移支持硬編碼默認值和識別為字符串的數據庫引擎默認值。要指定硬編碼默認值,我們可以使用defaultValue參數。下面我們會使用defaultValueSql,如下代碼:

 public override void Up()
 {
     AddColumn("dbo.Donators", "CreationTime", c => c.DateTime(nullable: false,defaultValueSql:"GetDate()"));
 }
 

上面的代碼中,我們使用了SQL Server中的GetDate函數使用當前日期填充新加入的列。實際上,我們也可以在EntityTypeConfiguration類中做許多相同的事情。

DbMigration基類支持許多數據庫工件的維護(不僅僅是列和表),我們可以執行下面的東西:

  • 創建、刪除和更改存儲過程
  • 添加和刪除外鍵
  • 移動數據庫模式之間的工件,例如表和存儲過程
  • 重命名對象,如表、存儲過程和列
  • 維護主鍵約束
  • 創建、重命名和刪除索引

最后,當遇到所說的這些方法不起作用時,可以使用Sql或者SqlFile方法。前者方法需要傳入sql字符串,后者需要一個sql文件名作為參數。

所有遷移默認都以事務的一部分運行,確保所有的遷移操作要么成功,要么什么都不做。這對SQL Server是成立的,但是對於其他RDBMS可能不成立。比如,Oracle不支持DDL(Data Definition Language)定義的結構化操作事務。DDL指的是定義數據庫結構的sql語句。還有DML(Data Manipulation Language),它指的是CRUD操作語句或者其他操作數據的語句。

要創建遷移,我們不一定非要有未處理的更改。比如,要創建一個索引,不需要未處理的更改。另外,我們可以使用EF 6.1中引入的API,它允許我們通過model builder API創建索引。為了這個例子的需要,我們使用遷移API。我們仍然使用之前的指令Add-Migration,這樣就給項目添加了一個遷移,但是Up和Down方法都是空的。現在,我們需要添加創建索引的自定義的代碼,如下所示:

public partial class Donator_Add_Index_Name : DbMigration
{
    public override void Up()
    {
        CreateIndex(
            "Donators",
            new []{"Name"},
            name:"Index_Donator_Name"
            );
    }
    
    public override void Down()
    {
        DropIndex("Donators", "Index_Donator_Name");
    }
}


在上面的遷移中,我創建了一個新的索引,命名為“Donator_Add_Index_Name”,該索引是建立在Donators表上的,包含了Name列,如果需要多個列,只需要在字符串數組中添加列就可以了,至於是聚集索引還是非聚集索引,大家也可以自行配置。在Down方法中,我撤銷了上面的更改,刪除了索引。更新數據庫,在MMSM中可以看到,生成了自己創建的索引:

圖片

到現在,我們還沒有使用Down方法。事實上,EF數據庫遷移支持目標遷移,也就是說,開發者可以將數據庫結構移動到任何版本,這就是遷移,可以前進,也可以后退,就像版本控制工具一樣。遷移實際上是按照文件名的創建時間進行排序的,該文件名被編碼到了遷移的設計器文件中,如201606100502209_InitalMigration.Designer.cs。在解決方案資源管理器中,我們可以看到每個遷移都包含了三個文件,第一個文件是實際的遷移代碼,第二個包含了單詞Designer,指定了遷移Id和一些其他屬性,第三個文件是資源文件,包含了模式名稱和遷移目標哈希值。

現在,嘗試通過指定創建索引遷移之前的目標遷移刪除該索引,創建索引之前的遷移名稱是Donator_Add_CreationTime,要退回上一個遷移,我們可以使用下面的指令:
Update-Database -TargetMigration Donator_Add_CreationTime

圖片

EF會立即通知我們會撤銷哪一個遷移,我們的例子中,就是Donator_Add_Index_Name,如果我們在MMSM中查看數據庫結構的話,會看到之前的索引已經不存在了。

圖片

應用遷移

至今,我們使用VS應用了所有遷移,在VS內部使用這些功能沒有問題,但是當提到更新,測試或生產環境時,這種方式就不奏效了。為了更新這些軟件安裝,提供了以下更多的選擇:

  • 生成更改腳本
  • 使用migrate.exe
  • 使用遷移初始化器

通過腳本應用遷移

包管理器控制台窗口中,我們可以通過Update-Database -Script生成腳本,該命令一執行完成,生成的腳本就會在VS中打開。這個腳本包含了目標數據庫從上次生成到生成腳本這段時間內發生的更改。我們只需要將這個腳本交給DBA,他會使用該腳本維護生產環境的。

需要注意的是,我們要指定匹配目標環境的數據庫的正確連接字符串,因為遷移API會使用上下文比較實時數據庫。我們要么在Update-Database后面帶上連接字符串,要么在配置文件中使用正確的連接字符串。

通過migrate.exe應用遷移

Migrate.exe是伴隨EF發布的工具。使用Nuget安裝EF時,也會將該工具放到EF的安裝包所在的文件夾下。我們只需要將這個工具發布到應用程序的二進制文件夾下(也就是bin目錄),使得該工具可以找到它需要的所有程序集。這個工具需要和Update-Database指令相同的參數,例如:

migrate.exe Data
/connectionString="Data Source=.;Initial Catalog=DatabaseMigrationApp;Integrated Security=SSPI"
/connectionProviderName="System.Data.SqlClient"
/startupConfigurationFile=DatabaseMigrationApp.exe.config

圖片

為了清晰明了,我們將命令行分成多行,為了可讀性,每個參數自成一行。第一個參數是包含上下文和遷移的程序集。然后,指定了連接字符串、provider和配置文件。之所以需要配置文件,是因為上下文構造函數使用了配置文件中的連接字符串名字。

通過初始化器應用遷移

當數據庫模式發生變化時,我們可以使用初始化器重建數據庫。EF自帶了可應用未處理遷移的初始化器基類,這個基類是MigrateDatabaseToLatestVersion。下面定義初始化器:

internal class Initializer:MigrateDatabaseToLatestVersion<Context,Configuration>
{
    public override void InitializeDatabase(Context context)
    {
        base.InitializeDatabase(context);
    }
}


這是個很簡單的類,我們不需要寫任何代碼。如果你想在應用遷移時運行一些代碼,那么可以使用InitializeDatabase方法。該方法會獲得上下文的一個實例,因此我們可以在該方法內將數據添加到數據庫或者執行其他的一些函數。或者,可以使用遷移配置類,它也有Seed方法,可以使用一些種子數據填充數據庫。

現在,只需要在程序啟動時將這個新的初始化器插入到EF中,然后調用上下文強制應用遷移,如下所示:

Database.SetInitializer(new Initializer());
using (var context = new Context())
{
context.Database.Initialize(true);
}

使用其他的初始化器時,我們已經看到了相似的代碼,這里,我們也調用了數據庫的Initialize方法強制對已存在的數據庫執行模式驗證和遷移應用。如果數據庫不存在,就會創建數據庫。

給已存在的數據庫添加遷移

有時,我們想為一個已存在的數據庫添加EF遷移,為的是將處理模式變化從一種方式移動到遷移API。當然,因為數據庫已存在於生產環境,所以我們需要讓遷移知道遷移起始的已知狀態。使用 Add-Migration -IgnoreChanges指令處理這個是相當簡單的,當執行該命令時,EF會創建一個空的遷移,它會假設上下文和實體定義的模型和數據庫是兼容的。一旦通過運行這個遷移更新了數據庫,數據庫模式不會發生變化,但是會在 _MigrationHistory表中添加一條新的數據來對應初次遷移。這個完成之后,我們就可以安全地切換到EF的遷移API來維護數據庫模式變化了。

一些數據庫系統不支持表名的首字母為下划線,EF允許開發者自定義該表名。

另一個想要解決的用例是為已存在的數據庫創建實體類,然后不僅要給已存在的數據庫添加EF,還要給已存在的軟件添加。這個任務可以使用VS的Entity Framework Power Tools插件完成。一旦安裝了該插件,在項目的右鍵菜單上會多個選項Reverse Engineer Code First(工程轉換為Code First)。開發者需要做的是將這個工具指向想要使用EF支持的數據庫,然后該工具會將數據庫中的所有表轉換為實體類、上下文和配置類。我們也可以使用Entity Framework Tools。這個工具集也支持從數據庫生成Code First。為了使用這個功能,只需要從Add New Item對話框選擇ADO.NET Entity Data Model,然后按向導步驟進行即可。

EF的其他功能

下面看一下更多至今還沒有討論的功能,這些功能並不經常使用,但是知道這些功能存在很重要。

自定義約定

有時,我們想做出應用於很多實體類型或者表的全局更改。比如,我們想使得所有的decimal字段默認是確定的大小,除非我們明確指定為其他大小;美國人也可能想要全局設置所有的字符串屬性為非Unicode,因為他們的應用只打算為講英語的人用。我們可以通過使用全局配置API或者自定義約定完成這些任務。比如,下面是如何設置所有的string屬性在數據庫中存儲為非Unicode列:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Properties<string>().Configure(config=>config.IsUnicode(false));
}


我們也可以寫相同的代碼作為自定義約定,然后在model builder中加入到約定集合中。要這么做的話,首先創建一個繼承自Convention的類,重寫構造函數,然后使用之前相同的代碼,調用Convention類的Properties方法。代碼如下:

public class CustomConventions:Convention
{
    public CustomConventions()
    {
        Properties<string>().Configure(config=>config.IsUnicode(false));
    }
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    //modelBuilder.Properties<string>().Configure(config=>config.IsUnicode(false));
    modelBuilder.Conventions.Add<CustomConventions>();
}


必須要記住,要將自定義的約定添加到modelBuilder的Conventions的集合中。

這些類型的約定指的是配置約定。EF中的模型約定API可以創建兩種類型約定:存儲模型和概念模型約定。這些約定允許我們在模型中的許多地方全局地應用更改,而不是一個個實體或者一個個屬性地修改。每種.Net類型也可以寫多個約定,因為EF允許我們控制應用的約定的順序。

地理空間數據

除了標量類型數據,如string或decimal,EF也通過.Net中的DbGeometryDbGeography支持地理空間數據。這些類型有支持地理空間查詢的內置支持和正確翻譯,例如地圖上兩點之間的距離。這些特定的查詢方法對於具有地理空間屬性的實體的查詢很有用,換言之,當使用空間類型時,我們仍編寫.Net代碼。

依賴注入DI和日志記錄

EF現在已經實現了服務定位模式,因此開啟了依賴注入。DI用於支持配置方法。比如,可以使用我們自定義方式來創建依賴解析器Resolver,然后創建通用的EF對象,例如IDbConnectionFactory。請通過閱讀EF的文檔找出可以注入到正在運行的應用中的類或接口,強制EF使用它們而不是使用默認的實現。

也可以將一個自定義的logger注入EF,這樣就可以記錄EF執行的所有actions到自定義的日志源。要創建自定義日志,開發者可以設置Database對象的Log屬性。Log屬性需要賦予一個帶有一個字符串參數的方法,如context.Database.Log = System.Console.Write;,這樣日志就會在控制台中輸出。如果你不喜歡這種記錄日志的方式,也可以創建自定義的方式。

啟動性能

有時,對於大型數據庫和上下文啟動時間可能相對較長,Entity Framework Power Tools允許我們通過暴露一種預生成視圖的能力加速這個過程。這里我們說的不是數據庫視圖,而指的是EF生成的語句可以創建CRUD操作語句。要生成這些預編譯的視圖,我們要做的是在含有派生自DbContext的類所在的文件上右鍵(前提是安裝了Power Tools),在Entity Framework菜單下選擇 Generate Views操作。這個操作會創建需要編譯到程序集的所有代碼。

一個數據庫,多個上下文

我們不必總是將映射到表的所有實體集合放到一個上下文里。使用多個DbContext類有很多優點。這種方式可能會減少啟動時間,因為這個時間一般是和上下文第一次訪問的集合的數量成比例的,也會減少每個上下文對開發者暴露的數據面。還有,它會幫助開發者將數據組織到數據模塊中。當然,如果我們使用遷移,我們仍然需要一個包含每個集合或表的上下文,因為我們會使用這個上下文用於支持遷移。這是我們需要實際配置的唯一上下文。當我們使用多個上下文並在一個事務中將數據保存到多個上下文時,需要做一些額外的事情。每個SaveChanges調用都是事務的,但我們需要為所有的SaveChanges調用創建一個首要的事務。我們也許會發現,對於涉及多個模塊的保存操作,將所有的集合放到單個大型的DbContext中是更簡單的。

本章小結

這篇博客,我們學會了如何使用EF維護數據庫模式,學習了在Nuget包管理器控制台里運行Enable-Migration命令在一個項目上啟用遷移。一旦啟用了遷移,會生成一個配置類,我們可以開始將數據庫模式向前移動了。對於遷移,開發者有兩種選擇,可以依賴自動遷移或者創建顯式遷移。自動遷移有許多限制,例如不可能設置默認值。為了確保遷移一致性,開發者只能選擇使用顯式遷移。所有的顯式遷移繼承自DbMigration類,該類包含了允許更新目標數據庫模式的方法。該類的方法允許我們創建或刪除表,創建、刪除和更改列,創建和刪除索引等等。最后,如果找不到合適的方法或者只需要對數據改變時,可以使用Sql方法來運行任意的SQL命令。如果需要在一個已存在的數據庫上開啟遷移,那么只需要創建一個空的遷移,這樣就將數據庫對應的上下文標記為最新的。一旦這個空的初始遷移創建之后,我們就可以像平常那樣編寫遷移了。在VS內部使用Update-Database命令可以相當容易地更新開發環境中的數據庫。當更新生產環境的數據庫時,我們可以使用初始化器來遷移數據庫,也可以使用migrate.exe或者在VS里生成一個遷移腳本。

最后,我們還介紹了一些EF中額外的功能,這些功能平時可能不怎么用,但是知道一些總是好的。因此我們快速瀏覽了一下這些功能。現在EF支持地理空間數據了,也可以使用日志功能捕獲EF對數據庫創建並執行的一些命令,通過使用多個數據庫上下文類或者預生成視圖可以加速EF的啟動時間。

自我測試

  1. 要發揮EF內置的模式更新的優勢,你必須在項目上開啟遷移,對嗎?
  2. 自動遷移百分之百的時間都會奏效,所以不需要使用顯式遷移,對嗎?
  3. 要生成最新版本的遷移腳本,不需要訪問目標數據庫,對嗎?
  4. 為了在已存在的生產數據庫上添加遷移,你需要怎么做?
  5. 要更新本地開發環境的數據庫,不能使用VS,對嗎?
  6. EF遷移不支持存儲過程,因此必須使用其他的工具來實現,對嗎?
  7. 要為所有的實體類和表的所有decimal字段設置一個通用的精度,必須為每個實體的每個字段指定這個精度,對嗎?
  8. 如果想要通過EF對RDBMS記錄已經觸發的命令,只可以使用數據庫工具,例如SQL Server profiler,對嗎?
  9. 要確定兩個地理坐標(使用SQL Server地理數據類型可以識別坐標存儲)的距離,必須使用存儲過程,對嗎?

如果您覺得這篇文章對您有價值或者有所收獲,請點擊右下方的店長推薦,然后查看答案,謝謝!


參考書籍:
《Mastering Entity Framework》
《Code-First Development with Entity Framework》
《Programming Entity Framework Code First》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM