EntityFramework 6.x多個上下文遷移實現分布式事務


前言

自從項目上了.NET Core平台用上了EntityFramework Core就再沒碰過EntityFramework 6.x版本,目前而言EntityFramework 6.x是用的最多,無論是找工作而言還是提升自身技術而言皆自身收益,同時呢,大多數時間除了工作之外,還留有一小部分時間在寫EntityFramework 6.x和EntityFramework Core的書籍,所以將EntityFramework 6.x相當於是從零學起,EntityFramework 6.x又添加了許多特性,所以花了一些時間去看並整理了下來,本節相當於是自己一直未碰到過的問題,於是花了一點時間在多個上下文遷移到不同數據庫並實現分布式事務上,作為基礎入口且同步於書籍,供閱讀者學習也是我的點滴積累,文章如有錯誤,請指正。

模型建立

在開始EntityFramework 6.x內容敘述之前,我們還是老套路,首先准備模型,我們搞一個預約航班的基本模型,一個是航班實體,另外一個為預約實體,請看如下:

    /// <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>
    /// 預訂
    /// </summary>
    public class Reservation
    {
        /// <summary>
        /// 預訂Id
        /// </summary>
        public int BookingId { get; set; }

        /// <summary>
        /// 預訂人
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 預訂日期
        /// </summary>
        public DateTime BookingDate { get; set; } = DateTime.Now;
    }
    public class TripReservation
    {
        public FlightBooking Filght { get; set; }
        public Reservation Hotel { get; set; }
    }

此類用於維護航班和預約的實體,在創建預約航班時使用。在EntityFramework 6.0+版本上出現了基於代碼配置(Code-based Configuration),對於數據庫初始化策略和其他等等配置,我們單獨建立一個配置類來維護,而無需如我們以往一樣放在DbContext上下文派生類構造函數中,這樣一來上下文派生類看起來則潔凈很多。

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

對應的映射配置已經敘述很多次了,我們不用廢話,直接給出。

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

如上兩個上下文我們將遷移到不同數據庫,所以連接字符串當然是兩個啦。

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

好了,模型和上下文一切都已構建完畢,接下來進入到遷移,請往下看。

多個上下文遷移

一個上下文進行遷移已經沒有什么可說的了,在大多數場景下,貌似都是一個應用程序中僅僅存在一個上下文,因為幕后對應的只有一個數據庫,這個大家是手到擒來,而對於多個上下文遷移對應不同數據庫遷移又怎么去操作呢?如果你非常熟悉遷移命令,那么就當做是回顧吧,如若不然,可以此作為基本參考,有點啰嗦了哈,我們進入正文。將模型遷移至數據庫並持久化只需要如下三步。

多個上下文遷移至不同文件夾目錄

  • Enable-Migrations命令

 

  • Add-Migration命令

  • Update-database命令

當統一應用程序只存在一個上下文時,我們只需要Enabel-Migrations即可,但是若存在多個上下文,若不明確指定上下文很顯然會遷移報錯,首先我們在NuGet控制台將項目更換到上下文所在項目中。

接下來運行Enable-Migrations初始化遷移目錄,很明顯會出現遷移異常。

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

  • Enable-Migrations -ContextTypeName FlightDbContext -MigrationsDirectory:FlightMigrations

接下來利用Add-Migration命令對已掛起模型改變搭建基架,也就是說將上次遷移后我們對模型發生了更改,以此為下一次遷移搭建基架,此時生成的模型狀態為掛起狀態或者稱作為待定狀態。我們需要遷移上述生成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來持久化到數據庫生成表了。

  • Update-Database -ConfigurationTypeName EntityFrameworkTransactionScope.Data.FlightMigrations.Configuration 

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

上述遷移也沒任何毛病,將每個上下文單獨遷移生成文件夾,那么我們是否有想過將多個上下文遷移到同一目錄文件夾下且區分開來呢,在我們只有一個上下文時默認給我們創建的文件夾為Migrations,我們就在Migrtions文件夾下生成不同上下文遷移配置。

 多個上下文遷移至相同文件夾目錄

 這個其實也很簡單,我們在-MigrationDirectoty后面可以直接指定某個文件夾生成上下文,例如C:\A\DbContext,EntityFramework也做到了這點,下面我們來看看。

  • Enable-Migrations -ContextTypeName FlightDbContext -MigrationsDirectory Migrations\FlightDbContext
  • Enable-Migrations -ContextTypeName HotelDbContext -MigrationsDirectory Migrations\HotelDbContext

 其余兩步運行方式和遷移不同一樣,最終我們會看到想要的結果。

