一. 什么是事务
我们通俗的理解事务就是一系列操作要么全部成功、要么全部失败(不可能存在部分成功,部分失败的情况)。
举一个事务在我们日常生活中的经典例子:两张银行卡(甲、乙),甲向乙转钱,整个过程需要执行两个操作,甲账户钱减少,乙账户钱增加,这是转账成功的情况;转账失败时候,二者均不执行,甲乙账户钱都不变。
1. SQLServer中的事务
SQLServer中的事务具体封装方式有多种,但无论是哪种,都需要依赖于这三句代码:begin transaction 、commit transaction、rollback transaction。
具体怎么使用,详见数据库事务章节:去复习
2. EF中的事务
EF中的事务主要分为三类,分别是SaveChanges、DbContextTransaction、TransactionScope。
需要掌握的是这三种事务各自的使用方式和适用场景。
二. SaveChanges
SaveChanges是EF中最常见的事务了,在前面章节,多次提到,SaveChanges一次性将本地缓存中所有的状态变化一次性提交到数据库,这就是一个事务,要么统一成功,要么统一回滚。
应用场景:该种事务模式在实际开发中非常常用,在处理模块业务同时需要对一张表(或多张表)进行一系列增删改操作,但这些操作都是要么都成功,要么都失败,这种情况下可以在流程的最后 调用SaveChanges,一次性保存本地属性状态的全部变化。
该模式作用于一个DBContext,即一个数据库的EF的上下文,不能控制多个数据库。
比如:注册场景,可能需要向用户表、日志表、账户表等多张表中插入数据,其中一张表插入失败,所有的都需要回滚,这种情况,就可以在该业务的最后,统一调用SaveChanges一次性的事务提交。
1. 事务成功
1 private static void TestSaveChanges()2 {3 using (DbContext db = new CodeFirstModel())4 {5 //增加6 TestInfor t1 = new TestInfor()7 {8 id = Guid.NewGuid().ToString("N"),9 txt1 = "txt1",
10 txt2 = "txt2"
11 };
12 db.Set<TestInfor>().Add(t1);
13 //删除
14 TestInfor t2 = db.Set<TestInfor>().Where(u => u.id == "1").FirstOrDefault();
15 if (t2 != null)
16 {
17 db.Set<TestInfor>().Remove(t2);
18 }
19 //修改
20 TestInfor t3 = db.Set<TestInfor>().Where(u => u.id == "3").FirstOrDefault();
21 t3.txt2 = "我是李马茹23";
22
23 //SaveChanges事务提交
24 int n = db.SaveChanges();
25 Console.WriteLine("数据作用条数:" + n);
26 }
27 }
2. 事务回滚
1 private static void TestSaveChangesFailure()2 {3 using (DbContext db = new CodeFirstModel())4 {5 //增加6 TestInfor t1 = new TestInfor()7 {8 id = Guid.NewGuid().ToString("N"),9 txt1 = "txt1",
10 txt2 = "txt2"
11 };
12 db.Set<TestInfor>().Add(t1);
13 //删除
14 TestInfor t2 = db.Set<TestInfor>().Where(u => u.id == "1").FirstOrDefault();
15 if (t2 != null)
16 {
17 db.Set<TestInfor>().Remove(t2);
18 }
19 //修改
20 TestInfor t3 = db.Set<TestInfor>().Where(u => u.id == "3").FirstOrDefault();
21 t3.txt2 = "我是李马茹23";
22 //增加(制造错误)
23 TestInfor t4 = new TestInfor()
24 {
25 id = Guid.NewGuid().ToString("N") + "123",
26 txt1 = "txt1",
27 txt2 = "txt2"
28 };
29 db.Set<TestInfor>().Add(t4);
30
31 //SaveChanges事务提交
32 int n = db.SaveChanges();
33 Console.WriteLine("数据作用条数:" + n);
34 }
35 }
三. DbContextTransaction事务
该事务为EF6新增的事务,通常用于手动接管事务,某些操作是一个事务,某些操作是另外一个事务。
使用场景:EF调用SQL语句的时候使用该事务、 多个SaveChanges的情况(解决自增ID另一个业务中使用的场景)。
核心代码:BeginTransaction、Commit、Rollback、Dispose. 如果放到using块中,就不需要手动Dispose了。
该种事务与数据库中的transaction事务原理一致,在EF中,由于每次调用 db.Database.ExecuteSqlCommand(sql1, pars1)的时候,即刻执行了该SQL语句,所以要把他放到一个大的事务中,整体提交、回滚.(EF6新增的)
1. 事务成功
1 private static void TestDbContextTransaction()2 {3 using (DbContext db = new CodeFirstModel())4 {5 DbContextTransaction trans = null;6 try7 {8 //开启事务9 trans = db.Database.BeginTransaction();
10 //增加
11 string sql1 = @"insert into TestInfor values(@id,@txt1,@txt2)";
12 SqlParameter[] pars1 ={
13 new SqlParameter("@id",Guid.NewGuid().ToString("N")),
14 new SqlParameter("@txt1","txt11"),
15 new SqlParameter("@txt2","txt22")
16 };
17 db.Database.ExecuteSqlCommand(sql1, pars1);
18 //删除
19 string sql2 = @"delete from TestInfor where id=@id";
20 SqlParameter[] pars2 ={
21 new SqlParameter("@id","22")
22 };
23 db.Database.ExecuteSqlCommand(sql2, pars2);
24 //修改
25 string sql3 = @"update TestInfor set txt1=@txt1 where id=@id";
26 SqlParameter[] pars3 ={
27 new SqlParameter("@id","3"),
28 new SqlParameter("@txt1","二狗子")
29 };
30 db.Database.ExecuteSqlCommand(sql3, pars3);
31
32 //提交事务
33 trans.Commit();
34 Console.WriteLine("事务成功了");
35 }
36 catch (Exception ex)
37 {
38 Console.WriteLine(ex.Message);
39 trans.Rollback();
40
41 }
42 finally
43 {
44 trans.Dispose(); //也可以把该事务写到using块中,让其自己托管,就不需要手动释放了
45 }
46 }
47 }
2. 事务回滚
1 private static void TestDbContextTransactionFailure()2 {3 using (DbContext db = new CodeFirstModel())4 {5 DbContextTransaction trans = null;6 try7 {8 //开启事务9 trans = db.Database.BeginTransaction();
10 //增加
11 string sql1 = @"insert into TestInfor values(@id,@txt1,@txt2)";
12 SqlParameter[] pars1 ={
13 new SqlParameter("@id",Guid.NewGuid().ToString("N")),
14 new SqlParameter("@txt1","txt11"),
15 new SqlParameter("@txt2","txt22")
16 };
17 db.Database.ExecuteSqlCommand(sql1, pars1);
18 //删除
19 string sql2 = @"delete from TestInfor where id=@id";
20 SqlParameter[] pars2 ={
21 new SqlParameter("@id","22")
22 };
23 db.Database.ExecuteSqlCommand(sql2, pars2);
24 //修改
25 string sql3 = @"update TestInfor set txt1=@txt1 where id=@id";
26 SqlParameter[] pars3 ={
27 new SqlParameter("@id","3"),
28 new SqlParameter("@txt1","二狗子222")
29 };
30 db.Database.ExecuteSqlCommand(sql3, pars3);
31
32
33 //增加(制造错误)
34 string sql4 = @"insert into TestInfor values(@id,@txt1,@txt2)";
35 SqlParameter[] pars4 ={
36 new SqlParameter("@id",Guid.NewGuid().ToString("N")+"123"),
37 new SqlParameter("@txt1","txt11"),
38 new SqlParameter("@txt2","txt22")
39 };
40 db.Database.ExecuteSqlCommand(sql4, pars4);
41
42 //提交事务
43 trans.Commit();
44 Console.WriteLine("事务成功了");
45 }
46 catch (Exception ex)
47 {
48 Console.WriteLine(ex.Message);
49 trans.Rollback();
50
51 }
52 finally
53 {
54 trans.Dispose();
55 }
56 }
57 }
3. 自DbContextTransaction事务测试多个SaveChanges的情况统一回滚
1 private static void DbContextTransSaveChanges()2 {3 using (DbContext db = new CodeFirstModel())4 {5 //自动脱管,不需要手动释放6 using (DbContextTransaction trans = db.Database.BeginTransaction())7 {8 try9 {
10 TestInfor t1 = new TestInfor()
11 {
12 id = Guid.NewGuid().ToString("N"),
13 txt1 = "111111111",
14 txt2 = "222222222222"
15 };
16 db.Entry(t1).State = EntityState.Added;
17 db.SaveChanges();
18
19 //人为制造失败情况
20 TestInfor t2 = new TestInfor()
21 {
22 id = Guid.NewGuid().ToString("N") + "123",
23 txt1 = "111111111",
24 txt2 = "222222222222"
25 };
26 db.Entry(t2).State = EntityState.Added;
27 db.SaveChanges();
28
29 trans.Commit();
30 }
31 catch (Exception)
32 {
33 trans.Rollback();
34 }
35
36 }
37 }
38 }
四. TransactionScope事务
1. 该事务用来处理多个SaveChanges的事务(特殊情况的业务)或者多个DBContext(每个DBContext是一个实例,代表不同的数据库连接).
2. 核心代码:(一个Complete函数走天下,异常的话,自动回滚 ,也可以结合try-catch Transaction.Current.Rollback();实现回滚)
需要引入程序集:using System.Transactions;
3. 适用场景:
①该种事务适用于多数据库连接的情况
特别注意:如果使用该事务来处理多个数据库(多个DBContex)时,必须手动开启msdtc服务,这样才可以将多个DB的SaveChange给放到一个事务中,如果失败, 则多个数据库的数据统一回滚.
开启msdtc服务的步骤: cmd命令→net start msdtc
②主键id自增的情况,同一个业务线中,需要拿到新增加的数据的主键id,进行操作。
③多线程带锁的情况,同一条业务线前半部分必须先SaveChanges,才能保证数据准确性(测试简单版本,实际的业务场景待补充!!!)
4. 主键自增被使用的情况的解决方案
1 private static void Test3()2 {3 using (DbContext db = new CodeFirstModel())4 {5 using (TransactionScope trans = new TransactionScope())6 {7 try8 {9 TestInfor2 t1 = new TestInfor2()
10 {
11 txt11 = Guid.NewGuid().ToString("N"),
12 txt22 = Guid.NewGuid().ToString("N")
13 };
14 db.Set<TestInfor2>().Add(t1);
15 //如果这里不写SaveChanges的话,t1.id永远为0
16 db.SaveChanges();
17
18
19 TestInfor2 t2 = new TestInfor2()
20 {
21 txt11 = (t1.id + 1).ToString(),
22 txt22 = Guid.NewGuid().ToString("N")
23 };
24 db.Entry(t2).State = EntityState.Added;
25 db.SaveChanges();
26
27 //事务统一提交(若失败则统一回滚)
28 trans.Complete();
29 Console.WriteLine("自增提交成功");
30 }
31 catch (Exception)
32 {
33 Transaction.Current.Rollback();
34 }
35 }
36 }
37 }
5. 多个DBContext、多个数据库的情况。
1 private static void TransactionScopeTwoDB()2 {3 using (TransactionScope trans = new TransactionScope())4 {5 try6 {7 DbContext db1 = new CodeFirstModel();8 DbContext db2 = new CodeFirstModel2();9 //数据库1
10 TestInfor t1 = new TestInfor()
11 {
12 id = Guid.NewGuid().ToString("N"),
13 txt1 = "111111111",
14 txt2 = "222222222222"
15 };
16 db1.Entry(t1).State = EntityState.Added;
17 db1.SaveChanges();
18
19 //数据库2
20 TestOne t2 = new TestOne()
21 {
22 id = Guid.NewGuid().ToString("N"), //可以在此处手动制造个错误,来测试多数据库的回滚问题
23 t1 = "222",
24 t2 = "2222"
25 };
26 db2.Entry(t2).State = EntityState.Added;
27 db2.SaveChanges();
28
29 trans.Complete();
30 Console.WriteLine("多个数据库添加成功");
31 }
32 catch (Exception)
33 {
34 Transaction.Current.Rollback();
35 }
36
37 }
38 }