unitofworkspring 事务回滚 怎么回滚

自由、创新、研究、探索
Linux/Windows Mono/DotNet [ Open Source .NET Development/ 使用开源工具进行DotNet软件开发]锐意进取,志存高远.成就梦想,只争朝夕.从你开始,创新世界.【That I exist is a perpetual supprise which is life. Focus on eCommerce】
目录索引 
  本章我们来创建仓储类Repository 并且引入 UnitOfWork
我对UnitOfWork的一些理解
  &UnitOfWork 工作单元,对于这个的解释和实例,网上很多很多大神之作,我在这就不班门弄斧了,只是浅谈 一下个人理解,不一定对,希望大家指正:
  UnitOfWork 看了很多,个人理解就是 统一事务,在很多操作中我们可能是进行多项操作的,比如 同时保存两个表的数据,如果我们先保存一个表(SaveChanges()之后),再保存另一个表(SaveChanges())其实是两个不同的事务,通俗的讲,我们对数据库提交了两次数据。而UnitOfWork 就是 把本次操作(分别保存两张表)放在统一事务中,所有的CRUD完成后,统一提交数据,即保证数据的一致性,又减少了提交次数,提高性能。
  举个例子,我们之前的Repository的保存操作:
  Repository:
  public virtual bool Save(T entity)  {    _Context.Add(entity);    return _Context.SaveChanges() & 0;&  }
  Controller:&
  public ActionResult Save()
    var Users=new Users(){Id=1,UserName="张三"};
    var Logs=new Logs(){log="注册了用户张三"};
    _UserService.Save(Users);
    _LogService.Save(Logs);
因为本身EF就是带事务的,但是_Context.SaveChanges()之后,事务就结束了,也提交了数据。所以,上面的例子 应该是开启了两次事务,提交了两次数据(一次是Users 一次是 Logs)
这显然是不合理的,首先不说提交两次数据性能的问题,如果用户注册失败,那么日志也不应该添加,应该回滚。但是,显然,上面没有这么做。
所以有了UnitOfWork。
使用UnitOfWork之后,代码应该是这样的:
  UnitOfWork:
  public bool Commit()  {    return _Context.SaveChanges() & 0;&  }
  Repository:
  public virtual bool Save(T entity)  {    _Context.Add(entity);  }
  Controller:&
  public ActionResult Save()
    var Users=new Users(){Id=1,UserName="张三"};
    var Logs=new Logs(){log="注册了用户张三"};
    _UserService.Save(Users);
    _LogService.Save(Logs);
    _mit();
UnitOfWork接口和实现类
&我们在wkmvc.Core类库下,新建一个接口&IUnitOfWork:
1 namespace wkmvc.Core
/// &summary&
/// Describe:工作单元接口
/// Author:yuangang
/// Date:
/// Blogs:/yuangang
/// &/summary&
public interface IUnitOfWork
bool Commit();
  IUnitOfWork&实现类&UnitOfWork:
2 using wkmvc.D
4 namespace wkmvc.Core
/// &summary&
/// Describe:工作单元实现类
/// Author:yuangang
/// Date:
/// Blogs:/yuangang
/// &/summary&
public class UnitOfWork : IUnitOfWork, IDisposable
#region 数据上下文
/// &summary&
/// 数据上下文
/// &/summary&
private ApplicationDbContext _C
public UnitOfWork(ApplicationDbContext Context)
_Context = C
#endregion
public bool Commit()
return _Context.SaveChanges() & 0;
public void Dispose()
if(_Context!=null)
_Context.Dispose();
GC.SuppressFinalize(this);
  &这样,UnitOfWork&就完成了。下面我们来添加仓储:
仓储 IRepository、Repository
新建接口&IRepository&添加基本的操作方法:
2 using System.Linq.E
4 namespace wkmvc.Core
/// &summary&
/// Describe:仓储接口
/// Author:yuangang
/// Date:
/// Blogs:/yuangang
/// &/summary&
/// &typeparam name="T"&实体模型&/typeparam&
public interface IRepository&T& where T : class
#region 单模型 CRUD 操作
/// &summary&
/// 增加一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
bool Save(T entity, bool IsCommit = true);
/// &summary&
/// 更新一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
bool Update(T entity, bool IsCommit = true);
/// &summary&
/// 增加或更新一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsSave"&是否增加&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
bool SaveOrUpdate(T entity, bool IsSave, bool IsCommit = true);
/// &summary&
/// 通过Lamda表达式获取实体
/// &/summary&
/// &param name="predicate"&Lamda表达式(p=&p.Id==Id)&/param&
/// &returns&&/returns&
T Get(Expression&Func&T, bool&& predicate);
/// &summary&
/// 删除一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
bool Delete(T entity, bool IsCommit = true);
#endregion
  &实现类&Repository&:
