EntityFramework 6.x學(xué)習(xí)之多個上下文遷移實(shí)現(xiàn)分布式事務(wù)詳解
前言
自從項(xiàng)目上了.NET Core平臺用上了EntityFramework Core就再沒碰過EntityFramework 6.x版本,目前而言EntityFramework 6.x是用的最多,無論是找工作而言還是提升自身技術(shù)而言皆自身收益,同時呢,大多數(shù)時間除了工作之外,還留有一小部分時間在寫EntityFramework 6.x和EntityFramework Core的書籍,所以將EntityFramework 6.x相當(dāng)于是從零學(xué)起,EntityFramework 6.x又添加了許多特性,所以花了一些時間去看并整理了下來,本節(jié)相當(dāng)于是自己一直未碰到過的問題,于是花了一點(diǎn)時間在多個上下文遷移到不同數(shù)據(jù)庫并實(shí)現(xiàn)分布式事務(wù)上,作為基礎(chǔ)入口且同步于書籍,供閱讀者學(xué)習(xí)也是我的點(diǎn)滴積累,文章如有錯誤,請指正。
模型建立
在開始EntityFramework 6.x內(nèi)容敘述之前,我們還是老套路,首先準(zhǔn)備模型,我們搞一個預(yù)約航班的基本模型,一個是航班實(shí)體,另外一個為預(yù)約實(shí)體,請看如下:
/// <summary>
/// 航班
/// </summary>
public class FlightBooking
{
/// <summary>
/// 航班Id
/// </summary>
public int FlightId { get; set; }
/// <summary>
/// 航班名稱
/// </summary>
public string FilghtName { get; set; }
/// <summary>
/// 航班號
/// </summary>
public string Number { get; set; }
/// <summary>
/// 出行日期
/// </summary>
public DateTime TravellingDate { get; set; }
}
/// <summary>
/// 預(yù)訂
/// </summary>
public class Reservation
{
/// <summary>
/// 預(yù)訂Id
/// </summary>
public int BookingId { get; set; }
/// <summary>
/// 預(yù)訂人
/// </summary>
public string Name { get; set; }
/// <summary>
/// 預(yù)訂日期
/// </summary>
public DateTime BookingDate { get; set; } = DateTime.Now;
}
public class TripReservation
{
public FlightBooking Filght { get; set; }
public Reservation Hotel { get; set; }
}
此類用于維護(hù)航班和預(yù)約的實(shí)體,在創(chuàng)建預(yù)約航班時使用。在EntityFramework 6.0+版本上出現(xiàn)了基于代碼配置(Code-based Configuration),對于數(shù)據(jù)庫初始化策略和其他等等配置,我們單獨(dú)建立一個配置類來維護(hù),而無需如我們以往一樣放在DbContext上下文派生類構(gòu)造函數(shù)中,這樣一來上下文派生類看起來則潔凈很多。
public class HotelFlightConfiguration : DbConfiguration
{
public HotelFlightConfiguration()
{
SetDatabaseInitializer(new DropCreateDatabaseIfModelChanges<HotelDBContext>());
SetDatabaseInitializer(new DropCreateDatabaseIfModelChanges<FlightDBContext>());
}
}
接下來我們再來配置兩個DbContext上下文派生類即HotelDbContext和FlightDbContext,并且基本配置信息利用特性來修飾,如下:
[DbConfigurationType(typeof(HotelFlightConfiguration))]
public class FlightDBContext : DbContext
{
public FlightDBContext() : base("name=flightConnection")
{ }
public DbSet<FlightBooking> FlightBookings { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new FlightBookingMap());
base.OnModelCreating(modelBuilder);
}
}
[DbConfigurationType(typeof(HotelFlightConfiguration))]
public class HotelDBContext: DbContext
{
public HotelDBContext():base("name=reservationConnction")
{ }
public DbSet<Reservation> Reservations { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new ReservationMap());
base.OnModelCreating(modelBuilder);
}
}
對應(yīng)的映射配置已經(jīng)敘述很多次了,我們不用廢話,直接給出。
public class FlightBookingMap : EntityTypeConfiguration<FlightBooking>
{
public FlightBookingMap()
{
//table
ToTable("FlightBookings");
//key
HasKey(k => k.FlightId);
//property
Property(p => p.FilghtName).HasMaxLength(50);
Property(p => p.Number);
Property(p => p.TravellingDate);
}
}
public class ReservationMap : EntityTypeConfiguration<Reservation>
{
public ReservationMap()
{
//table
ToTable("Reservations");
//key
HasKey(k => k.BookingId);
//property
Property(p => p.BookingId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
Property(p => p.Name).HasMaxLength(20);
Property(p => p.BookingDate);
}
}
如上兩個上下文我們將遷移到不同數(shù)據(jù)庫,所以連接字符串當(dāng)然是兩個啦。
<connectionStrings> <add name="reservationConnction" connectionString="Data Source=WANGPENG;Initial Catalog=ReservationDb;Integrated Security=true" providerName="System.Data.SqlClient" /> <add name="flightConnection" connectionString="Data Source=WANGPENG;Initial Catalog=FlightDb;Integrated Security=true" providerName="System.Data.SqlClient" /> </connectionStrings>
好了,模型和上下文一切都已構(gòu)建完畢,接下來進(jìn)入到遷移,請往下看。
多個上下文遷移
一個上下文進(jìn)行遷移已經(jīng)沒有什么可說的了,在大多數(shù)場景下,貌似都是一個應(yīng)用程序中僅僅存在一個上下文,因?yàn)槟缓髮?yīng)的只有一個數(shù)據(jù)庫,這個大家是手到擒來,而對于多個上下文遷移對應(yīng)不同數(shù)據(jù)庫遷移又怎么去操作呢?如果你非常熟悉遷移命令,那么就當(dāng)做是回顧吧,如若不然,可以此作為基本參考,有點(diǎn)啰嗦了哈,我們進(jìn)入正文。將模型遷移至數(shù)據(jù)庫并持久化只需要如下三步。
多個上下文遷移至不同文件夾目錄
Enable-Migrations命令

