LINQ快速开发设计最佳实践(三) LINQ数据访问与业务逻辑层对象模板
一.摘要
LINQ在项目架构中的角色层次到底是什么? DataCotext对象应该放在哪里?层次间如何调用?在项目设计中我就不停的思考这些问题, 其实最后确定下来LINQ的结构体系还是在几个版本的尝试后,其间经过了几次论证和修改.作为本系列文章主题内容的收尾之作, 分享一下我的思考,以及我设计的LINQ业务逻辑对象与数据访问对象模板代码.
二.当三层架构遇上LINQ
传统的三层架构我不需要多介绍: UI(Web)层, 业务逻辑层, 数据访问层. 其中可能还有模型层,公共层等辅助层.
数据访问层是和数据持久化打交道的.打交道的对象不仅仅是数据库.数据库仅仅是数据持久化的一种方式而已. 当我们使用LINQ的时候, 往往使用LINQ TO SQL 来实现ORM, 上一章我技巧性的介绍了如何创建一个我认为标准的LINQ TO SQL的Model对象. 此后我们都是通过操作Model和DataContext对象实现对数据库的操作. 所以我曾想舍弃数据访问层, 有了LINQ TO SQL觉得不在需要这个层次了.LINQ的语法更接近业务对象查询的逻辑.感觉直接在业务逻辑层使用Model对象进行LINQ查询就可以了. 但是又考虑或者Model层也许应该改名叫数据访问层, 因为是它在和数据库打交道.又或者......为了广大程序员的精神安全, 在此省略万字.总之最后我的实践结论是:
1.Model层不可少, 用于存放我们创建的实现了ORM的Model对象.
2.数据访问层不可少. DataContext对象应该放置在数据访问层. 每一个表至少都有一个数据访问层对象.每个表都只能负责自己的基础的CURD操作.其中逻辑的实现是使用LINQ语法操作Model对象实现.
3.业务逻辑层中, 可以存在两个数据访问层对象(即数据库中的两个表)对应一个业务逻辑对象的情况,因为业务逻辑对象有时候需要进行业务抽象.比如标签和标签名字语言表, 完全可以抽象为一个"标签业务逻辑对象".
注: 这一条在我的项目中没有实现, 我的每一个数据库表都有一个业务逻辑对象.
4.业务逻辑层对象之间不能相互调用.但是业务逻辑对象可以调用任何数据访问层对象.
5.数据访问层对象之间不能相互调用.
总有人怀疑业务逻辑层和数据访问层存在的必要性, 觉得他们两者应该合并.但是我相信有经验的开发人员都很清楚, 这两者职责分明逻辑清楚,都是必不可少的. 比如要实现程序的多数据库支持就可以使用依赖注入的方式,创建多个数据库的数据访问层,然后在初始化时选择性加载.
大家可能还经常碰到这种情况:业务逻辑层很多的方法都是单纯的返回数据访问层对象的方法数据, 只有一个"return"语句.我觉得这是一个颗粒度上面的问题.首先要明确数据访问层才是最细颗粒度的, 其实最细颗粒度的方法我们在程序中是很少直接使用的, 都是要增加很多的逻辑在里面. 所以在创建业务逻辑层对象的时候, 如果一个最基础的方法可能根本不会被UI层调用, 则没必要创建. 但是在业务逻辑层创建了所有的最细颗粒度的方法, 我觉得也不算错. 一般我将这两类方法使用过两个region折叠块分离, 也还算代码清晰.
三.实例简介
TagItem是我们常见的"标签", 在博客园发表文章的时候也可以为一篇文章添加多个标签.假设数据库中有这么两个表, 一个是TagItem主表,另一个是语言表.对于现实世界一个"标签"表示的抽象, 可以有多种语言命名.
四.数据访问层模板.
下面是一个完整的TagItemLanguage表的的数据访问层对象代码. 对于TagItem表,我们只需要批量替换将"TagItemLanguage"替换成"TagItem"即可, 其实更科学的是使用模板生成工具比如CodeSmith自动生成数据访问类的代码.
/*************************************************************************************** * * * * File Name : TagItemLanguageDA.cs * * Creator : ziqiu.zhang * * Create Time : 2008-11-10 * * Functional Description : TagItemLanguage数据访问类 * * Remark : * * * * Copyright (c) eLong Corporation. All rights reserved. * ***************************************************************************************/ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Linq; using Com.Elong.Model.Tag; using Com.Elong.Model.Common; using Com.Elong.Common.Comment; namespace Com.Elong.DataAccess.Tag { /// <summary> /// TagItemLanguage数据访问类 /// </summary> public class TagItemLanguageDA : IDisposable { #region ==================== Private Field ==================== private bool isDisposed = false; #endregion #region ==================== Property ========================= private string m_ConnectionString; /// <summary> /// 数据库连接字符串 /// </summary> public string ConnectionString { get { return m_ConnectionString; } set { m_ConnectionString = value; } } #endregion #region ==================== Constructed Method =============== /// <summary> /// 禁止使用 /// </summary> private TagItemLanguageDA() { } /// <summary> /// 构造函数 /// </summary> /// <param name="connectionString">数据库连接串</param> public TagItemLanguageDA(string connectionString) { m_ConnectionString = connectionString; } #endregion #region ==================== Public Method ==================== #region ===== Select ===== /// <summary> /// 根据主键得到对象 /// </summary> /// <param name="pkid">主键</param> /// <returns>对象</returns> public TagItemLanguage GetItemByPkid(int pkid) { TagItemLanguage result = new TagItemLanguage(); TagDataContext dc = new TagDataContext(ConnectionString); dc.DeferredLoadingEnabled = true; var query = from tagItemLanguage in dc.TagItemLanguage where tagItemLanguage.pkid == pkid select tagItemLanguage; result = query.SingleOrDefault(); return result; } /// <summary> /// 根据主键列表得到对象集合 /// </summary> /// <param name="pkid">主键列表</param> /// <returns>对象集合</returns> public List<TagItemLanguage> GetListByPkid(List<int> pkidList) { if (pkidList.Count > 2100) { throw new ParameterOverflowException("传入的pkid列表个数大于2100,超过了Sql允许的最大参数个数。"); } List<TagItemLanguage> result = new List<TagItemLanguage>(); TagDataContext dc = new TagDataContext(ConnectionString); dc.DeferredLoadingEnabled = true; var query = from tagItemLanguage in dc.TagItemLanguage where pkidList.Contains(tagItemLanguage.pkid) select tagItemLanguage; result = query.ToList(); return result; } #endregion #region ===== Insert ===== /// <summary> /// 插入对象 /// </summary> /// <param name="item">TagItemLanguage对象</param> /// <returns>变更集</returns> public ChangeSet Insert(TagItemLanguage item) { TagDataContext dc = new TagDataContext(ConnectionString); dc.TagItemLanguage.InsertOnSubmit(item); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } /// <summary> /// 插入集合 /// </summary> /// <param name="item">TagItemLanguage集合</param> /// <returns>变更集</returns> public ChangeSet Insert(List<TagItemLanguage> itemList) { TagDataContext dc = new TagDataContext(ConnectionString); dc.TagItemLanguage.InsertAllOnSubmit(itemList); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } #endregion #region ===== InsertUpdate ===== /// <summary> /// InsertUpdate对象 /// </summary> /// <param name="item">TagItemLanguage对象</param> /// <returns>变更集</returns> public ChangeSet InsertUpdate(TagItemLanguage item) { TagDataContext dc = new TagDataContext(m_ConnectionString); var query = from tagItemLanguage in dc.TagItemLanguage where tagItemLanguage.TagName == item.TagName && tagItemLanguage.TagItemId == item.TagItemId && tagItemLanguage.LanguageCode == item.LanguageCode select tagItemLanguage; List<TagItemLanguage> itemList = query.ToList(); if (itemList != null && itemList.Count > 0) { item.pkid = itemList[0].pkid; if (itemList.Count > 1) { //发现了多条重复的数据.保留第一条,删除其他的条目 itemList.RemoveAt(0); this.PhysicsDelete(itemList); WebLog.CommentLog.ErrorLogger.Error(string.Format(@"在TagItemLanguage表发现错误数据. TagName:{0}, TagItemId:{1}, LanguageCode:{2}, 保留pkid:{3},删除其他数据.", item.TagName, item.TagItemId.ToString(), item.LanguageCode, item.pkid.ToString())); } //更新对象 ChangeSet result = this.Update(item); return result; } else { //插入对象 ChangeSet result = this.Insert(item); return result; } } /// <summary> /// InsertUpdate对象集合 /// </summary> /// <param name="itemList">TagItemLanguage集合</param> /// <returns>变更集数组:索引0为插入操作变更集,索引1为更新操作变更集</returns> public ChangeSet[] InsertUpdate(List<TagItemLanguage> itemList) { ChangeSet[] result = new ChangeSet[2]; List<string> tagNameList = new List<string>(); List<string> languageCodeList = new List<string>(); List<int> tagItemIdList = new List<int>(); foreach (TagItemLanguage tempItem in itemList) { if (!tagNameList.Contains(tempItem.TagName)) { tagNameList.Add(tempItem.TagName); } if (!languageCodeList.Contains(tempItem.LanguageCode)) { languageCodeList.Add(tempItem.LanguageCode); } if (!tagItemIdList.Contains(tempItem.TagItemId)) { tagItemIdList.Add(tempItem.TagItemId); } } //选取所有可能存在于数据库中的集合. TagDataContext dc = new TagDataContext(m_ConnectionString); var query = from tagItemLanguage in dc.TagItemLanguage where tagNameList.Contains(tagItemLanguage.TagName) && languageCodeList.Contains(tagItemLanguage.LanguageCode) && tagItemIdList.Contains(tagItemLanguage.TagItemId) select tagItemLanguage; //从数据库集合中筛选出需要更新和插入的列表 List<TagItemLanguage> allList = query.ToList(); List<TagItemLanguage> insertList = new List<TagItemLanguage>(); List<TagItemLanguage> updateList = new List<TagItemLanguage>(); foreach (TagItemLanguage tempItem in itemList) { List<TagItemLanguage> itemItemList = allList.Where(c => c.LanguageCode == tempItem.LanguageCode && c.TagName == tempItem.TagName && c.TagItemId == tempItem.TagItemId).ToList(); if (itemItemList != null && itemItemList.Count > 0) { tempItem.pkid = itemItemList[0].pkid; if (itemItemList.Count > 1) { //发现了多条重复的数据.保留第一条,删除其他的条目 itemItemList.RemoveAt(0); this.PhysicsDelete(itemItemList); WebLog.CommentLog.ErrorLogger.Error(string.Format(@"在TagItemLanguage表发现错误数据. TagName:{0}, TagItemId:{1}, LanguageCode:{2}, 保留pkid:{3},删除其他数据.", tempItem.TagName, tempItem.TagItemId.ToString(), tempItem.LanguageCode, tempItem.pkid.ToString())); } updateList.Add(tempItem); } else { insertList.Add(tempItem); } } //执行操作 result[0] = this.Insert(insertList); result[1] = this.Update(updateList); return result; } #endregion #region ===== Update ===== /// <summary> /// 更新对象 /// </summary> /// <param name="item">TagItemLanguage对象</param> /// <returns>变更集</returns> public ChangeSet Update(TagItemLanguage item) { TagDataContext dc = new TagDataContext(ConnectionString); item.Detach(); dc.TagItemLanguage.Attach(item, true); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } /// <summary> /// 更新对象集合 /// </summary> /// <param name="itemList">TagItemLanguage集合</param> /// <returns>变更集</returns> public ChangeSet Update(List<TagItemLanguage> itemList) { TagDataContext dc = new TagDataContext(ConnectionString); foreach (TagItemLanguage tempItem in itemList) { tempItem.Detach(); } dc.TagItemLanguage.AttachAll(itemList, true); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } #endregion #region ===== Delete ===== /// <summary> /// 物理删除对象 /// </summary> /// <param name="item">TagItemLanguage对象</param> /// <returns>变更集</returns> public ChangeSet PhysicsDelete(TagItemLanguage item) { TagDataContext dc = new TagDataContext(m_ConnectionString); item.Detach(); dc.TagItemLanguage.Attach(item); dc.TagItemLanguage.DeleteOnSubmit(item); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } /// <summary> /// 物理删除对象集合 /// </summary> /// <param name="itemList">TagItemLanguage集合</param> /// <returns>变更集</returns> public ChangeSet PhysicsDelete(List<TagItemLanguage> itemList) { TagDataContext dc = new TagDataContext(m_ConnectionString); foreach (TagItemLanguage tempItem in itemList) { tempItem.Detach(); } dc.TagItemLanguage.AttachAll(itemList); dc.TagItemLanguage.DeleteAllOnSubmit(itemList); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } #endregion #endregion #region IDisposable 成员 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public virtual void Dispose(bool disposing) { if (!this.isDisposed) { if (disposing) { //释放非托管资源 } //释放托管资源 m_ConnectionString = null; isDisposed = true; } } ~TagItemLanguageDA() { Dispose(false); } #endregion } }
这个标准的数据访问类中, 除了基本的Insert,Select,Update,Delete操作外,需要特别讲解的就是上面的InsertUpdate操作.在我们的数据库中有一个IsDeleted标志位用来表示本条记录是否被逻辑删除.而当用户希望插入另外一条记录的时候,有时候在数据库中并不是Insert操作而仅仅是恢复逻辑删除的数据.
如果每次Insert前,都要先Select看是否存在本条数据, 是十分笨重的.所以InsertUpdate提供了"逻辑插入"功能, 可以自己判断是需要Insert还是Update.
五.业务逻辑层模板
再来看看我们通用的业务逻辑层对象的代码. 则是一个最简单的业务逻辑层对象, 其中大部分都是单纯的对数据访问层方法的返回, 但是其中有的"逻辑删除"方法是自己特有的.
/*************************************************************************************** * * * * File Name : TagCategoryLanguageBL.cs * * Creator : ziqiu.zhang * * Create Time : 2008-11-10 * * Functional Description : TagCategoryLanguage业务逻辑类 * * Remark : * * * * Copyright (c) eLong Corporation. All rights reserved. * ***************************************************************************************/ using System; using System.Collections.Generic; using System.Linq; using System.Data.Linq; using System.Text; using Com.Elong.DataAccess.Tag; using Com.Elong.Model.Tag; using Com.Elong.BusinessRules.Common; using Com.Elong.Model.Common; namespace Com.Elong.BusinessRules.Tag { /// <summary> /// TagCategoryLanguage业务逻辑类 /// </summary> public class TagCategoryLanguageBL : IDisposable { #region ==================== Private Field ==================== private bool isDisposed = false; private TagCategoryLanguageDA tagCategoryLanguageDA; #endregion #region ==================== Property ========================= private string m_ConnectionString; /// <summary> /// 数据库连接字符串 /// </summary> public string ConnectionString { get { return m_ConnectionString; } set { m_ConnectionString = value; } } #endregion #region ==================== Constructed Method =============== /// <summary> /// 禁止使用 /// </summary> private TagCategoryLanguageBL() { } /// <summary> /// 构造函数 /// </summary> /// <param name="connectionString">数据库连接串</param> public TagCategoryLanguageBL(string connectionString) { m_ConnectionString = connectionString; tagCategoryLanguageDA = new TagCategoryLanguageDA(connectionString); } #endregion #region ==================== Basic Method ==================== #region ===== Select ===== /// <summary> /// 根据主键得到对象 /// </summary> /// <param name="pkid">主键</param> /// <returns>对象</returns> public TagCategoryLanguage GetItemByPkid(int pkid) { return tagCategoryLanguageDA.GetItemByPkid(pkid); } /// <summary> /// 根据主键列表得到对象集合 /// </summary> /// <param name="pkid">主键列表</param> /// <returns>对象集合</returns> public List<TagCategoryLanguage> GetListByPkid(List<int> pkidList) { return tagCategoryLanguageDA.GetListByPkid(pkidList); } #endregion #region ===== Insert ===== /// <summary> /// 插入对象 /// </summary> /// <param name="item">TagCategoryLanguage对象</param> /// <returns>变更集</returns> public ChangeSet Insert(TagCategoryLanguage item) { return tagCategoryLanguageDA.Insert(item); } /// <summary> /// 插入集合 /// </summary> /// <param name="item">TagCategoryLanguage集合</param> /// <returns>变更集</returns> public ChangeSet Insert(List<TagCategoryLanguage> itemList) { return tagCategoryLanguageDA.Insert(itemList); } #endregion #region ===== InsertUpdate ===== /// <summary> /// InsertUpdate对象 /// </summary> /// <param name="item">TagCategoryRefCity对象</param> /// <returns>变更集</returns> public ChangeSet InsertUpdate(TagCategoryLanguage item) { return tagCategoryLanguageDA.InsertUpdate(item); } /// <summary> /// InsertUpdate集合 /// </summary> /// <param name="itemList">TagCategoryRefCity集合</param> /// <returns>变更集</returns> public ChangeSet[] InsertUpdate(List<TagCategoryLanguage> itemList) { return tagCategoryLanguageDA.InsertUpdate(itemList); } #endregion #region ===== Update ===== /// <summary> /// 更新对象 /// </summary> /// <param name="item">TagCategoryLanguage对象</param> /// <returns>变更集</returns> public ChangeSet Update(TagCategoryLanguage item) { return tagCategoryLanguageDA.Update(item); } /// <summary> /// 更新集合 /// </summary> /// <param name="item">agCategory集合</param> /// <returns>变更集</returns> public ChangeSet Update(List<TagCategoryLanguage> itemList) { return tagCategoryLanguageDA.Update(itemList); } #endregion #region ===== Delete ===== /// <summary> /// 物理删除对象 /// </summary> /// <param name="item">TagCategoryLanguage对象</param> /// <returns>变更集</returns> public ChangeSet PhysicsDelete(TagCategoryLanguage item) { return tagCategoryLanguageDA.PhysicsDelete(item); } /// <summary> /// 物理删除集合 /// </summary> /// <param name="item">TagCategoryLanguage集合</param> /// <returns>变更集</returns> public ChangeSet PhysicsDelete(List<TagCategoryLanguage> itemList) { return tagCategoryLanguageDA.PhysicsDelete(itemList); } /// <summary> /// 逻辑删除对象 /// </summary> /// <param name="item">TagCategoryLanguage对象</param> /// <returns>变更集</returns> public ChangeSet LogicDelete(int pkid) { TagCategoryLanguage item = tagCategoryLanguageDA.GetItemByPkid(pkid); if (item.IsDeleted != 1) { item.IsDeleted = 1; } ChangeSet result = tagCategoryLanguageDA.Update(item); return result; } /// <summary> /// 逻辑删除集合 /// </summary> /// <param name="item">TagCategoryLanguage集合</param> /// <returns>变更集</returns> public ChangeSet LogicDelete(List<int> pkidList) { List<TagCategoryLanguage> itemList = tagCategoryLanguageDA.GetListByPkid(pkidList); foreach (TagCategoryLanguage item in itemList) { item.IsDeleted = 1; } return tagCategoryLanguageDA.Update(itemList); } #endregion #endregion #region ==================== Extend Method ==================== #endregion #region IDisposable 成员 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public virtual void Dispose(bool disposing) { if (!this.isDisposed) { if (disposing) { //释放非托管资源 tagCategoryLanguageDA.Dispose(); } //释放托管资源 m_ConnectionString = null; isDisposed = true; } } ~TagCategoryLanguageBL() { Dispose(false); } #endregion } }
六.如何组合应用
上面是TagItemLanguage表的BL(Business Logic)和DA(Data Access)对象, 同理TagItem表的TagItemBL和TagItemDA几乎是一模一样的代码.
一个TagItem对象带有一个TagItemLanguage对象集合的属性, 经常我们更新一个TagItem需要更新关联的TagItemLanguage对象, 这个时侯我们就可以在TagItemBL对象中创建一个方法, 先调用TagItemDA更新TagItem表, 再调用TagItemLanguageDA更新TagItemLanguage表.
七.经验和总结
使用LINQ第一个挑战是拆分业务逻辑和对象职责, 第二挑战就是性能. 既然将每个表的更新都放到了具体的对象去做, 那么很显然会造成大量的数据库访问请求. 比如一开始对于一个集合的逻辑更新, 如果循环每条记录去判断是要更新还是删除, 那就会造成多次数据库连接. 而我的做法是首先群穷举出所有的可能需要的数据条目, 都取出来后在程序中进行查询来判断哪些对象需要更新, 哪些需要添加, 最后执行一次批量更新和批量插入操作.一共访问三次数据库. 我虽然加重了传输数据量的传输但是已经大幅减少了数据库连接次数. 一般数据库和服务器的通信都是内网的, 所以牺牲数据量是值得的, 实践证明数据库连接才是最宝贵的资源.
另外我发现LINQ TO SQL生成的SQL效率很高, 比初级开发人员的SQL效率要高. 项目的性能问题从开发的第一天我就开始担忧了. 但是没想到居然还可以接受.这也和目前频道初期的访问量没有上来有关系. YY一下, 艺龙点评频道即将开始推广活动了, 希望到时候大家多多支持! 最近看到千橡收购艺龙流通股消息, 也为我们这些员工制造了YY的空间. 如果真的合作, 这么大的SNS流量过来系统是否还撑得住, 拭目以待!
出处:http://www.cnblogs.com/zhangziqiu/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。