1 using Microsoft.EntityFrameworkC
3 using System.L
4 using System.Linq.E
5 using wkmvc.D
7 namespace wkmvc.Core
/// &summary&
/// Describe:仓储实现类
/// Author:yuangang
/// Date:
/// Blogs:/yuangang
/// &/summary&
/// &typeparam name="T"&实体模型&/typeparam&
public abstract class Repository&T& : IRepository&T& where T : class
#region 数据上下文
/// &summary&
/// 数据上下文
/// &/summary&
private ApplicationDbContext _C
/// &summary&
/// 工作单元
/// &/summary&
UnitOfWork _UnitOfW
public Repository(ApplicationDbContext Context)
_Context = C
_UnitOfWork = new UnitOfWork(_Context);
#endregion
#region 单模型 CRUD 操作
/// &summary&
/// 增加一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
public virtual bool Save(T entity,bool IsCommit=true)
_Context.Add(entity);
if (IsCommit)
return _mit();
/// &summary&
/// 更新一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
public virtual bool Update(T entity, bool IsCommit = true)
_Context.Attach(entity);
_Context.Entry(entity).State = EntityState.M
if (IsCommit)
return _mit();
/// &summary&
/// 增加或更新一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsSave"&是否增加&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
public virtual bool SaveOrUpdate(T entity, bool IsSave, bool IsCommit = true)
return IsSave ? Save(entity, IsCommit) : Update(entity, IsCommit);
/// &summary&
/// 通过Lamda表达式获取实体
/// &/summary&
/// &param name="predicate"&Lamda表达式(p=&p.Id==Id)&/param&
/// &returns&&/returns&
public virtual T Get(Expression&Func&T, bool&& predicate)
return _Context.Set&T&().AsNoTracking().SingleOrDefault(predicate);
/// &summary&
/// 删除一条记录
/// &/summary&
/// &param name="entity"&实体模型&/param&
/// &param name="IsCommit"&是否提交(默认提交)&/param&
/// &returns&&/returns&
public virtual bool Delete(T entity, bool IsCommit = true)
if (entity == null)
_Context.Remove(entity);
if (IsCommit)
return _mit();
#endregion
我们都添加了一个&&bool IsCommit = true&参数,是为了方便,如果仅仅是进行一项操作,那就没必要使用 UnitOfWork 统一 直接 SaveChanages() 就行了
我们来看下使用:&
  //第一种  public bool TestOne()  {    var users = new SYS_USER() { UserName="张三",Account="zhangsan",Password="123456" };
    return _UserService.Save(users);  }
  //第二种  public bool TestTwo()  {    var users1 = new SYS_USER() { UserName = "张三", Account = "zhangsan", Password = "123456" };    var users2 = new SYS_USER() { UserName = "李四", Account = "lisi", Password = "456789" };    var users3 = new SYS_USER() { UserName = "王五", Account = "wangwu", Password = "321654" };
    _UserService.Save(users1);    _UserService.Save(users2);    _UserService.Save(users3);
    return _mit();  }