Add-Migration命令

Update-database命令

當(dāng)統(tǒng)一應(yīng)用程序只存在一個上下文時,我們只需要Enabel-Migrations即可,但是若存在多個上下文,若不明確指定上下文很顯然會遷移報(bào)錯,首先我們在NuGet控制臺將項(xiàng)目更換到上下文所在項(xiàng)目中。

接下來運(yùn)行Enable-Migrations初始化遷移目錄,很明顯會出現(xiàn)遷移異常。

由于存在多個上下文,所以我們需要明確指定遷移哪個上下文。通過在其命令后繼續(xù)添加-ContextTypeName指定上下文,并繼續(xù)利用-MigrtionsDirectory指定遷移目錄,最后則是如下命令(不知道有哪些命令嗎,在每個命令后添加一個【-】橫桿并按下Tab鍵則出現(xiàn)你想要的命令)。
Enable-Migrations -ContextTypeName FlightDbContext -MigrationsDirectory:FlightMigrations

接下來利用Add-Migration命令對已掛起模型改變搭建基架,也就是說將上次遷移后我們對模型發(fā)生了更改,以此為下一次遷移搭建基架,此時生成的模型狀態(tài)為掛起狀態(tài)或者稱作為待定狀態(tài)。我們需要遷移上述生成FlightMigrations目錄下的Configuration類,所以此時在Add-Migration命令后指定-ConfigurationTypeName,然后通過-Name指定第一次基架名稱。
Add-Migration -ConfigurationTypeName EntityFrameworkTransactionScope.Data.FlightMigrations.Configuration -Name Initial
或者
Add-Migration -ConfigurationTypeName EntityFrameworkTransactionScope.Data.FlightMigrations.Configuration "Initial"

最后則只需要通過Update-database來持久化到數(shù)據(jù)庫生成表了。
Update-Database -ConfigurationTypeName EntityFrameworkTransactionScope.Data.FlightMigrations.Configuration

同理我們對HotelDbContext利用上述三步命令來進(jìn)行遷移,最后我們能夠很清晰的看到,每個上下文遷移在不同目錄,如下:

上述遷移也沒任何毛病,將每個上下文單獨(dú)遷移生成文件夾,那么我們是否有想過將多個上下文遷移到同一目錄文件夾下且區(qū)分開來呢,在我們只有一個上下文時默認(rèn)給我們創(chuàng)建的文件夾為Migrations,我們就在Migrtions文件夾下生成不同上下文遷移配置。
多個上下文遷移至相同文件夾目錄
這個其實(shí)也很簡單,我們在-MigrationDirectoty后面可以直接指定某個文件夾生成上下文,例如C:\A\DbContext,EntityFramework也做到了這點(diǎn),下面我們來看看。
Enable-Migrations -ContextTypeName FlightDbContext -MigrationsDirectory Migrations\FlightDbContext
Enable-Migrations -ContextTypeName HotelDbContext -MigrationsDirectory Migrations\HotelDbContext
其余兩步運(yùn)行方式和遷移不同一樣,最終我們會看到想要的結(jié)果。

