上期回顾
在上一篇中,我介绍了自己在开发伊始时所做的一些事情,主要包括需求分析、进度安排和概要设计。在这个过程中有过迷茫和纠结,但很庆幸最后得到了想要的结果。
在本篇中,我会重点介绍下数据层的搭建。该层位于整个架构的最底层,只有确保数据层的健壮性,才能为整个项目提供正常运行的保证。OK,废话少说,Let's go!
创建项目,建立框架
先新建一个空白的solution文件,然后创建三个文件夹,分别叫Charley.Blog.UI、Charley.Blog.BLL、Charley.Blog.DAL。顾名思义,UI文件夹主要用于存放和表示层相关的代码,BLL文件夹则用于存放业务逻辑相关的代码,而与数据层相关的一切代码则保存在DAL文件夹中。整体的效果如下图所示:
图 1:解决方案
我在上一篇中曾提到采用ASP.NET MVC模式来构建表示层,所以在UI文件夹中直接创建ASP.NET MVC 3 Web应用程序。BLL.ServiceCore中存储业务逻辑的接口代码,而具体的实现则由BLL.Service负责。DAL层是今天的主角,稍后将详细介绍。
数据层
啰嗦了这么多,终于要开始介绍数据层了。在进入正题前,还是请大家先思考两个问题:1. 数据层提供哪些服务? 2. 说说你心目中良好设计的数据层应该符合哪些条件呢?
答1:CRUD操作、事务管理、延迟加载
答2:数据库的独立性(不依赖于任何的数据库,可以在多个数据库间任意切换)、OO(能够使用OO的方式进行CRUD的操作)
我的答案,欢迎拍砖(只要大家选中上面的空白区域,答案便将奇迹般地出现)
什么是数据库的独立性?试想你去菜场买菜,如果让你直接和种菜的农民伯伯联系,价格上可能便宜了,但是万一哪天这个农民伯伯有事儿,那恐怕那天的饭桌上就要冷冷清清了。因此为了减小这种潜在的风险,我就会选择找卖菜的小贩。中间多了一层,价格上虽然上去了,但是我再也不用去担心农民伯伯的事情了(这里有个前提:小贩是相对固定的,可以保证永远提供服务)。回到数据库的话题上来,农民伯伯相当于具体的数据库,消费者则是上层的业务层,小贩则是这两者之前的抽象层。业务层永远不需要去担心数据库是MySql,还是Sql Server,更或者是NoSql等,它只需要告诉抽象层:“我要100条用户数据!”,然后就等着接收吧。
图 2:数据库的独立性,虚线框起来的就是抽象层,大家也可以查看Repository的官方定义
OO这个东东,相信只要是做过软件开发都认识,用这种方式去操作数据库更直观也更容易维护,实现这种OO的方式就是采用ORM(对象关系映射)。
图 3:加上了ORM之后
事务也是必不可少的。Repository主要提供的是CRUD这几个最基本的原子操作,而在实际项目中,为实现一个功能,往往就需要使用多个原子操作。比如“删除文章”这个功能,就至少需要“删除文章"、“删除相关评论”这两个操作。试想我们在成功的完成了第一个操作,而第二个操作却失败了,会得到什么结果? 这也就是为什么我们需要提供事务的功能。事务为我们提供了逻辑上的原子性,只要其中有一个操作失败,整个事务就会回滚到最初的状况,这样我们就可以确保不会向数据库提交脏数据了。但是,“事务”本身的实现逻辑高度依赖于具体数据库,这样就违反了上面说的数据库独立性。因此,我尝试使用了UnitofWork(工作单元)模式,如下图所示:
图 4:加上UnitofWork之后
通过上面的叙述,希望大家已经基本清楚了在Charley Blog项目中数据层各项目之间的联系。接下来,我通过代码展示下实际使用过程中的应用:
ORM:目前在.NET家族中存在的ORM框架有EntityFramework、NHibernat、LinqToSql等等,在本项目中使用的是EntityFramework 5.0。代码位于Model模块中:
public class BaseModel { [Key] public Guid ID { get; set; } } public class Comment : BaseModel { [ForeignKey("Author")] public Guid AuthorID { get; set; } [ForeignKey("AuthorID")] [Required] public virtual Author Author { get; set; } [Required] public string Content { get; set; } //省略部分代码 } public class BlogContext : DbContext { public DbSet<Post> Posts { get; set; } public DbSet<Comment> Comments { get; set; } public DbSet<Author> Authors { get; set; } //...省略部分代码 protected override void OnModelCreating(DbModelBuilder modelBuilder) { Database.SetInitializer<BlogContext>(new BlogInitializer()); } }
Repository:在实际代码中,使用泛型接口来为每个实体提供了一个属于该实体的Repository。又因为ORM框架有许多,所以通过依赖抽象而非具体的EntityFramework来进行解耦。IGenericRepository便是这个抽象。
public interface IGenericRepository<T> : IDisposable where T : class{ IQueryable<T> GetAll(); IQueryable<T> GetAll(Expression<Func<T, bool>> predicate); T GetSingle(Expression<Func<T, bool>> predicate); T GetSingleByKey(params object[] keys); void Add(T entity); void Delete(params object[] keys); void Delete(T entity); void DeleteAll(); int DeleteBy(Expression<Func<T, bool>> predicate); void Update(T entity); } public abstract class GenericRepositoryForEF<C, T> : IGenericRepository<T> where T : BaseModel where C : DbContext, new() { protected C _context; protected DbSet<T> _dbSet; public GenericRepositoryForEF(C context) { if (context == null) { throw new ArgumentNullException("context"); } _context = context; _dbSet = _context.Set<T>(); } public virtual IQueryable<T> GetAll() { return _dbSet.AsNoTracking(); } public virtual void Add(T entity) { _dbSet.Add(entity); } public virtual void Delete(params object[] keys) { T entity = _dbSet.Find(keys); Delete(entity); } public virtual void Save() { _context.SaveChanges(); } //...省略部分代码 }
UnitofWork:UnitofWork通过持有对DBContext的引用,来统一控制数据的提交和回滚。
public interface IUnitofWork : IDisposable { bool CommitChangesWhenDisposing { get; } void RollbackChanges(); IGenericRepository<T> GetRepository<T>() where T : BaseModel; void Commit(); } public class UnitofWorkForEF : IUnitofWork{ #region Fields private BlogContext _context; private Dictionary<Type, object> _repositories; #endregion #region Properties public bool CommitChangesWhenDisposing { get; set; } #endregion public UnitofWorkForEF() { _context = new BlogContext(); _repositories = new Dictionary<Type, object>(); } #region Methods public virtual IGenericRepository<T> GetRepository<T>() where T : BaseModel { Type type = typeof(T); if (!_repositories.ContainsKey(type)) { if (type == typeof(Post)) { IGenericRepository<Post> _repo = new PostRepository<BlogContext>(_context); _repositories[type] = _repo; } else { IGenericRepository<T> _repo = new BaseRepository<BlogContext, T>(_context); _repositories[type] = _repo; } } return _repositories[type] as IGenericRepository<T>; } public virtual void Commit() { _context.SaveChanges();//提交到数据库 } public virtual void RollbackChanges() { _context.ChangeTracker.Entries().ToList().ForEach(entity => entity.State = System.Data.EntityState.Unchanged); //回滚 } #endregion }
最后,再给出一段业务层的示例来看一下,是如何使用UnitofWork和Repository的:
public InformationCode DeleteArticle(Guid id) { try { Post article = _repoPost.GetSingle(p => p.ID == id); _repoCmt.DeleteBy(cmt => cmt.PostID == id); //删除评论 _repoPost.DeleteBy(post => post.ID == id); //删除文章 UoW.Commit(); //提交到数据库 return InformationCode.MSG_ARTICLE_DELETE_SUCCESS; } catch { UoW.RollbackChanges(); //如果提交失败,则回滚 throw; } }
上面的代码中,大家会发现多个Class合并在一个文件中,这个是为了演示所需。在实际开发中,建议大家每个类写在单独的文件中,以便于维护。
如果大家在阅读过程中发现任何问题,欢迎留言,本人一定会在第一时间给予答复。