一.摘要
会用LINQ的人很多, 但是用过LINQ做企业应用的人却不多.所以能搜索到的资料很有限.不才的我恰巧在新的项目中使用了LINQ,结果遇到了重重困难.在此将我经历了LINQ的磨难后,总结的经验和方法分享给大家.
第一篇文章将我总结出来的LINQ开发的流程, 后面的文章将对其中的很多细节和难点一一讲解.一家之言仅供参考,如有不对的地方请大家指正.
二.前言
先说几句题外话.有关项目的实际效果, 大家可以去新上线没多久的项目: dianping.elong.com 上参观一下. 我的痛苦经历也就是在实现此频道的过程中.其实从纯技术上说, 这套系统有很多闪光点:
- 领域通用的Tag标签系统和点评系统,可以支持任意产品线和产品的点评和标签.
- 支持任意语言扩展.所有的内容都是用语言表存储.
- 动态投票模型,主要是使用了数据库存储元数据的方式,实现了可以随时添加投票项.
- 使用LINQ实现ORM
- 使用SOA思想将UI与业务逻辑通过服务切割开来.
- 使用WCF作为SOA的实现
- 支持动态更换推荐指数计算的DLL,无需重启服务器
- 在通用的Tag和点评系统上组装成艺龙酒店点评系统
看起来忽悠人不?开发的时候我真的是希望将此项目作为一个典范或者说开发企业应用的模板, 来做一个可以让我骄傲的产品. 我是喜欢追求代码艺术的人, 然后很悲剧的是往往质量要为进度让路. 而且我不能让小组中的每个人都按照我想的方向去实现.最后终于完成了这款不完美的作品.亮点很多, 但是在基本的实现中却有很多的业务没有切割清楚, 各种设计准则和设计模式更是被一些人抛到脑后.来一张此项目的截图,限于公司保密原因不能提供给大家源代码:
这里面有一些很让我看着不舒服的项目是被别人加进来的.尤其是Service层.本来在我创建项目的第一个版本的时候Host就是用来存放服务宿主的,无论是WebService还是WCF的IIS宿主,都可以放在HOST中.但是随后被领导们添加进来了那几个名字超长的项目.
闲话不说,抱怨无罪也无用.下面来看看如果设计实现一套系统,我认为的"敏捷"开发应该如何去做:
三.在PowerDesign中设计数据模型
在此我们跳过理解分析需求,指定业务模型的过程.即使已有了业务模型也依然要有数据模型.所以下面假定我们已经开始了数据库设计.
我习惯使用PD设计数据库模型.并在在这个阶段要将索引,外键,字段默认值,注释等统统设计进去.这里多花费的时间是完全可以值得的.
下面是两个表的截图:
四.在数据库中创建表,外键和索引
在PowerDesign中设计的数据模型可以选择对应的数据库类型,比如SqlServer2005. 并且在Preview选项卡中可以查看生成的SQL.这时上一步我们设计好的所有对象都派上了用场,甚至包括数据库中的字段注释.
比如下图:
五.使用 Sqlmetal.EXE 生成Model层代码
入门的LINQ教程多举例使用可视化的"对象关系设计器(O/R设计器)"创建LINQ模型,但是实践证明此设计器属于鸡肋.原因是默认生成的代码100%不符合需求, 比如属性没有默认值,没有任何注释,没有Detach方法等.
可视化设计使用dbml文件, 保存后自动生成 [dbml名].design.cs后缀的代码文件, 生成的类都是部分类.同时允许我们手工添加 [dbml名].cs文件,这个不会在dbml修改时被重写,因为自动生成的类都是部分类,虽然可以在此文件中扩展类成员和方法.但是如果添加属性默认值和注释的话是必须要修改.design.cs文件.
问题是dbml的修改是单向的.在dbml中可视化修改会同时更新关联的.design.cs文件,但是如果手动修改了.cs文件代码,dbml不会更新.而且在dbml中的任何修改都会导致关联的.design.cs文件重写, 如果我们手工给其加上注释,则只要在对象关系设计器中做了任何修改,就会被冲掉代码.手工添加注释可不是一件轻松的工作.
所以最佳建议是: 第一次设计时可以使用"对象关系设计器(O/R设计器)"进行可视化设计.以后对LINQ模型的修改都是手动修改[dbml名].design.cs和[dbml名].cs代码文件完成.如果新添加了表,可以使用SqlMetal.exe工具生成新的代码文件,然后在将文件中的新增的表的模型Copy到项目中.
下面是一个最简单的SqlMetal命令工具,其中conn参数是数据库连接串,code是要生成的新的代码文件路径.
Sqlmetal /conn:"uid=****;pwd=****;initial catalog=DataBaseName;data source=192.168.0.1;Connect Timeout=900;Connection Reset=FALSE" /code:"d:\DB.cs"
在生成的DB.cs中有整个数据库所有表的Model模型, 以及完整的DataContext.
自动生成的模型代码.有很多需要修改的点,这些细节我将在下一篇文章中详细讲解.
六.打造基础数据访问层与业务逻辑层对象
虽然再外层封装了服务,但是我认为内部的具体实现代码仍然少不了这两个层次.LINQ仅仅是帮我们完成了ORM的任务,让我们直接操作对象,而不用关心如何操作数据库.
但是如何使用LINQ来创建这两个层次对象呢? 下面是我的总结:
1.将添加和更新操作放在每个表的实体模型中
因为LINQ会将数据库中的"键",转换成对象模型中的属性.比如表A,表B 和 A-B关系表(多对多),转换成的模型后,A对象有一个属性是EntitySet<A-B>, B同理.A-B对象则有两个属性分别是EntitySet<A>和EntitySet<B>.至于1对多的表的模型, 会在"多"的一方模型上创建EntityRef属性, 这个属性不是一个集合, 而是对"1"这一方对象的反向引用.
使用LINQ最容易导致的误区是对象职责拆分不清楚.因为DataContext包含了所有的数据库对象, 很可能我们在使用B对象的一个方法插入了一个A对象. 对于复杂的业务逻辑来说职责的拆分是必不可少的, 任何的设计模式以及原则其实本质上都是为实现SRP单一职责原则而努力的.比如我们希望在发表点评数据的时候做一些运算和统计工作.如果没有一个统一的点评发表点, 源程序中到处散布着可以添加点评对象的地方, 那么完成这个工作就要花费大量的成本.
所以我构造对象的原则就是:
每个表都有一个Model类,一个数据访问层对象,一个业务逻辑层对象.每个表的添加和更新操作只能使用本表的相关对象完成.即:A只负责A表的添加个更新操作,不能更新和操作B表.如果需要只能通过使用B对象的方式.
2.更新和插入操作一定要自顶向下.
已经将职责拆分好以后,我们还需要针对EntitySet和EntityRef对象的插入和更新指定顺序. 否则会产生循环更新.
原则:更新和插入操作要从 "一对多"中"一"的一方开始,在叶子节点或者多对多表中终结.
这两点也许太过抽象.下面提供一个数据访问层对象的模板类.对于其他的表只需要批量替换其中表名即可:
/****************************************************************************************************
* *
* * File Name : TagRefEntityInstanceDA.cs
* * Creator : ziqiu.zhang
* * Create Time : 2008-11-10
* * Functional Description : TagRefEntityInstance数据访问类
* * 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>
/// TagRefEntityInstance数据访问类
/// </summary>
public class TagRefEntityInstanceDA : 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 TagRefEntityInstanceDA()
{ }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="connectionString">数据库连接串</param>
public TagRefEntityInstanceDA(string connectionString)
{
m_ConnectionString = connectionString;
}
#endregion
#region ==================== Public Method ====================
#region ===== Select =====
/// <summary>
/// 得到Tag和实体的关系
/// </summary>
/// <param name="entityId">实体类型Id</param>
/// <param name="entityInstanceId">实体实例Id</param>
/// <param name="tagItemId">TagItemId</param>
/// <returns>TagRefEntityInstance列表</returns>
public List<TagRefEntityInstance> GetItemByEntity(int entityId, string entityInstanceId, int tagItemId)
{
List<TagRefEntityInstance> result = new List<TagRefEntityInstance>();
TagDataContext dc = new TagDataContext(ConnectionString);
dc.DeferredLoadingEnabled = true;
var query =
from tagRefEntityInstance in dc.TagRefEntityInstance
where tagRefEntityInstance.EntityId == entityId
&& tagRefEntityInstance.EntityInstanceId == entityInstanceId
&& tagRefEntityInstance.TagItemId == tagItemId
select tagRefEntityInstance;
result = query.ToList();
return result;
}
/// <summary>
/// 根据主键得到对象
/// </summary>
/// <param name="pkid">主键</param>
/// <returns>对象</returns>
public TagRefEntityInstance GetItemByPkid(int pkid)
{
TagRefEntityInstance result = new TagRefEntityInstance();
TagDataContext dc = new TagDataContext(ConnectionString);
dc.DeferredLoadingEnabled = true;
var query =
from tagRefEntityInstance in dc.TagRefEntityInstance
where tagRefEntityInstance.pkid == pkid
select tagRefEntityInstance;
result = query.SingleOrDefault();
return result;
}
/// <summary>
/// 根据主键列表得到对象集合
/// </summary>
/// <param name="pkid">主键列表</param>
/// <returns>对象集合</returns>
public List<TagRefEntityInstance> GetListByPkid(List<int> pkidList)
{
if (pkidList.Count > 2100)
{
throw new ParameterOverflowException("传入的pkid列表个数大于2100,超过了Sql允许的最大参数个数。");
}
List<TagRefEntityInstance> result = new List<TagRefEntityInstance>();
TagDataContext dc = new TagDataContext(ConnectionString);
dc.DeferredLoadingEnabled = true;
var query =
from tagRefEntityInstance in dc.TagRefEntityInstance
where pkidList.Contains( tagRefEntityInstance.pkid )
select tagRefEntityInstance;
result = query.ToList();
return result;
}
/// <summary>
/// 根据TagItemId,EntityId,获取EntityInstanceId集合:
/// </summary>
/// <param name="tagItemId">TagItemId</param>
/// <param name="entityId">实体分类Id</param>
/// <returns>EntityInstanceId列表</returns>
public List<string> GetEntityInstanceId(int tagItemId, int entityId)
{
List<string> result = new List<string>();
TagDataContext tagDataContext = new TagDataContext(m_ConnectionString);
tagDataContext.DeferredLoadingEnabled = false;
//主查询语句
var query = from tagRegEntityInstance in tagDataContext.TagRefEntityInstance
where tagRegEntityInstance.TagItemId == tagItemId
&& tagRegEntityInstance.EntityId == entityId
&& tagRegEntityInstance.IsDeleted == 0
select tagRegEntityInstance.EntityInstanceId;
//执行查询,返回结果。
result = query.Distinct().ToList();
return result;
}
#endregion
#region ===== Insert =====
/// <summary>
/// 插入对象
/// </summary>
/// <param name="item">TagRefEntityInstance对象</param>
/// <returns>变更集</returns>
public ChangeSet Insert(TagRefEntityInstance item)
{
TagDataContext dc = new TagDataContext(ConnectionString);
dc.TagRefEntityInstance.InsertOnSubmit(item);
ChangeSet result = dc.GetChangeSet();
dc.SubmitChanges();
return result;
}
/// <summary>
/// 插入集合
/// </summary>
/// <param name="item">TagRefEntityInstance集合</param>
/// <returns>变更集</returns>
public ChangeSet Insert(List<TagRefEntityInstance> itemList)
{
TagDataContext dc = new TagDataContext(ConnectionString);
dc.TagRefEntityInstance.InsertAllOnSubmit(itemList);
ChangeSet result = dc.GetChangeSet();
dc.SubmitChanges();
return result;
}
#endregion
#region ===== InsertUpdate =====
/// <summary>
/// InsertUpdate对象
/// </summary>
/// <param name="item">TagRefEntityInstance对象</param>
/// <returns>变更集</returns>
public ChangeSet InsertUpdate(TagRefEntityInstance item)
{
TagDataContext dc = new TagDataContext(m_ConnectionString);
//TagRefEntityInstance数据完整性: 只存在一条 TagItemId, EntityId, EntityInstanceId 相同数据
var query =
from tagRefEntityInstance in dc.TagRefEntityInstance
where tagRefEntityInstance.TagItemId == item.TagItemId
&& tagRefEntityInstance.EntityId == item.EntityId
&& tagRefEntityInstance.EntityInstanceId == item.EntityInstanceId
select tagRefEntityInstance;
List<TagRefEntityInstance> 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(@"在TagRefEntityInstance表中发现错误数据.
TagItemId:{0},EntityId:{1},EntityInstanceId:{2} 保留Pkid:{3}, 删除其他数据.",
item.TagItemId.ToString(),
item.EntityId.ToString(),
item.EntityInstanceId,
item.pkid.ToString()));
}
//更新对象
ChangeSet result = this.Update(item);
return result;
}
else
{
//插入对象
ChangeSet result = this.Insert(item);
return result;
}
}
/// <summary>
/// InsertUpdate对象集合
/// </summary>
/// <param name="itemList">TagRefEntityInstance集合</param>
/// <returns>变更集数组:索引0为插入操作变更集,索引1为更新操作变更集</returns>
public ChangeSet[] InsertUpdate(List<TagRefEntityInstance> itemList)
{
ChangeSet[] result = new ChangeSet[2];
List<int> entityIdList = new List<int>();
List<string> entityInstanceIdList = new List<string>();
List<int> tagItemIdList = new List<int>();
//将itemList中所有出现过的entityId,entityInstanceId和tagItemId保存在列表中
foreach (TagRefEntityInstance tempItem in itemList)
{
if (!entityIdList.Contains(tempItem.EntityId))
{
entityIdList.Add(tempItem.EntityId);
}
if (!entityInstanceIdList.Contains(tempItem.EntityInstanceId))
{
entityInstanceIdList.Add(tempItem.EntityInstanceId);
}
if (!tagItemIdList.Contains(tempItem.TagItemId))
{
tagItemIdList.Add(tempItem.TagItemId);
}
}
//选取所有可能存在于数据库中的集合.
TagDataContext dc = new TagDataContext(m_ConnectionString);
var query =
from tagRefEntityInstance in dc.TagRefEntityInstance
where entityIdList.Contains( tagRefEntityInstance.EntityId )
&& entityInstanceIdList.Contains(tagRefEntityInstance.EntityInstanceId)
&& tagItemIdList.Contains(tagRefEntityInstance.TagItemId)
select tagRefEntityInstance;
//从数据库集合中筛选出需要更新和插入的列表
List<TagRefEntityInstance> allList = query.ToList();
List<TagRefEntityInstance> insertList = new List<TagRefEntityInstance>();
List<TagRefEntityInstance> updateList = new List<TagRefEntityInstance>();
foreach (TagRefEntityInstance tempItem in itemList)
{
List<TagRefEntityInstance> tempItemList = allList.Where(c => c.EntityId == tempItem.EntityId && c.EntityInstanceId == tempItem.EntityInstanceId && c.TagItemId == tempItem.TagItemId).ToList();
if (tempItemList != null && tempItemList.Count > 0)
{
tempItem.pkid = tempItemList[0].pkid;
if (tempItemList.Count > 1)
{
tempItemList.RemoveAt(0);
this.PhysicsDelete(tempItemList);
WebLog.CommentLog.ErrorLogger.Error(string.Format(@"在TagRefEntityInstance表中发现错误数据.
TagItemId:{0},EntityId:{1},EntityInstanceId:{2} 保留Pkid:{3}, 删除其他数据.",
tempItem.TagItemId.ToString(),
tempItem.EntityId.ToString(),
tempItem.EntityInstanceId,
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">TagRefEntityInstance对象</param>
/// <returns>变更集</returns>
public ChangeSet Update(TagRefEntityInstance item)
{
TagDataContext dc = new TagDataContext(ConnectionString);
item.Detach();
dc.TagRefEntityInstance.Attach(item, true);
ChangeSet result = dc.GetChangeSet();
dc.SubmitChanges();
return result;
}
/// <summary>
/// 更新对象集合
/// </summary>
/// <param name="itemList">TagRefEntityInstance集合</param>
/// <returns>变更集</returns>
public ChangeSet Update(List<TagRefEntityInstance> itemList)
{
TagDataContext dc = new TagDataContext(ConnectionString);
foreach (TagRefEntityInstance tempItem in itemList)
{
tempItem.Detach();
}
dc.TagRefEntityInstance.AttachAll(itemList, true);
ChangeSet result = dc.GetChangeSet();
dc.SubmitChanges();
return result;
}
#endregion
#region ===== Delete =====
/// <summary>
/// 物理删除对象
/// </summary>
/// <param name="item">TagRefEntityInstance对象</param>
/// <returns>变更集</returns>
public ChangeSet PhysicsDelete(TagRefEntityInstance item)
{
TagDataContext dc = new TagDataContext(m_ConnectionString);
item.Detach();
dc.TagRefEntityInstance.Attach(item);
dc.TagRefEntityInstance.DeleteOnSubmit(item);
ChangeSet result = dc.GetChangeSet();
dc.SubmitChanges();
return result;
}
/// <summary>
/// 物理删除对象集合
/// </summary>
/// <param name="itemList">TagRefEntityInstance集合</param>
/// <returns>变更集</returns>
public ChangeSet PhysicsDelete(List<TagRefEntityInstance> itemList)
{
TagDataContext dc = new TagDataContext(m_ConnectionString);
foreach (TagRefEntityInstance tempItem in itemList)
{
tempItem.Detach();
}
dc.TagRefEntityInstance.AttachAll(itemList);
dc.TagRefEntityInstance.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;
}
}
~TagRefEntityInstanceDA()
{
Dispose(false);
}
#endregion
}
}
七.实现具体业务逻辑
现在我们的业务逻辑层对象已经使用LINQ实现了基础的CURD操作, 接下来就需要在业务逻辑类中完成和功能的开发了. 在此我们可以根据项目考虑是否可以使用服务的方式组织调用业务逻辑对象, 对于我的项目我就是抽象出来了服务层. 有对内的服务也有对外暴露的服务.
八.开发UI层(Web层)
通过服务将业务逻辑与页面展示彻底分开, 不同于以前使用业务逻辑层, 使用WCF构建的服务层我们可以方便的实现分布式调用.可以将提供服务的宿主和Web项目放在不同的服务器里.
九.常见问题与总结
在PD中添加的字段注释,并不能反映在自动生成的代码中.我认为这是LINQ的Bug, 因为在使用LINQ之前, 我使用自己的"伪ORM框架",说"伪"是因为我还是通过写SQL语句实现的在数据访问层将数据转化成对象, 从而在业务逻辑层操作的都是对象. 但是数据访问层,业务逻辑层,和Model层我使用自己开发的CodeSmith模板, 全部自动生成, 并且能够将PD中生成的注释全部带到源程序中. 不需要修改一行代码就能实现某一个对象的基础CURD操作以及"按照对象任意属性组合条件查询". 将数据库中的注释带到代码中,这种连我都能做到的事情为何微软做不到呢? 在PD生成的SQL中是带有字段注释的.
本篇文章个人觉的写的不好, 文字描述过多.在此表示歉意! 本来写了很多实际的Coding要点, 但是只能放在下篇文章中了, 本篇文章赶紧收尾睡觉.希望做梦不要有大家扔板砖过来......