通過上述遷移最終將生成FlightDb和ReservationDb兩個數(shù)據(jù)庫并對應(yīng)FlightBookings和Reservations表。好了到此關(guān)于多個上下文遷移兩種方式就結(jié)束了,我們繼續(xù)本節(jié)的話題。
分布式事務(wù)
有時候我們需要跨數(shù)據(jù)庫管理事務(wù),例如有這樣一個場景,有兩個數(shù)據(jù)庫db1和db2,而tb1在db1中,tb2在db2中,同時tb1和tb2是關(guān)聯(lián)的,在上述中我們創(chuàng)建的航班和預(yù)訂模型,我們需要同時插入航班數(shù)據(jù)和預(yù)約數(shù)據(jù)到不同數(shù)據(jù)庫中,此時則要求事務(wù)一致性,所以為了處理這樣的要求,在.NET 2.0,在System.Transaction命名空間下為我們提供了TransactionScope類。 此類提供了一種使代碼塊參與事務(wù)而不需要與事務(wù)本身交互的簡單方式。強(qiáng)烈建議在using塊中創(chuàng)建TransactionScope對象。
當(dāng)TransactionScope被實(shí)例化時,事務(wù)管理器需要確定要參與哪個事務(wù)。一旦確定,該實(shí)例將一直參與到事務(wù)中。 在創(chuàng)建TransactionScope對象時,我們需要傳遞具有以下值的TransactionScopeOption枚舉:
- Required:實(shí)例必須需要事務(wù),如果事務(wù)已存在,則使用已存在事務(wù),否則將創(chuàng)建新事務(wù)。
- RequiresNew:始終為實(shí)例創(chuàng)建一個新的事務(wù)。
- Suppress:創(chuàng)建實(shí)例時,其他已存在事務(wù)將被抑制,因?yàn)樵搶?shí)例內(nèi)的所有操作的完成而無需其他已存在事務(wù)。
接下來我們利用上述枚舉中第二種方式來實(shí)現(xiàn)航班預(yù)約,簡單邏輯如下:
public class MakeReservation
{
FlightDBContext flight;
HotelDBContext hotel;
public MakeReservation()
{
flight = new FlightDBContext();
hotel = new HotelDBContext();
}
//處理事務(wù)方法
public bool ReservTrip(TripReservation trip)
{
bool reserved = false;
//綁定處理事務(wù)范圍
using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
try
{
//航班信息
flight.FlightBookings.Add(trip.Filght);
flight.SaveChanges();
//預(yù)約信息
hotel.Reservations.Add(trip.Hotel);
hotel.SaveChanges();
reserved = true;
//完成事務(wù)并提交
scope.Complete();
}
catch (Exception ex)
{
throw ex;
}
}
return reserved;
}
}
上述ReservTrip方法接受TripReservation對象。 該方法定義了TransactionScope,并在事務(wù)的上下文中捆綁了用于Flight和Hotel的Create操作,并將代碼寫入try-catch塊中。 如果兩個實(shí)體的SaveChanges方法成功執(zhí)行,那么事務(wù)將被完成,否則回滾。接下來進(jìn)行控制器調(diào)用。
public class TripController : Controller
{
MakeReservation reserv;
public TripController()
{
reserv = new MakeReservation();
}
public ActionResult Index()
{
return View();
}
public ActionResult Create()
{
return View(new TripReservation());
}
[HttpPost]
public ActionResult Create(TripReservation tripinfo)
{
try
{
tripinfo.Filght.TravellingDate = DateTime.Now;
tripinfo.Hotel.BookingDate = DateTime.Now;
var res = reserv.ReservTrip(tripinfo);
if (!res)
{
return View("Error");
}
}
catch (Exception)
{
return View("Error");
}
return View("Success");
}
}
我們添加航班預(yù)約視圖:
@model EntityFrameworkTransactionScope.Data.Entity.TripReservation
@{
ViewBag.Title = "Create";
}
<h2 class="text-center">旅游出行</h2>
@using(Html.BeginForm()){
<table class="table table-condensed table-striped table-bordered">
<tr>
<td>
<table class="table table-condensed table-striped table-bordered">
<tr>
<td colspan="2" class="text-center">
航班信息
</td>
</tr>
<tr>
<td>
航班Id:
</td>
<td>
@Html.EditorFor(m => m.Filght.FlightId)
</td>
</tr>
<tr>
<td>
航班名稱:
</td>
<td>
@Html.EditorFor(m => m.Filght.FilghtName)
</td>
</tr>
<tr>
<td>
航班號:
</td>
<td>
@Html.EditorFor(m => m.Filght.Number)
</td>
</tr>
</table>
</td>
<td>
<table class="table table-condensed table-striped table-bordered">
<tr>
<td colspan="2" class="text-center">
預(yù)約信息
</td>
</tr>
<tr>
<td>
預(yù)約Id:
</td>
<td>
@Html.EditorFor(m => m.Hotel.BookingId)
</td>
</tr>
<tr>
<td>
客戶名稱
</td>
<td>
@Html.EditorFor(m => m.Hotel.Name)
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td colspan="2" class="text-center">
<input type="submit" value="提交預(yù)約" />
</td>
</tr>
</table>
}
視圖展示UI如下:

