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

(0)

相关推荐

  • 详解如何在ASP.NET Core中应用Entity Framework

    首先为大家提醒一点,.NET Core和经典.NET Framework的Library是不通用的,包括Entity Framework! 哪怎么办? 别急,微软为.NET Core发布了.NET Core版本的Entity Framework,具体配置方法与经典.NET Framework版本的稍有区别,下面的内容就为带领大家在ASP.NET Core中应用Entity Framework DB first. 注:目前部分工具处于Preview版本,正式版本可能会稍有区别. 前期准备: 1.推

  • NopCommerce架构分析之(三)EntityFramework数据库初试化及数据操作

    系统启动时执行任务:IStartupTask,启动时执行的任务主要是数据库的初始化和加载. IStartupTask调用IEfDataProvider进行数据库的初始化. IEfDataProvider,SqlCeDataProvider:获取数据连接工厂,不同类型数据库,连接工厂不同. 接口IStartupTask的实体类EfStartUpTask的实现如下: public class EfStartUpTask : IStartupTask { public void Execute() {

  • EntityFramework 6.x学习之多个上下文迁移实现分布式事务详解

    前言 自从项目上了.NET Core平台用上了EntityFramework Core就再没碰过EntityFramework 6.x版本,目前而言EntityFramework 6.x是用的最多,无论是找工作而言还是提升自身技术而言皆自身收益,同时呢,大多数时间除了工作之外,还留有一小部分时间在写EntityFramework 6.x和EntityFramework Core的书籍,所以将EntityFramework 6.x相当于是从零学起,EntityFramework 6.x又添加了许多

  • Python学习之装饰器与类的装饰器详解

    目录 装饰器 装饰器的定义 装饰器的用法 类中的装饰器 类的装饰器-classmethod 类的装饰器-staticmethod 类的装饰器-property 通过学习装饰器可以让我们更好更灵活的使用函数,通过学会使用装饰器还可以让我们的代码更加优雅. 在我们的实际工作中,很多场景都会用到装饰器,比如记录一些日志.或者屏蔽一些不太合法的程序执行从而使我们的代码更加安全. 装饰器 什么是装饰器?虽然对这个次感到陌生,但是完全不需要担心. 首先,装饰器也是一种函数:只不过装饰器可以接收 函数 作为参

  • Android 自定义精美界面包含选项菜单 上下文菜单及监听详解流程

    目录 activity_main.xml源码 main.xml源码 main_menu.xml MainActivity.java源码 先放实现结果 activity_main.xml.main_menu.xml和main.xml设计界面完成如下: MainActivity.java运行结果.长按选择英雄和长按选择铭文结果如下: 目录 需要在menu下创建如下文件,把图片插入drawable activity_main.xml源码 <?xml version="1.0" enco

  • OpenCV学习之图像加噪与滤波的实现详解

    目录 一.实验内容 二.实验环境和配置 三.实验原理及操作 1. 添加噪声 2.噪声二值化 3. 滤波处理 四.实验结果 2.椒盐噪声二值图与白噪声二值图 3.椒盐噪声处理图经处理后图像 4.白噪声处理图经处理后图像 五.结果分析 六.实验源码 一.实验内容 编写一Python程序,要求实现以下功能: 读入一幅图像. 使用两种以上的方法分别向图像中添加噪声. 输出一幅二值图像,图像中未加入噪声的区域为黑色,加入噪声的区域为白色. 使用三种滤波方法对上述添加了噪声的图像进行滤波处理. 输出滤波处理

  • Python深度学习之Keras模型转换成ONNX模型流程详解

    目录 从Keras转换成PB模型 从PB模型转换成ONNX模型 改变现有的ONNX模型精度 部署ONNX 模型 总结 从Keras转换成PB模型 请注意,如果直接使用Keras2ONNX进行模型转换大概率会出现报错,这里笔者曾经进行过不同的尝试,最后都失败了. 所以笔者的推荐的情况是:首先将Keras模型转换为TensorFlow PB模型. 那么通过tf.keras.models.load_model()这个函数将模型进行加载,前提是你有一个基于h5格式或者hdf5格式的模型文件,最后再通过改

  • Python中的上下文管理器相关知识详解

    前言 with 这个关键字,对于每一学习Python的人,都不会陌生. 操作文本对象的时候,几乎所有的人都会让我们要用 with open ,这就是一个上下文管理的例子.你一定已经相当熟悉了,我就不再废话了. with open('test.txt') as f: print f.readlines() 什么是上下文管理器? 基本语法 with EXPR as VAR: BLOCK 先理清几个概念 1. 上下文表达式:with open('test.txt') as f: 2. 上下文管理器:o

  • Python上下文管理器全实例详解

    Python上下文管理器 简介 最近用到这个,仔细了解了一下,感觉是十分有用的,记录一下 使用场景 当我们需要获取一个临时打开的资源,并在使用完毕后进行资源释放和异常处理,利用try-catch语句可以完成,举个例子. 打开文件: f = None try: print("try") f = open("__init__.py", "r") print(f.read()) except Exception as e: print("ex

  • javascript学习笔记(五)原型和原型链详解

    私有变量和函数 在函数内部定义的变量和函数,如果不对外提供接口,外部是无法访问到的,也就是该函数的私有的变量和函数. 复制代码 代码如下: <script type="text/javascript">     function Test(){         var color = "blue";//私有变量         var fn = function() //私有函数         { }     } </script> 这样在

  • yii2学习教程之5种内置行为类详解

    前言 众所周知学习所有知识都需要循序渐进,行为也是一样,在我们学会很牛逼的新建行为,然后轻松注入到组件类之前,先看看yii2框架为我们准备的5个内置的行为类,也许你刚要用到~话不多说了,来一起看看详细的介绍: 本节的目的是让各位小伙伴在使用过程中对行为有一个整体上的感觉. 先亮亮相 TimestampBehavior SluggableBehavior BlameableBehavior AttributeTypecastBehavior AttributeBehavior 网上很多文章只是讲解

  • python基础学习之如何对元组各个元素进行命名详解

    元祖的创建 元祖创建很简单,只需要在括号中添加元素,并使用逗号隔开即可. >>> temp=(1) >>> temp 1 >>> type(temp) <class 'int'> >>> temp2=1,2,3,4,5 >>> temp2 (1, 2, 3, 4, 5) >>> type(temp2) <class 'tuple'> >>> temp=[]

随机推荐