本篇主要是介绍用法,具体的解释清大家参考大神们的讲解,如有不对的地方希望指正。具体的实现代码,我们后面一起一点一点写。
希望跟大家一起学习Asp.net Core&
刚开始接触,水平有限,很多东西都是自己的理解和翻阅网上大神的资料,如果有不对的地方和不理解的地方,希望大家指正!
虽然Asp.net Core 现在很火热,但是网上的很多资料都是前篇一律的复制,所以有很多问题我也暂时没有解决,希望大家能共同帮助一下!
原创文章 转载请尊重劳动成果&
阅读(...) 评论()
随笔 - 14862
评论 - 1017EF Code First:Repository,UnitOfWork,DbContext(1)
EF Code First:Repository,UnitOfWork,DbContext(1)
  (四) 业务整合,实现登录功能
  为了在业务实现中方便调用 IUnitOfWork 进行多实体的单元操作,定义一个核心与业务实现基类
  namespace&GMF.Demo.Core.Impl &  { &  ///&&summary&   ///&核心业务实现基类   ///&&/summary&   publicabstractclass&CoreServiceBase &  &&&&{ &  ///&&summary&   ///&获取或设置&工作单元对象,用于处理同步业务的事务操作   ///&&/summary&   &&&&&&&&[Import] &  protected&IUnitOfWork&UnitOfWork&{&&&} &  &&&&} &  }&
  在核心业务实现中添加用到的各个实体Repository仓储操作接口对象的属性,并更新登录方法,使其调用仓储操作完成登录业务
  namespace&GMF.Demo.Core.Impl &  { &  ///&&summary&   ///&&&&&账户模块核心业务实现   ///&&/summary&   publicabstractclass&AccountService&:&CoreServiceBase,&IAccountContract &  &&&&{ &  &&&&&&&&#region&属性 &  &&&&&&&&#region&受保护的属性   ///&&summary&   ///&获取或设置&用户信息数据访问对象   ///&&/summary&   &&&&&&&&[Import] &  protected&IMemberRepository&MemberRepository&{&&&} &  ///&&summary&   ///&获取或设置&登录记录信息数据访问对象   ///&&/summary&   &&&&&&&&[Import] &  protected&ILoginLogRepository&LoginLogRepository&{&&&} &  &&&&&&&&#endregion &  &&&&&&&&#endregion   ///&&summary&   ///&用户登录   ///&&/summary&   ///&&param&name=&loginInfo&&登录信息&/param&   ///&&returns&业务操作结果&/returns&   publicvirtual&OperationResult&Login(LoginInfo&loginInfo) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(loginInfo,&&loginInfo&); &  &&&&&&&&&&&&Member&member&=&MemberRepository.Entities.SingleOrDefault(m&=&&m.UserName&==&loginInfo.Access&||&m.Email&==&loginInfo.Access); &  if&(member&==&null) &  &&&&&&&&&&&&{ &  returnnew&OperationResult(OperationResultType.QueryNull,&&指定账号的用户不存在。&); &  &&&&&&&&&&&&} &  if&(member.Password&!=&loginInfo.Password) &  &&&&&&&&&&&&{ &  returnnew&OperationResult(OperationResultType.Warning,&&登录密码不正确。&); &  &&&&&&&&&&&&} &  &&&&&&&&&&&&LoginLog&loginLog&=&new&LoginLog&{&IpAddress&=&loginInfo.IpAddress,&Member&=&member&}; &  &&&&&&&&&&&&LoginLogRepository.Insert(loginLog); &  returnnew&OperationResult(OperationResultType.Success,&&登录成功。&,&member); &  &&&&&&&&} &  &&&&} &  }&
  在此需要特别说明的是,为了保持项目结构的整洁与规范,实体数据操作的Repository与UnitOfWork只能被业务层(核心层及其派生层次)调用,而不能被业务层的上层调用,比如MVC项目中,不能被控制器调用,防止开发人员不遵守规范,在控制器中进行业务实现导致项目结构的混乱。因此给实体的Repository接口属性与业务基类中的UnitOfWork属性的可访问性都要设置为 protected,而不能为 public。
  在MVC的Web.Config中添加一个数据连接配置:
  &connectionStrings&  &addname=&default&connectionString=&Data&Source=.;&Integrated&Security=T&Initial&Catalog=DemoC&Pooling=T&MultipleActiveResultSets=T&providerName=&System.Data.SqlClient&/&  &/connectionStrings&
  至此,我们的项目结构变成了如下图所示:
  运行项目,尝试登录并成功,查看登录记录表,我们可以发现新增了一条登录记录:
  五、源码下载
  GMFrameworkForBlog3.zip
  为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 ">Entity Framework技术系列
  EF框架step by step
  这些系列都写得非常好,基本涵盖了EF的所有常见技术点。我再详写这些就显得多余了,但为了整个系列的完整性,还是要提上一提,不然后面就没法继续了。
  本篇会比较长,主要讲解UnitOfWork、Repository模式以及DbContext实现类的构造,来给架构demo添加数据访问功能,实现数据库验证的用户登录功能。
  二、Repository
  在数据库系统中,对于数据层来说,所有的操作归根结底无非&C(增加)、R(读取)、U(修改)、D(删除)&这四种操作。四种操作当中,与与业务相关度最大的是读取操作,根据各种不同的业务需求提交不同的查询,其最终执行应该放到业务层面中去进行,而增加,修改,删除这三种操作较为通用,可以作为通用数据操作封装到Repository中。在Repository中,唯一的变化点就是各种不同的实体类型,既然是变化点就应该进行封装,这里使用泛型来封装这个变化点。
  对于实体的查询操作,需要着重说明一下。EF是ORM,ORM最大的特点就是看到的使用的都是对象,读取一个对象,就相当于执行了一个&select * from table&的查询操作,稍微了解数据库的同学都知道,这是非常消耗性能与内存的,数据查询最好还是应该按需查询,要什么取什么。EF的查询操作如果不处理好,很容易造成巨大的性能问题,比如有些同学喜欢在Repository中定义诸如GetByName,GetByXX,GetByXXX的操作,这些操作通常并不是每个实体都需要,即使需要也很有局限性,最后这些操作大都伦为&鸡肋&,限制了使用场景,也不能按需查询。还有些同学定义考虑到了按条件查询,定义了比如GetByPredicate(predicate)的操作,但使用了IEnumerable&T&的返回值,相当于把这部分数据都加载到内存中,再进行后续操作。甚至还有定义 IEnumerable&T& GetAll(); 操作的,明白的同学都知道这是件多么恐怖的事,相当于把整个表的数据都加载到内存中,再进行后续操作!!!诸如此类,问题多多&&其实,数据查询,只要在Repository中定义一个只读的IQueryable&T&的数据集,其他的事就完全不用管了,业务层中想要什么就取什么就好了,当然,为了充分利用EF对主键查询的缓存特性,定义一个 GetByKey 的查询操作还是可以接受的。PS:关于EF数据查询的问题,后面还会专门有一篇文章来探讨。总之,EF的数据查询做不好,影响很大,这也是很多同学说EF性能不好的重要原因。
  不知不觉说了那么多,呵呵,主要是关于数据查询有太多的话想说了,但这不是本文的重点,严重跑题了。直接上接口定义吧:
  namespace&ponent.Data &  { &  ///&&summary&   ///&&&&&定义仓储模型中的数据标准操作   ///&&/summary&   ///&&typeparam&name=&TEntity&&动态实体类型&/typeparam&   publicinterface&IRepository&TEntity&&where&TEntity&:&Entity &  &&&&{ &  &&&&&&&&#region&属性   ///&&summary&   ///&&&&&获取&当前实体的查询数据集   ///&&/summary&   &&&&&&&&IQueryable&TEntity&&Entities&{&&} &  &&&&&&&&#endregion &  &&&&&&&&#region&公共方法   ///&&summary&   ///&&&&&插入实体记录   ///&&/summary&   ///&&param&name=&entity&&&实体对象&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   int&Insert(TEntity&entity,&bool&isSave&=&true); &  ///&&summary&   ///&&&&&批量插入实体记录集合   ///&&/summary&   ///&&param&name=&entities&&&实体记录集合&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   int&Insert(IEnumerable&TEntity&&entities,&bool&isSave&=&true); &  ///&&summary&   ///&&&&&删除指定编号的记录   ///&&/summary&   ///&&param&name=&id&&&实体记录编号&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   int&Delete(object&id,&bool&isSave&=&true); &  ///&&summary&   ///&&&&&删除实体记录   ///&&/summary&   ///&&param&name=&entity&&&实体对象&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   int&Delete(TEntity&entity,&bool&isSave&=&true); &  ///&&summary&   ///&&&&&删除实体记录集合   ///&&/summary&   ///&&param&name=&entities&&&实体记录集合&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   int&Delete(IEnumerable&TEntity&&entities,&bool&isSave&=&true); &  ///&&summary&   ///&&&&&删除所有符合特定表达式的数据   ///&&/summary&   ///&&param&name=&predicate&&&查询条件谓语表达式&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   int&Delete(Expression&Func&TEntity,&bool&&&predicate,&bool&isSave&=&true); &  ///&&summary&   ///&&&&&更新实体记录   ///&&/summary&   ///&&param&name=&entity&&&实体对象&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   int&Update(TEntity&entity,&bool&isSave&=&true); &  ///&&summary&   ///&&&&&查找指定主键的实体记录   ///&&/summary&   ///&&param&name=&key&&&指定主键&&/param&   ///&&returns&&符合编号的记录,不存在返回null&&/returns&   &&&&&&&&TEntity&GetByKey(object&key); &  &&&&&&&&#endregion   &&&&} &  }&
  还要说明一下,每个操作方法都带有一个 isSave 可选参数,是为了单个实体操作的需要,免去了每次都要调用 context.SaveChanged()的麻烦。如果是进行多个实体的单元事务操作,就需要把这个参数设置为 false 。
  Repository的通用实现如下:
  namespace&ponent.Data &  { &  ///&&summary&   ///&&&&&EntityFramework仓储操作基类   ///&&/summary&   ///&&typeparam&name=&TEntity&&动态实体类型&/typeparam&   publicabstractclass&EFRepositoryBase&TEntity&&:&IRepository&TEntity&&where&TEntity&:&Entity &  &&&&{ &  &&&&&&&&#region&属性   ///&&summary&   ///&&&&&获取&仓储上下文的实例   ///&&/summary&   &&&&&&&&[Import] &  public&IUnitOfWork&UnitOfWork&{&&&} &  ///&&summary&   ///&&&&&获取或设置&EntityFramework的数据仓储上下文   ///&&/summary&   protected&IUnitOfWorkContext&EFContext &  &&&&&&&&{ &  get  &&&&&&&&&&&&{ &  if&(UnitOfWork&is&IUnitOfWorkContext) &  &&&&&&&&&&&&&&&&{ &  return&UnitOfWork&as&IUnitOfWorkC &  &&&&&&&&&&&&&&&&} &  thrownew&DataAccessException(string.Format(&数据仓储上下文对象类型不正确,应为IUnitOfWorkContext,实际为&{0}&,&UnitOfWork.GetType().Name)); &  &&&&&&&&&&&&} &  &&&&&&&&} &  ///&&summary&   ///&&&&&获取&当前实体的查询数据集   ///&&/summary&   publicvirtual&IQueryable&TEntity&&Entities &  &&&&&&&&{ &  get&{&return&EFContext.Set&TEntity&();&} &  &&&&&&&&} &  &&&&&&&&#endregion &  &&&&&&&&#region&公共方法   ///&&summary&   ///&&&&&插入实体记录   ///&&/summary&   ///&&param&name=&entity&&&实体对象&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   publicvirtualint&Insert(TEntity&entity,&bool&isSave&=&true) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(entity,&&entity&); &  &&&&&&&&&&&&EFContext.RegisterNew(entity); &  return&isSave&?&mit()&:&0; &  &&&&&&&&} &  ///&&summary&   ///&&&&&批量插入实体记录集合   ///&&/summary&   ///&&param&name=&entities&&&实体记录集合&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   publicvirtualint&Insert(IEnumerable&TEntity&&entities,&bool&isSave&=&true) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(entities,&&entities&); &  &&&&&&&&&&&&EFContext.RegisterNew(entities); &  return&isSave&?&mit()&:&0; &  &&&&&&&&} &  ///&&summary&   ///&&&&&删除指定编号的记录   ///&&/summary&   ///&&param&name=&id&&&实体记录编号&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   publicvirtualint&Delete(object&id,&bool&isSave&=&true) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(id,&&id&); &  &&&&&&&&&&&&TEntity&entity&=&EFContext.Set&TEntity&().Find(id); &  return&entity&!=&null&?&Delete(entity,&isSave)&:&0; &  &&&&&&&&} &  ///&&summary&   ///&&&&&删除实体记录   ///&&/summary&   ///&&param&name=&entity&&&实体对象&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   publicvirtualint&Delete(TEntity&entity,&bool&isSave&=&true) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(entity,&&entity&); &  &&&&&&&&&&&&EFContext.RegisterDeleted(entity); &  return&isSave&?&mit()&:&0; &  &&&&&&&&} &  ///&&summary&   ///&&&&&删除实体记录集合   ///&&/summary&   ///&&param&name=&entities&&&实体记录集合&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   publicvirtualint&Delete(IEnumerable&TEntity&&entities,&bool&isSave&=&true) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(entities,&&entities&); &  &&&&&&&&&&&&EFContext.RegisterDeleted(entities); &  return&isSave&?&mit()&:&0; &  &&&&&&&&} &  ///&&summary&   ///&&&&&删除所有符合特定表达式的数据   ///&&/summary&   ///&&param&name=&predicate&&&查询条件谓语表达式&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   publicvirtualint&Delete(Expression&Func&TEntity,&bool&&&predicate,&bool&isSave&=&true) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(predicate,&&predicate&); &  &&&&&&&&&&&&List&TEntity&&entities&=&EFContext.Set&TEntity&().Where(predicate).ToL &  return&entities.Count&&&0&?&Delete(entities,&isSave)&:&0; &  &&&&&&&&} &  ///&&summary&   ///&&&&&更新实体记录   ///&&/summary&   ///&&param&name=&entity&&&实体对象&&/param&   ///&&param&name=&isSave&&&是否执行保存&&/param&   ///&&returns&&操作影响的行数&&/returns&   publicvirtualint&Update(TEntity&entity,&bool&isSave&=&true) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(entity,&&entity&); &  &&&&&&&&&&&&EFContext.RegisterModified(entity); &  return&isSave&?&mit()&:&0; &  &&&&&&&&} &  ///&&summary&   ///&&&&&查找指定主键的实体记录   ///&&/summary&   ///&&param&name=&key&&&指定主键&&/param&   ///&&returns&&符合编号的记录,不存在返回null&&/returns&   publicvirtual&TEntity&GetByKey(object&key) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(key,&&key&); &  return&EFContext.Set&TEntity&().Find(key); &  &&&&&&&&} &  &&&&&&&&#endregion   &&&&} &  }&
  实现类中所有操作最终都是通过单元操作来提交的,关于单元操作,马上就会讲到。
  三、UnitOfWork
  引入单元操作,主要是为了给各个实体维护一个共同的DbContext上下文对象,保证所有的操作都是在共同的上下文中进行的。EF的操作提交 context.SaveChanged() 默认就是事务性的,只要保证了当前的所有实体的操作都是在一个共同的上下文中进行的,就实现了事务操作了。
  在业务层中,各个实体的增删改操作都是通过各个实体的Repository进行的,只需要提供一个提交保存的功能作为最后调用,即可保证当前的提交是事务性的。因此定义给业务层引用的单元操作接口如下:
  namespace&ponent.Data &  { &  ///&&summary&   ///&&&&&业务单元操作接口   ///&&/summary&   publicinterface&IUnitOfWork &  &&&&{ &  &&&&&&&&#region&属性   ///&&summary&   ///&&&&&获取&当前单元操作是否已被提交   ///&&/summary&   bool&IsCommitted&{&&} &  &&&&&&&&#endregion &  &&&&&&&&#region&方法   ///&&summary&   ///&&&&&提交当前单元操作的结果   ///&&/summary&   ///&&returns&&/returns&   int&Commit(); &  ///&&summary&   ///&&&&&把当前单元操作回滚成未提交状态   ///&&/summary&   void&Rollback(); &  &&&&&&&&#endregion   &&&&} &  }&
  在数据组件内部,数据操作最终都提交到一个与IUnitOfWork接口的实现类中进行操作,以保证各个实体的Repository与IUnitOfWork使用的是同一个DbContext上下文。定义数据单元操作接口如下:
  namespace&ponent.Data &  { &  ///&&summary&   ///&&&&&数据单元操作接口   ///&&/summary&   publicinterface&IUnitOfWorkContext&:&IUnitOfWork,&IDisposable &  &&&&{ &  ///&&summary&   ///&&&为指定的类型返回&System.Data.Entity.DbSet,这将允许对上下文中的给定实体执行&CRUD&操作。   ///&&/summary&   ///&&typeparam&name=&TEntity&&&应为其返回一个集的实体类型。&&/typeparam&   ///&&returns&&给定实体类型的&System.Data.Entity.DbSet&实例。&&/returns&   &&&&&&&&DbSet&TEntity&&Set&TEntity&()&where&TEntity&:&E &  ///&&summary&   ///&&&注册一个新的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entity&&&要注册的对象&&/param&   void&RegisterNew&TEntity&(TEntity&entity)&where&TEntity&:&E &  ///&&summary&   ///&&&批量注册多个新的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entities&&&要注册的对象集合&&/param&   void&RegisterNew&TEntity&(IEnumerable&TEntity&&entities)&where&TEntity&:&E &  ///&&summary&   ///&&&注册一个更改的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entity&&&要注册的对象&&/param&   void&RegisterModified&TEntity&(TEntity&entity)&where&TEntity&:&E &  ///&&summary&   ///&&&注册一个删除的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entity&&&要注册的对象&&/param&   void&RegisterDeleted&TEntity&(TEntity&entity)&where&TEntity&:&E &  ///&&summary&   ///&&&批量注册多个删除的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entities&&&要注册的对象集合&&/param&   void&RegisterDeleted&TEntity&(IEnumerable&TEntity&&entities)&where&TEntity&:&E &  &&&&} &  }&
  在单元操作的实现基类中,定义一个只读的DbContext抽象属性,实际的DbContext上下文需要在实现类中进行重写赋值。
  namespace&ponent.Data &  { &  ///&&summary&   ///&&&&&单元操作实现   ///&&/summary&   publicabstractclass&UnitOfWorkContextBase&:&IUnitOfWorkContext &  &&&&{ &  ///&&summary&   ///&获取&当前使用的数据访问上下文对象   ///&&/summary&   protectedabstract&DbContext&Context&{&&} &  ///&&summary&   ///&&&&&获取&当前单元操作是否已被提交   ///&&/summary&   publicbool&IsCommitted&{&&&} &  ///&&summary&   ///&&&&&提交当前单元操作的结果   ///&&/summary&   ///&&returns&&/returns&   publicint&Commit() &  &&&&&&&&{ &  if&(IsCommitted) &  &&&&&&&&&&&&{ &  return&0; &  &&&&&&&&&&&&} &  try  &&&&&&&&&&&&{ &  int&result&=&Context.SaveChanges(); &  &&&&&&&&&&&&&&&&IsCommitted&=& &  return& &  &&&&&&&&&&&&} &  catch&(DbUpdateException&e) &  &&&&&&&&&&&&{ &  if&(e.InnerException&!=&null&&&&e.InnerException.InnerException&is&SqlException) &  &&&&&&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&&&&&SqlException&sqlEx&=&e.InnerException.InnerException&as&SqlE &  string&msg&=&DataHelper.GetSqlExceptionMessage(sqlEx.Number); &  throw&PublicHelper.ThrowDataAccessException(&提交数据更新时发生异常:&&+&msg,&sqlEx); &  &&&&&&&&&&&&&&&&} &   &  &&&&&&&&&&&&} &  &&&&&&&&} &  ///&&summary&   ///&&&&&把当前单元操作回滚成未提交状态   ///&&/summary&   publicvoid&Rollback() &  &&&&&&&&{ &  &&&&&&&&&&&&IsCommitted&=& &  &&&&&&&&} &  publicvoid&Dispose() &  &&&&&&&&{ &  if&(!IsCommitted) &  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&Commit(); &  &&&&&&&&&&&&} &  &&&&&&&&&&&&Context.Dispose(); &  &&&&&&&&} &  ///&&summary&   ///&&&为指定的类型返回&System.Data.Entity.DbSet,这将允许对上下文中的给定实体执行&CRUD&操作。   ///&&/summary&   ///&&typeparam&name=&TEntity&&&应为其返回一个集的实体类型。&&/typeparam&   ///&&returns&&给定实体类型的&System.Data.Entity.DbSet&实例。&&/returns&   public&DbSet&TEntity&&Set&TEntity&()&where&TEntity&:&Entity &  &&&&&&&&{ &  return&Context.Set&TEntity&(); &  &&&&&&&&} &  ///&&summary&   ///&&&&&注册一个新的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entity&&&要注册的对象&&/param&   publicvoid&RegisterNew&TEntity&(TEntity&entity)&where&TEntity&:&Entity &  &&&&&&&&{ &  &&&&&&&&&&&&EntityState&state&=&Context.Entry(entity).S &  if&(state&==&EntityState.Detached) &  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&Context.Entry(entity).State&=&EntityState.A &  &&&&&&&&&&&&} &  &&&&&&&&&&&&IsCommitted&=& &  &&&&&&&&} &  ///&&summary&   ///&&&&&批量注册多个新的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entities&&&要注册的对象集合&&/param&   publicvoid&RegisterNew&TEntity&(IEnumerable&TEntity&&entities)&where&TEntity&:&Entity &  &&&&&&&&{ &  try  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&Context.Configuration.AutoDetectChangesEnabled&=& &  foreach&(TEntity&entity&in&entities) &  &&&&&&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&&&&&RegisterNew(entity); &  &&&&&&&&&&&&&&&&} &  &&&&&&&&&&&&} &  finally  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&Context.Configuration.AutoDetectChangesEnabled&=& &  &&&&&&&&&&&&} &  &&&&&&&&} &  ///&&summary&   ///&&&&&注册一个更改的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entity&&&要注册的对象&&/param&   publicvoid&RegisterModified&TEntity&(TEntity&entity)&where&TEntity&:&Entity &  &&&&&&&&{ &  if&(Context.Entry(entity).State&==&EntityState.Detached) &  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&Context.Set&TEntity&().Attach(entity); &  &&&&&&&&&&&&} &  &&&&&&&&&&&&Context.Entry(entity).State&=&EntityState.M &  &&&&&&&&&&&&IsCommitted&=& &  &&&&&&&&} &  ///&&summary&   ///&&&注册一个删除的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entity&&&要注册的对象&&/param&   publicvoid&RegisterDeleted&TEntity&(TEntity&entity)&where&TEntity&:&Entity &  &&&&&&&&{ &  &&&&&&&&&&&&Context.Entry(entity).State&=&EntityState.D &  &&&&&&&&&&&&IsCommitted&=& &  &&&&&&&&} &  ///&&summary&   ///&&&批量注册多个删除的对象到仓储上下文中   ///&&/summary&   ///&&typeparam&name=&TEntity&&&要注册的类型&&/typeparam&   ///&&param&name=&entities&&&要注册的对象集合&&/param&   publicvoid&RegisterDeleted&TEntity&(IEnumerable&TEntity&&entities)&where&TEntity&:&Entity &  &&&&&&&&{ &  try  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&Context.Configuration.AutoDetectChangesEnabled&=& &  foreach&(TEntity&entity&in&entities) &  &&&&&&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&&&&&RegisterDeleted(entity); &  &&&&&&&&&&&&&&&&} &  &&&&&&&&&&&&} &  finally  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&Context.Configuration.AutoDetectChangesEnabled&=& &  &&&&&&&&&&&&} &  &&&&&&&&} &  &&&&} &  }&
  实体数据操作中,需要特别说明一下的是批量操作。EF不支持批量操作(直接执行SQL语句的方式除外),但我们可以使用多次变更一次提交的方式来进行批量的插入,删除等操作。在进行数据的变更时,EF默认会自动的跟踪数据的变化(AutoDetectChangesEnabled = true),当变更的数据量较大的时候,EF的跟踪工作量就会骤增,使指定操作变得非常缓慢(这也是部分同学怀疑EF的性能问题的一个怀疑点),其实,只要在批量操作的时候把自动跟踪关闭(AutoDetectChangesEnabled = false),即可解决缓慢的问题。如以上代码 144 行与 152 行所示。
  特别说明:本文的UnitOfWork实现方案参考了 dax.net 的 《深度剖析Byteart Retail案例:仓储(Repository)及其上下文(Repository Context)》,在此表示感谢。
  至此,我们回顾一下解决方案的结构:
  与业务实体无关的数据组件 ponent.Data 已经搭建完成,下面的工作将与业务实体密切相关。
  四、业务整合应用
  与业务实体相关的数据功能代码定义在 GMF.Demo.Core.Data 项目中。
  (一) 业务实体的仓储实现
  首先,实现各个实体的 Repository 仓储操作,这里只以 用户信息(Member)实体为例:
  namespace&GMF.Demo.Core.Data.Repositories &  { &  ///&&summary&   ///&&&&&仓储操作接口&&用户信息   ///&&/summary&   publicinterface&IMemberRepository&:&IRepository&Member&&{&} &  }&
  namespace&GMF.Demo.Core.Data.Repositories.Impl &  { &  ///&&summary&   ///&&&&&仓储操作实现&&用户信息   ///&&/summary&   &&&&[Export(typeof(IMemberRepository))] &  publicclass&MemberRepository&:&EFRepositoryBase&Member&,&IMemberRepository&{&} &  }&
  可以发现,通用仓储操作在数据组件中封装好后,在实际业务中只需要编写非常少量的代码即可实现各个实体的仓储操作,这就是封装的好处。
  (二) 数据上下文实现
  DbContext上下文的实现,这里先使用微软官方示例中的传统方案:
  namespace&GMF.Demo.Core.Data.Context &  { &  ///&&summary&   ///&&&&&Demo项目数据访问上下文   ///&&/summary&   &&&&[Export(typeof&(DbContext))] &  publicclass&DemoDbContext&:&DbContext &  &&&&{ &  &&&&&&&&#region&构造函数   ///&&summary&   ///&&&&&初始化一个&使用连接名称为&default&的数据访问上下文类&的新实例   ///&&/summary&   public&DemoDbContext() &  &&&&&&&&&&&&:&base(&default&)&{&} &  ///&&summary&   ///&初始化一个&使用指定数据连接名称或连接串&的数据访问上下文类&的新实例   ///&&/summary&   public&DemoDbContext(string&nameOrConnectionString) &  &&&&&&&&&&&&:&base(nameOrConnectionString)&{&&} &  &&&&&&&&#endregion &  &&&&&&&&#region&属性   public&DbSet&Role&&Roles&{&&&} &  public&DbSet&Member&&Members&{&&&} &  public&DbSet&MemberExtend&&MemberExtends&{&&&} &  public&DbSet&LoginLog&&LoginLogs&{&&&} &  &&&&&&&&#endregion   protectedoverridevoid&OnModelCreating(DbModelBuilder&modelBuilder) &  &&&&&&&&{ &  //移除一对多的级联删除约定,想要级联删除可以在&EntityTypeConfiguration&TEntity&的实现类中进行控制   &&&&&&&&&&&&modelBuilder.Conventions.Remove&OneToManyCascadeDeleteConvention&(); &  //多对多启用级联删除约定,不想级联删除可以在删除前判断关联的数据进行拦截   //modelBuilder.Conventions.Remove&ManyToManyCascadeDeleteConvention&();   &&&&&&&&} &  &&&&} &  }&
  值得注意的是,在EF中提供了很多的规则来定义 EF生成数据库 的行为,如下图所示:
  我们可以在 DbContext 的派生类中重写 OnModelCreating 方法来移除一些规则来达到某些需求,比如在此,我们移除&OneToManyCascadeDeleteConvention 来达到禁用数据库的 一对多的级联删除 ,需要时再在做实体映射时启用,就能防止由于误操作而导致实体相关的数据都被删除的情况。
  当然,这里只定义的DbContext上下文的实现类,并没有进行使用,如何来使用这个上下文呢,只需实现一个单元操作实现类即可:
  namespace&GMF.Demo.Core.Data.Context &  { &  ///&&summary&   ///&&&&&Demo项目单元操作类   ///&&/summary&   &&&&[Export(typeof&(IUnitOfWork))] &  publicclass&DemoUnitOfWorkContext&:&UnitOfWorkContextBase &  &&&&{ &  ///&&summary&   ///&&&&&获取或设置&当前使用的数据访问上下文对象   ///&&/summary&   protectedoverride&DbContext&Context &  &&&&&&&&{ &  get&{&return&DemoDbC&} &  &&&&&&&&} &  ///&&summary&   ///&&&&&获取或设置&默认的Demo项目数据访问上下文对象   ///&&/summary&   &&&&&&&&[Import(typeof&(DbContext))] &  public&DemoDbContext&DemoDbContext&{&&&} &  &&&&} &  }&
  (三) 创建数据库时的数据初始化
  在本系列前面的示例中,由于没有数据访问功能,使用了一个数据来模拟数据源,现在,我们可以通过创建数据库时初始化一些数据,以使得在下面的操作中有数据可用。
  EF创建数据库的策略同样有多种可选:
  由各个策略类的名称即可猜到具体的作用(可见代码的语义化很重要),这里使用CreateDatabaseIfNotExists,即数据库不存在时创建。
  namespace&GMF.Demo.Core.Data.Initialize &  { &  ///&&summary&   ///&数据库初始化策略   ///&&/summary&   publicclass&SampleData&:&CreateDatabaseIfNotExists&DemoDbContext& &  &&&&{ &  protectedoverridevoid&Seed(DemoDbContext&context) &  &&&&&&&&{ &  &&&&&&&&&&&&List&Member&&members&=&new&List&Member& &  &&&&&&&&&&&&{ &  new&Member&{&UserName&=&&admin&,&Password&=&&123456&,&Email&=&&&,&NickName&=&&管理员&&}, &  new&Member&{&UserName&=&&gmfcn&,&Password&=&&123456&,&Email&=&&mf.&,&NickName&=&&郭明锋&&} &  &&&&&&&&&&&&}; &  &&&&&&&&&&&&DbSet&Member&&memberSet&=&context.Set&Member&(); &  &&&&&&&&&&&&members.ForEach(m&=&&memberSet.Add(m)); &  &&&&&&&&&&&&context.SaveChanges(); &  &&&&&&&&} &  &&&&} &  }&
  为了不使业务代码与EF的耦合过大,我们规定:项目中对 EntityFramework.dll 的引用到 GMF.Demo.Core.Data项目止,再上层的代码不依赖于EF组件。因此还需要在这定义一个执行初始化的辅助类:
  namespace&GMF.Demo.Core.Data.Initialize &  { &  ///&&summary&   ///&数据库初始化操作类   ///&&/summary&   publicstaticclass&DatabaseInitializer &  &&&&{ &  ///&&summary&   ///&数据库初始化   ///&&/summary&   publicstaticvoid&Initialize(&) &  &&&&&&&&{ &  &&&&&&&&&&&&Database.SetInitializer(new&SampleData()); &  using&(var&db&=&new&DemoDbContext()) &  &&&&&&&&&&&&{ &  &&&&&&&&&&&&&&&&db.Database.Initialize(false); &  &&&&&&&&&&&&} &  &&&&&&&&} &  &&&&} &  }&
  此辅助类将在程序初始化时调用,网站项目里,在Global的Application_Start方法中调用:
  (四) 业务整合,实现登录功能
  为了在业务实现中方便调用 IUnitOfWork 进行多实体的单元操作,定义一个核心与业务实现基类
  namespace&GMF.Demo.Core.Impl &  { &  ///&&summary&   ///&核心业务实现基类   ///&&/summary&   publicabstractclass&CoreServiceBase &  &&&&{ &  ///&&summary&   ///&获取或设置&工作单元对象,用于处理同步业务的事务操作   ///&&/summary&   &&&&&&&&[Import] &  protected&IUnitOfWork&UnitOfWork&{&&&} &  &&&&} &  }&
  在核心业务实现中添加用到的各个实体Repository仓储操作接口对象的属性,并更新登录方法,使其调用仓储操作完成登录业务
  namespace&GMF.Demo.Core.Impl &  { &  ///&&summary&   ///&&&&&账户模块核心业务实现   ///&&/summary&   publicabstractclass&AccountService&:&CoreServiceBase,&IAccountContract &  &&&&{ &  &&&&&&&&#region&属性 &  &&&&&&&&#region&受保护的属性   ///&&summary&   ///&获取或设置&用户信息数据访问对象   ///&&/summary&   &&&&&&&&[Import] &  protected&IMemberRepository&MemberRepository&{&&&} &  ///&&summary&   ///&获取或设置&登录记录信息数据访问对象   ///&&/summary&   &&&&&&&&[Import] &  protected&ILoginLogRepository&LoginLogRepository&{&&&} &  &&&&&&&&#endregion &  &&&&&&&&#endregion   ///&&summary&   ///&用户登录   ///&&/summary&   ///&&param&name=&loginInfo&&登录信息&/param&   ///&&returns&业务操作结果&/returns&   publicvirtual&OperationResult&Login(LoginInfo&loginInfo) &  &&&&&&&&{ &  &&&&&&&&&&&&PublicHelper.CheckArgument(loginInfo,&&loginInfo&); &  &&&&&&&&&&&&Member&member&=&MemberRepository.Entities.SingleOrDefault(m&=&&m.UserName&==&loginInfo.Access&||&m.Email&==&loginInfo.Access); &  if&(member&==&null) &  &&&&&&&&&&&&{ &  returnnew&OperationResult(OperationResultType.QueryNull,&&指定账号的用户不存在。&); &  &&&&&&&&&&&&} &  if&(member.Password&!=&loginInfo.Password) &  &&&&&&&&&&&&{ &  returnnew&OperationResult(OperationResultType.Warning,&&登录密码不正确。&); &  &&&&&&&&&&&&} &  &&&&&&&&&&&&LoginLog&loginLog&=&new&LoginLog&{&IpAddress&=&loginInfo.IpAddress,&Member&=&member&}; &  &&&&&&&&&&&&LoginLogRepository.Insert(loginLog); &  returnnew&OperationResult(OperationResultType.Success,&&登录成功。&,&member); &  &&&&&&&&} &  &&&&} &  }&
  在此需要特别说明的是,为了保持项目结构的整洁与规范,实体数据操作的Repository与UnitOfWork只能被业务层(核心层及其派生层次)调用,而不能被业务层的上层调用,比如MVC项目中,不能被控制器调用,防止开发人员不遵守规范,在控制器中进行业务实现导致项目结构的混乱。因此给实体的Repository接口属性与业务基类中的UnitOfWork属性的可访问性都要设置为 protected,而不能为 public。
  在MVC的Web.Config中添加一个数据连接配置:
  &connectionStrings&  &addname=&default&connectionString=&Data&Source=.;&Integrated&Security=T&Initial&Catalog=DemoC&Pooling=T&MultipleActiveResultSets=T&providerName=&System.Data.SqlClient&/&  &/connectionStrings&
  至此,我们的项目结构变成了如下图所示:
  运行项目,尝试登录并成功,查看登录记录表,我们可以发现新增了一条登录记录:
  五、源码下载
  GMFrameworkForBlog3.zip
  为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 ,地址为:
H3C认证Java认证Oracle认证
基础英语软考英语项目管理英语职场英语
.NETPowerBuilderWeb开发游戏开发Perl
二级模拟试题一级模拟试题一级考试经验四级考试资料
港口与航道工程建设工程法规及相关知识建设工程经济考试大纲矿业工程市政公用工程通信与广电工程
操作系统汇编语言计算机系统结构人工智能数据库系统微机与接口
软件测试软件外包系统分析与建模敏捷开发
法律法规历年试题软考英语网络管理员系统架构设计师信息系统监理师
高级通信工程师考试大纲设备环境综合能力
路由技术网络存储无线网络网络设备
CPMP考试prince2认证项目范围管理项目配置管理项目管理案例项目经理项目干系人管理
Powerpoint教程WPS教程
电子政务客户关系管理首席信息官办公自动化大数据
职称考试题目
就业指导签约违约职业测评
招生信息考研政治
网络安全安全设置工具使用手机安全
3DMax教程Flash教程CorelDraw教程Director教程
Dreamwaver教程HTML教程网站策划网站运营Frontpage教程
生物识别传感器物联网传输层物联网前沿技术物联网案例分析
互联网电信IT业界IT生活
Java核心技术J2ME教程
Linux系统管理Linux编程Linux安全AIX教程
Windows系统管理Windows教程Windows网络管理Windows故障
组织运营财务资本
视频播放文件压缩杀毒软件输入法微博
数据库开发Sybase数据库Informix数据库
&&&&&&&&&&&&&&&
希赛网 版权所有 & &&

我要回帖

更多关于 spring 事务回滚 的文章

 

随机推荐