要運(yùn)行應(yīng)用程序并檢查事務(wù),我們需要使用分布式事務(wù)處理協(xié)調(diào)器(DTC)服務(wù)。 該服務(wù)協(xié)調(diào)更新兩個或多個事務(wù)受保護(hù)資源的事務(wù),例如數(shù)據(jù)庫,消息隊(duì)列,文件系統(tǒng)等。首先我們需要確保DTC是否已經(jīng)開啟,在服務(wù)中進(jìn)行查看并啟用。

接下來打開DTC設(shè)置,請按照下列步驟操作或者直接運(yùn)行【dcomcnfg.exe】一步到位打開組件服務(wù)。
- 打開控制面板
- 找到管理工具
- 找到組件服務(wù)

接下來我們填寫相關(guān)信息來進(jìn)行航班預(yù)約。



如上顯示已經(jīng)預(yù)約成功,我們看看兩個數(shù)據(jù)庫中的數(shù)據(jù)是否正確插入。


在DTC服務(wù)中,若每次提交未中止則提交數(shù)量將增加1,在我們對預(yù)約模型進(jìn)行配置時,我們將主鍵未設(shè)置為標(biāo)識列,所以在我們對主鍵重復(fù)的情況下再來看看表中數(shù)據(jù)。我們提交三次而預(yù)約主鍵不重復(fù),在第四次時主鍵輸入為第三次的主鍵,此時看看結(jié)果如下:




我們驗(yàn)證leFlightBookings和Reservations表中的數(shù)據(jù),則新添加的記錄將不會顯示在其中。 這意味著TransactionScope已經(jīng)通過在單個范圍中將連接與Flight和Hotel數(shù)據(jù)庫捆綁在一起來管理Transaction,并監(jiān)控了Committed和Aborted Transaction。
總結(jié)
正如我們在使用EntityFramework實(shí)體框架作為概念上的數(shù)據(jù)訪問層時,在ASP.NET MVC應(yīng)用程序中看到的那樣,在執(zhí)行多個數(shù)據(jù)庫操作以存儲與其相關(guān)的相關(guān)數(shù)據(jù)時,始終建議使用TransactionScope來管理事務(wù)。
相關(guān)文章
.NET Core實(shí)現(xiàn)企業(yè)微信獲取部門成員
這篇文章介紹了.NET Core實(shí)現(xiàn)企業(yè)微信獲取部門成員的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06
XmlReader 讀取器讀取內(nèi)存流 MemoryStream 的注意事項(xiàng)
XmlReader 讀取器讀取內(nèi)存流 MemoryStream 的注意事項(xiàng)...2007-04-04
.NET發(fā)送郵件的實(shí)現(xiàn)方法示例
這篇文章主要給大家介紹了關(guān)于.NET發(fā)送郵件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用.net具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-06-06
ASP.NET動態(tài)生成靜態(tài)頁面的實(shí)例代碼
生成靜態(tài)頁有很多好處,可以緩解服務(wù)器壓力、方便搜索網(wǎng)站搜索等等,下面介紹一下生成靜態(tài)頁的實(shí)例代碼,有需要的朋友可以參考一下2013-07-07
MVC默認(rèn)路由實(shí)現(xiàn)分頁(PagerExtend.dll下載)
這篇文章主要介紹了MVC默認(rèn)路由實(shí)現(xiàn)分頁,采用bootstrap的樣式,文末提供了PagerExtend.dll下載地址,感興趣的小伙伴們可以參考一下2016-07-07
ASP.NET WebAPi(selfhost)實(shí)現(xiàn)文件同步或異步上傳
這篇文章主要介紹了ASP.NET WebAPi(selfhost)實(shí)現(xiàn)文件同步或異步上傳,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11
asp.net 獲取TreeView中第一個選中的節(jié)點(diǎn)
今天做的項(xiàng)目中有一個要獲取TreeView中第一個選中的節(jié)點(diǎn),當(dāng)然子節(jié)點(diǎn)己包含checkbox以前做過,用的時候又不知道怎么做了,花了點(diǎn)時間又寫了一下,記錄下來,以備下次用.2010-03-03
.NET異步編程總結(jié)----四種實(shí)現(xiàn)模式代碼總結(jié)
本篇文章主要介紹了.NET異步編程總結(jié)----四種實(shí)現(xiàn)模式,詳細(xì)的介紹了每種方法的實(shí)現(xiàn)和實(shí)例,具有一定的參考價值,有興趣的可以了解一下。2016-12-12