通過上述遷移最終將生成FlightDb和ReservationDb兩個數據庫並對應FlightBookings和Reservations表。好了到此關於多個上下文遷移兩種方式就結束了,我們繼續本節的話題。

分布式事務

有時候我們需要跨數據庫管理事務,例如有這樣一個場景,有兩個數據庫db1和db2,而tb1在db1中,tb2在db2中,同時tb1和tb2是關聯的,在上述中我們創建的航班和預訂模型,我們需要同時插入航班數據和預約數據到不同數據庫中,此時則要求事務一致性,所以為了處理這樣的要求,在.NET 2.0,在System.Transaction命名空間下為我們提供了TransactionScope類。 此類提供了一種使代碼塊參與事務而不需要與事務本身交互的簡單方式。強烈建議在using塊中創建TransactionScope對象。

 

當TransactionScope被實例化時,事務管理器需要確定要參與哪個事務。一旦確定,該實例將一直參與到事務中。 在創建TransactionScope對象時,我們需要傳遞具有以下值的TransactionScopeOption枚舉:

  • Required:實例必須需要事務,如果事務已存在,則使用已存在事務,否則將創建新事務。
  • RequiresNew:始終為實例創建一個新的事務。
  • Suppress:創建實例時,其他已存在事務將被抑制,因為該實例內的所有操作的完成而無需其他已存在事務。

接下來我們利用上述枚舉中第二種方式來實現航班預約,簡單邏輯如下:

    public class MakeReservation
    {

        FlightDBContext flight;

        HotelDBContext hotel;

        public MakeReservation()
        {
            flight = new FlightDBContext();
            hotel = new HotelDBContext();
        }

        //處理事務方法
        public bool ReservTrip(TripReservation trip)
        {
            bool reserved = false;

            //綁定處理事務范圍
            using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew))
            {
                try
                {
                    //航班信息
                    flight.FlightBookings.Add(trip.Filght);
                    flight.SaveChanges();

                    //預約信息
                    hotel.Reservations.Add(trip.Hotel);
                    hotel.SaveChanges();

                    reserved = true;

                    //完成事務並提交
                    scope.Complete();
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
            return reserved;
        }
    }

上述ReservTrip方法接受TripReservation對象。 該方法定義了TransactionScope,並在事務的上下文中捆綁了用於Flight和Hotel的Create操作,並將代碼寫入try-catch塊中。 如果兩個實體的SaveChanges方法成功執行,那么事務將被完成,否則回滾。接下來進行控制器調用。

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

我們添加航班預約視圖:

@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">
                        預約信息
                    </td>
                </tr>
                <tr>
                    <td>
                        預約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="提交預約" />
        </td>
    </tr>
</table>

}

視圖展示UI如下:

要運行應用程序並檢查事務,我們需要使用分布式事務處理協調器(DTC)服務。 該服務協調更新兩個或多個事務受保護資源的事務,例如數據庫,消息隊列,文件系統等。首先我們需要確保DTC是否已經開啟,在服務中進行查看並啟用。

接下來打開DTC設置,請按照下列步驟操作或者直接運行【dcomcnfg.exe】一步到位打開組件服務。

  • 打開控制面板
  • 找到管理工具
  • 找到組件服務

接下來我們填寫相關信息來進行航班預約。

如上顯示已經預約成功,我們看看兩個數據庫中的數據是否正確插入。

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

 

我們驗證leFlightBookings和Reservations表中的數據,則新添加的記錄將不會顯示在其中。 這意味着TransactionScope已經通過在單個范圍中將連接與Flight和Hotel數據庫捆綁在一起來管理Transaction,並監控了Committed和Aborted Transaction。

總結

正如我們在使用EntityFramework實體框架作為概念上的數據訪問層時,在ASP.NET MVC應用程序中看到的那樣,在執行多個數據庫操作以存儲與其相關的相關數據時,始終建議使用TransactionScope來管理事務。

 


免責聲明!

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



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