在將項目遷移到MySQL 5.6.10數據庫上時,遇到和遷移到PostgreSQL數據庫相同的一個問題,就是TimeStamp/RowVersion並發控制類型在非Microsoft SQL Server數據庫中的實現。
先上網搜索解決方案,找到Ak.Ini的博文http://www.cnblogs.com/akini/archive/2013/01/30/2882767.html,於是嘗試使用文中介紹的方法。
項目中有一個類要解決並發更新的問題,該類定義:
public class Stock { public int Id { get; set; } [Required(ErrorMessageResourceName = "Generic_Required", ErrorMessageResourceType = typeof(ValidationMessage))] public Location Location { get; set; } [Required(ErrorMessageResourceName = "Generic_Required", ErrorMessageResourceType = typeof(ValidationMessage))] public Part Part { get; set; } public Batch Batch { get; set; } [Required(ErrorMessageResourceName = "Generic_Required", ErrorMessageResourceType = typeof(ValidationMessage))] public int Quantity { get; set; } [Required(ErrorMessageResourceName = "Generic_Required", ErrorMessageResourceType = typeof(ValidationMessage))] public int UpdatedBy { get; set; } [Required(ErrorMessageResourceName = "Generic_Required", ErrorMessageResourceType = typeof(ValidationMessage))] public DateTime UpdatedTime { get; set; } public DateTime RowVersion { get; set; } }
其中最后一個屬性是用作並發控制的,MySqlMigrationSqlGenerator不允許byte[]類型上標記TimeStamp/RowVersion,這里使用DateTime類型。
這是EF生成的Stocks表定義:
> DESC kit.Stocks + ---------- + --------- + --------- + -------- + ------------ + ---------- + | Field | Type | Null | Key | Default | Extra | + ---------- + --------- + --------- + -------- + ------------ + ---------- + | Id | int(11) | NO | PRI | | auto_increment | | Quantity | int(11) | NO | | | | | UpdatedBy | int(11) | NO | | | | | UpdatedTime | datetime | NO | | | | | RowVersion | datetime | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | | Location_Id | int(11) | NO | MUL | | | | Part_PartNo | varchar(50) | NO | MUL | | | | Batch_BatchNo | varchar(50) | YES | MUL | | | + ---------- + --------- + --------- + -------- + ------------ + ---------- + 8 rows
然后在DbContext的構造器中加入下面修改DbModelBuilder的代碼:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>(); modelBuilder.Entity<Stock>().Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); modelBuilder.Entity<Stock>().Property(p => p.RowVersion).IsConcurrencyToken().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); }
上面代碼中前兩行是為了禁用EF級聯刪除的特性,可以參考我以前的博文:http://www.cnblogs.com/jlzhou/archive/2012/03/13/2394333.html
后兩行顯式聲明Id屬性為自增類型,其實EF默認會將Id屬性設置為自增類型,但是在本例中,如果不顯式聲明,EF在生成數據庫時會莫名其妙的將Id屬性當作一般類型處理,不知道是不是因為最后一行設置RowVersion屬性為Identity造成的。
我編寫了一個小程序,用於顯式控制EF根據類定義生成數據庫,並且在生成數據庫后,使用執行SQL語句的方式,修改數據庫對象的定義,比如加入DEFAULT值或者添加索引等約束。下面是代碼片段:
//DbContext構造器中的部分代碼,通過isDoInitialize參數來控制是否初始化數據庫。 public BestDbContext(string databaseName, bool isDoInitialize = true) : base(databaseName) { if (!isDoInitialize) { Database.SetInitializer<BestDbContext>(null); } } //初始化數據庫 Database.SetInitializer(new DropCreateDatabaseAlways<BestDbContext>()); using (var db = new BestDbContext("name=" + databaseName)) { try { db.Database.Initialize(force: false); MessageBox.Show("Database initialized!"); } catch (Exception ex) { MessageBox.Show("Initialization Failed... " + ex.Message); } string sql; sql = @" ALTER TABLE `kit`.`Stocks` CHANGE COLUMN `RowVersion` `RowVersion` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ; "; db.Database.ExecuteSqlCommand(sql); }
注意上面代碼最后的sql執行部分,這里加入對RowVersion數據庫服務器端的缺省值設置,自MySQL 5.6.5版本開始,DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP 選項也可以應用到Datetime類型的列。
最后驗證上述方法,使用 Entity Framework Profiler 試用版(http://hibernatingrhinos.com/products/EFProf),下載解壓縮后,在Project引用中加入對HibernatingRhinos.Profiler.Appender.dll的引用,然后在應用的啟動代碼部分Application_Start in web applications,Program.Main in windows / console applications or the App constructor for WPF applications),加入這一行代碼:HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
啟動應用程序調試,並且啟動EFProf.exe監控程序,你就可以隨時看到EF動態生成的SQL命令了,很是方便,唯一的遺憾是這個工具是收費購買的,微軟又沒有提供非MSSQL的數據庫EF的SQL監控工具。
這是Stocks表在插入新記錄時,EF生成的SQL語句:
INSERT INTO `Stocks` (`Quantity`, `UpdatedBy`, `UpdatedTime`, `Location_Id`, `Part_PartNo`, `Batch_BatchNo`) VALUES ( 1, 1, '2013-03-14T21:37:53' /* @gp1 */, 1, 'PART_A' /* @gp2 */, NULL); SELECT `Id`, `RowVersion` FROM `Stocks` WHERE row_count() > 0 AND `Id` = last_insert_id()
可以看出,保存新對象實例到數據庫時,EF會從數據庫取回RowVersion的值,而這個值是數據庫那邊生成的。
這是更新Stocks表時,EF生成的SQL語句:
UPDATE `Stocks` SET `Quantity` = 6, `UpdatedTime` = '2013-03-14T21:41:14' /* @gp1 */ WHERE (`Id` = 1) AND (`RowVersion` = '2013-03-14T21:14:25' /* @gp2 */)
可以看出,在更新對象實例到數據庫時,EF會從使用先前從數據庫取回RowVersion的值和主鍵作為條件來更新數據行,從而實現樂觀並發控制。