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来管理事务。

 

时间: 2024-07-30 11:32:16

EntityFramework 6.x多个上下文迁移实现分布式事务的相关文章

最简单易懂的SpringCloudSleuth教程

事务mapjvm 大佬对下面的说法是否同意呢 能否比较下zipkin,pinpoint,以及skywalking.该如何选型 回答: 他们都提供了分布式服务跟踪的能力,pinpoint以及skywalking不仅仅提供了分布式服务跟踪的能力,还提供了其他性能监控,是一个APM解决方案.zipkin主要是分布式服务跟踪,同时与SpringCloud进行有效的集成.个人觉得pinpoint以及skywalking部署相对麻烦一些. 江湖上都推荐pingpointzipkin的监控易于搭建,但是监控的

Linq to Sql : 三种事务处理方式

原文:Linq to Sql : 三种事务处理方式     Linq to SQL支持三种事务处理模型:显式本地事务.显式可分发事务.隐式事务.(from  MSDN: 事务 (LINQ to SQL)).MSDN中描述得相对比较粗狂,下面就结合实例来对此进行阐述. 0. 测试环境 OS Windows Server 2008 Enterprise + sp1 IDE Visual Studio 2008, .net framework 3.5 + SP1 DB SQL Server 2000

传统应用层逻辑分库DB迁移阿里云DRDS+RDS分布式数据库

随着互联网快速发展,我们的结构化关系数据库在高并发.海量数据的情况下面临单机扩展性问题,首先是单机数据库容量瓶颈,单机数据库在业务高速增长的情况下依赖硬件升级也会到达天花板,并且使用成本变得非常高,而且扩展性的复杂性也是比较高,传统数据库扩容往往意味着服务中断,很难做到业务无感知或者少感知.     通过数据水平切换来现实分库可以帮助提升数据库整体性能.横向扩展性,切分后有效的降低了单台机器的访问负载,同时最大限度的降低了数据库服务节点宕机后的损失.      传统应用业务层逻辑或组件分库实现方

【沉淀】实例迁移、Insert和写入性能——数倍,甚至数十倍提升,HybridDB for MySQL负责人王骞谈自己经历和收获

<沉淀>是展示专家风采的人物栏目.它呈现每个专家独一无二的人生经历.认识和感悟的同时,也能帮助你沉淀技术,收获对技术和人生的判断.我们的想法是:"若你想精进为一个很厉害的人,不妨细细品味这些技术牛人背后的沉淀."如果你想了解这些云栖专家更多分享时,请点击云栖专家频道,当然我们也欢迎你往前走一步,成为我们的云栖专家(https://yq.aliyun.com/expert),与技术大牛一起"煮酒论英雄". 技术人是怎样的一群人?一千个人,或许有一千个答案.

分布式系统日志上下文查询功能

日志查看方式 随着技术的发展,越来越多的系统从单机转向到分布式,同时对应的日志查看方式除了直接查看文件外,也发展出了各种集中日志管理方式.其中常见日志查看方式如下: 单线程程序 开发单机程序时诊断非常简单:设置断点,检查断点上下变量状态即可. 当程序部署线上后,可以把断点中内容以日志方式记录下来.出现错误后,还原日志上下文(Context)不难,只要把日志文件中前后N行拿到后都能容易找到问题 多线程程序 当多线程出现后,还原上下文变得复杂一些,因为不同线程打日志顺序会造成一定干扰.我们可以在日志

NUTANIX:从虚拟机迁移到容器是不自然的,对新平台的质疑

本文讲的是NUTANIX:从虚拟机迁移到容器是不自然的,对新平台的质疑,[编者的话]当前的容器技术越来越火,但是没有技术是十全十美的,都有着自己的优势.劣势.Nutanix从自己的角度谈论了容器类技术优劣以及当前应用栈的现状. 如果企业要使用新技术,那就需要做出一些妥协.为此,旧的架构需要调整,并且新的技术需要可以兼容旧的架构. 虚拟化最开始只是想减少物理服务器的管理工作.服务器虚拟化后,用户就可以在虚拟层上创建规格不一的虚拟机.当然,这只是第一个阶段.第二个阶段就是把这些虚拟迁移到云上.第三阶

张逸:限界上下文的边界

边界通过限界上下文来确定,这在领域驱动设计中具有非凡的意义.对应于通用语言,限界上下文是语言的边界,对于领域模型,限界上下文是模型的边界,二者对应于问题空间(Problem Space)的界定.对于系统的架构,限界上下文还确定了应用边界和技术边界,进而帮助我们确定整个系统及各个限界上下文的解决方案.可以说,限界上下文是连接问题空间与解决方案空间的重要桥梁. 那么,限界上下文所界定的边界,究竟是逻辑边界,还是物理边界?这并没有定论,需得依据不同场景而做出不同的决策. 逻辑边界 根据业务对领域进行逻

SQL Server误区:在服务器故障转移后,正在运行的事务继续执行

误区 #1:在服务器故障转移后,正在运行的事务继续执行 这当然是错误的! 每次故障转移都伴随着某种形式的恢复.但是如果当正在执行的事务没有Commit时,由于服务器或实例崩溃导致连接断开,SQL Server可没有办法在故障转移后的服务器重新建立事务的上下文并继续执行事务-无论你使用的故障转移方式是集群,镜像,日志传送或是SAN复制. 对于故障转移集群来说,当故障转移发生后,一个SQL Server实例在另一个故障转移集群的节点启动.所有实例上的数据库都要经历Recovery阶段-也就是所有没有

微服务(Microservices)—Martin Flower【翻译】【转载】

本文转载自:http://www.cnblogs.com/liuning8023/p/4493156.html ---------------------------------------------------------------------------- 原文是 Martin Flower 于 2014 年 3 月 25 日写的<Microservices>. 本文内容 微服务 微服务风格的特性 组件化(Componentization )与服务(Services) 围绕业务功能的组