【IT168 技术文档】
1.项目背景
有感于网络上的技术文章大多是针对具体技术的讲解和demo,很少有各种技术整合在一个较大项目应用的例子,所以打算写这么一个系列,希望对大家有所帮助,因为里面很多东西也是自己根据实际项目经验琢磨出来的,不一定都是正确的,所以也希望大家指出其中的错误和问题。因为平时工作繁忙,也从来没有写过博客,可能排版啥的不是很地道,大伙凑合看吧。
另外需要注意的是 这个项目的框架设计和技术解决方案的应用是为较复杂的信息管理系统设计的,不是很适合小项目或网站项目,希望大家看清楚项目背景,勿盲目效仿。
业务概况
大型三甲医院信息管理系统。
特点:
业务复杂多变
模块众多,可灵活分合
日门诊量1w
B/S结构且对界面交互性极高
对系统稳定性和可扩展性要求较高
团队概况
10人左右小团队
开发环境
Microsoft Visual Studio 2008 sp1
Oracle10g
开发思想
分层开发
面向对象开发
业务领域驱动开发
对象关系映射
DI,AOP
SOA
B/S结构应用程序
工作流
...
应用技术
Asp.net MVC 1
NHibernate 2.1.0
Unity 1.2
EXT3.x
...
开发环境准备
Microsoft Visual Studio 2008
Microsoft Visual Studio 2008 Sp1(补丁)
Microsoft ASP.NET MVC 1.0
新建解决方案
新建空白解决方案 HISDemo
新建解决方案文件夹
对于大型项目而言,模块和代码众多,我们可以通过“解决方案文件夹”来对项目进行合理的管理,如下图:
lib文件夹用来存放项目中引用的第三方dll(这样有个好处,你可以把DLL拖进去,这样当你项目绑定VSS后,这些DLL大家就可以通过VSS获取了)
Presentation(展示层)用来存放UI层相关代码
因为项目模块众多,我们可以按照粗粒度的模块进行划分如Infrastructure(基础数据设置模块),住院模块,门诊挂号模块....
注:解决方案文件夹可以用中文,你也可以按照你的习惯和具体项目情况进行划分,总的原则就是方便管理和查看
基本分层思想
时间关系图花的比较糙,有些术语也不是很规范,能理解意思就行,如图:
数据访问层Repositories:主要用NHibernate访问数据库,但也有可能去访问其他模块或系统的WebService,也有可能用Linq去访问一些缓存(内存中的)数据,也有可能访问XML,文本文件等等....
业务领域层Core:系统的核心层,所有与数据访问无关的业务逻辑都应该内聚在这里,业务领域对象理论上应该是充血的,内聚自己的业务逻辑。但有一些业务逻辑在设计的时候涉及到了多个业务领域对象 ,我们很难决定放在哪个具体的业务对象里,所以我们有一个Service层来放这种业务逻辑。
装饰层Facade:把数据访问接口,业务领域对象的业务逻辑,Service接口简单的封装一下成为Facade层接口供展示层UI或SOA层调用,这个层需要注意的是它只是简单的封装,免得UI层调太多层的接口,这层不应含有业务逻辑。
SOA层:因为系统比较庞大 模块很多,且业务上要求各大模块间需要一定解耦,所以这一层作用是作为各大粗粒度模块间调用以及给其他系统调用,比如有基础数据管理大模块和门诊挂号大模块,他们之间的调用是必须要通过SOA层的,而不能直接走Facade层,传输的对象应该新建DTO数据传输对象,而不应该直接传递业务领域对象,从而通过SOA层我们把各个粗粒度模块完全隔离开。这个层取名为SOA也许不大恰当,大家也不必深究...计划是采用WCF 这样可以灵活的配置通讯方式
表现层Presentation:由Asp.net MVC的Action给ExtJs传输数据 ,Controler只需调用Facade接口
公共类库FrameWork:整个项目框架的公用代码,相当于公共类库,也许叫FrameWork不大恰当 呵呵
新建分层项目
新建3个类库项目如图:
注意修改一下项目的命名空间,如Demo.HIS.Infrastructure.Core [公司名].[项目名].[大模块名].[分层名]....在实际项目中合理的命名空间是很重要的
再在Presentation文件夹下新建2个项目Demo.HIS.MVC(项目类型:类库)和DemoHisSite(项目类型:Asp.net MVC Web Appliction),如图:
最后在解决方案根目录下新建一个类库Demo.HIS.FrameWork作为我们的公共类库就完成了,最后的项目结构是这样的:
这样我们整个系统所需的项目就建立完成了
3.业务领域对象建模
你是不是已经厌倦了和数据库表一一对应的Model或Entity?
Ok~我们现在尝试真正的用面向对象的思想去设计我们的业务实体类吧....
注:这里我并不是说和数据库表一一对应的Model这种做法有多么不好,因为毕竟表驱动设计模式经过这么多年的实践,到现在也具有很强的生命力 。并且我也用这样的设计做项目多年,可是一直存在一个疑惑,为啥在处理业务的时候大牛们所鼓吹的面向对象编程思想几乎一点都没用到呢?所以这里我们做一下新的尝试,也许由于各种具体实现技术的限制和我水平有限这种尝试可能并不彻底...
按照国际惯例,我们新建一个抽象类,作为我们系统中所有业务对象的基类,将来可以用来识别哪些类是我们自己的业务对象类
/// <summary>
/// 系统所有业务类都要继承的基类
/// </summary>
[Serializable]
public abstract class BaseObject {
}
}
一般在信息管理系统中,大多数的业务对象都需要持久到数据库,所以我们再新建一个抽象类作为所有可以被持久化的业务对象都要继承的基类
{
/// <summary>
/// 可以持久到数据库的业务类都要继承的基类
/// </summary>
public abstract class Entity : BaseObject
{
public Entity()
{
Id = Guid.NewGuid().ToString();
CreateTime = DateTime.Now;
IsDelete = false;
}
public virtual string Id { get; protected set; }
public virtual DateTime CreateTime { get; protected set; }
public virtual bool IsDelete { get; set; }
public virtual Int32 Version { get; protected set; }
}
}
·Entity继承了BaseObject。因为Entiy也是业务对象,只不过它是可以持久化的业务对象。
·为啥所有属性都要用virtual? 因为我们需要用NHibernate来做持久化,它要求属性必须是virtual,如果你还要问为啥NHibernate需要用virtual,据我所知NHibernate移植自Java的Hibernate,在Java里virtual是常态,为了移植方便 所以沿用...其实就算NHibernate不需要virtual,我也会写成virtual....这样子类可以重写,是不是很罗嗦....?
·所有可以持久到数据库中对象都有Id属性,别告诉你数据库里的表没有Id字段做为主键(特殊情况可能没有...比如多对多关联表)。为啥我的Id数据类型是string? 项目很大,生命周期也长,指不定啥时候因为某种原因需要改变Id的数据类型(我就遇到过...)所以我们不把Id的数据类型定死为int或guid,而且把Id的生成方式放在这个基类的构造函数里,注意Id = Guid.NewGuid().ToString();这里我们的Id生成方式采用GUID。
·为了防止误删除,以及一些表间关联情况下删除而导致程序出错,我们系统中默认的删除一般都是逻辑删除,所以用IsDelete字段进行标识(这样可能产生不少垃圾数据,以后我们可以进行过期删除处理或转移之类的功能来避免)。
·Version字段是 提供给NHibernate处理并发用的。
·CreateTime有啥用?我也不知道....
应用场景举例之InputItem
在我们的项目中经常需要用到各种下拉选择项,而且客户要求很BT,供选择的项可能从几个到上万个不等,而且为了快速录入需要模糊输入汉字,编码,拼音,五笔都可以检索到想要选择的项,如下图(这是B/S的用EXT实现):
于是我们想到为系统中所有需要这样录入的对象建立一个基类:
{
/// <summary>
/// 下拉输入项实体
/// </summary>
public abstract class InputItem : Entity
{
private string name;
private string code;
/// <summary>
/// 文本值
/// </summary>
public virtual string Name
{
get
{
return this.name;
}
set
{
if (String.IsNullOrEmpty(value))
throw new NotNullException();
this.name = value;
}
}
/// <summary>
/// 编码
/// </summary>
public virtual string Code
{
get
{
return this.code;
}
set
{
if (String.IsNullOrEmpty(value))
throw new NotNullException();
this.code = value;
}
}
/// <summary>
/// 助记码
/// </summary>
public virtual string InputCode1
{
get;
set;
}
/// <summary>
/// 助记码2
/// </summary>
public virtual string InputCode2
{
get;
set;
}
/// <summary>
/// 助记码3
/// </summary>
public virtual string InputCode3
{
get;
set;
}
}
}
NotNullException是我定义的异常类 ,这里可以暂时不用管它
有了这个类,如上图的那个“服务项目类”我们可以这样写:
{
/// <summary>
/// 服务项目
/// </summary>
public class ServiceItem : InputItem
{
//ServiceItem属性1
//ServiceItem属性2
}
}
应用场景举例之User
比如在我们的项目中可能登录我们系统的用户角色可能有 医生,护士,患者,医院行政人员,这几个对象的都应该属于系统用户对象
所以我们可以建立一个User抽象类,包含基本的用户名,密码等属性,且包含一些系统用户的方法比如登录,注销,修改密码等等....
然后针对 医生,护士,患者,医院行政人员这几个对象分别建类继承User抽象类,各自的对象中包含各自的属性和方法
具体的代码我就不贴了,反正这个系列以后会讲到...
以后我们的业务对象都按照这样的思路去设计,和以前我们单调的和数据库表一一对应的实体类比较起来,这样写是不是比较OO且比较有趣一些呢?
大套的理论到处都有这里就不吹了,至少我个人觉得这样写感觉上比较有设计感一些,而且对于应对需求变更的应付能力也强了很多
4.用NHibernate保存和查询我们的业务领域对象
关于NHibernate的相关资料不是很多,关于它在.Net项目中的实际应用的资料就更少了,在开始用NHibernate的时候也走了不少弯路,到现在自我感觉能在项目中合理应用NHibernate了。这里要感谢下同事1-2-3,关于NHibernate应用的不少技术细节问题基本都靠他努力解决的。也感谢李永京的NHibernate之旅系列文章导航,在入门初期省去了我阅读英文文档的麻烦,感谢了两位之后貌似NHibernate这块就没我啥事了...汗
注:额外说一下 目前.Net下可用使用ORM工具不少,但我个人觉得NHibernate是目前.Net下最强大最好用的ORM工具,这里我不想挑起.Net ORM框架之争,如果要争也希望大家在项目中实际应用之后再说,别老说什么NHibernate XML配置麻烦,效率低之类的 没有前提的无聊的话。至少在我的项目里正因为有了NHibernate才能使我可以比较OO的去建我的业务领域对象,貌似更加接近传说中的真正的所谓对象关系映射....
同样按照国际惯例,我们应该建立一个统一的一般的数据库访问接口
{
public interface IRepository<T> where T:Entity
{
T Load(string id);
T Get(string id);
IList<T> GetAll();
void SaveOrUpdate(T entity);
void Update(T entity);
void Delete(string id);
void PhysicsDelete(string id);
}
}
·大家应该通过这些接口方法的名字就应该猜出其中的含义了吧?什么获取一个对象,保存对象,删除对象,获取一个List...
·public interface IRepository
·Load和Get的命名和NHibernate有关 ,以后会讲到,心急的同学可以google“Hibernate Get Load 区别”
·Delete和PhysicsDelete ,上一篇有讲到 我们系统中默认删除为逻辑删除,所以物理删除属于“变态”特别命名
·有的同学看到接口设计的这么简单估计没兴趣看下去了...别急,咱分页,排序,多条件组合查询,多表查询等等典型应用场景一个不缺 请的慢慢看下去..有图有真相:
有了IRepository接口 我们就可以写针对这个接口的具体是实现了 因为我们是用NHibernate实现所以我们建一个这样的类
{
public class RepositoryNhbImpl<T> : IRepository<T> where T : Entity
{
#region IRepository<T> 成员
public T Load(string id)
{
throw new NotImplementedException();
}
public T Get(string id)
{
throw new NotImplementedException();
}
public IList<T> GetAll()
{
throw new NotImplementedException();
}
public void SaveOrUpdate(T entity)
{
throw new NotImplementedException();
}
public void Update(T entity)
{
throw new NotImplementedException();
}
public void Delete(string id)
{
throw new NotImplementedException();
}
public void PhysicsDelete(string id)
{
throw new NotImplementedException();
}
#endregion
}
}
如果你是想用Linq去实现数据库的访问 那你就建立个“public class RepositoryLinqImpl
插一句,下面的内容涉及到较多NHibernate技术细节,建议先去把李永京大哥的NHibernate之旅系列文章导航大概过一遍再来接着看,否则不大容易看懂,不过我尽量写的详细一点,至少做到让大家理解,写完也会贴上源码供大家参考 源码在下一篇下载
NHibernate的Session管理与初始化
NHibernate的Session和Asp.Net的Session是两码事,大家不要混淆了。NHibernate的Session是拿来干啥的?对用用过Linq2Sql的同学,可以把它理解成DataContext。要被持久化的对象都要放在Session里托管。Session同时也是一个缓存,比如在一定范围内你通过NHibernate ,Get了一个User ,当你再次Get同样的这个User的时候NHibernate就不会去操作数据库,而会直接从Session缓存中取出第一次获得的User。
为了充分利用NHibernate的延迟加载和它缓存机制,我们应该把Session的生命周期合理控制,比如在WCF应用中我们应该把Session绑定在一次通讯请求里,在Asp.netMVC中我们最好把Session的生命周期绑定在一次Action里。由于我们的系统设计将来会用到WCF或其他请求形式,所以我们做一个接口:
using NHibernate;
namespace Demo.HIS.FrameWork.Repository.NHb
{
public interface ISessionStorage
{
ISession Get();
void Set(ISession value);
}
}
注意添加引用NHibernate.dll,目前使用的版本是NHibernate-2.1.0
因为目前主要我们还是用Asp.net MVC 所以新建一个类HttpSessionStorage实现接口ISessionStorage:
using System.Web;
namespace Demo.HIS.FrameWork.Repository.NHb
{
public class HttpSessionStorage : ISessionStorage
{
#region ISessionStorage 成员
public NHibernate.ISession Get()
{
return (ISession)HttpContext.Current.Items["NhbSession"];
}
public void Set(ISession value)
{
if (value != null)
{
HttpContext.Current.Items.Add("NhbSession", value);
}
}
#endregion
}
}
这里我们就把NHibernate的Session的生命周期和一次Action请求( HttpContext.Current.Item)绑定在一起了,注意添加引用System.Web
我们新建一个类SessionBuilder去初始化NHibernate的Session:
{
public static class SessionBuilder
{
private static object locker = new object();
private static Configuration configuration = null;
private static ISessionFactory sessionFactory = null;
public static ISessionStorage sessionStorage { set; get; }
private static void CreateConfiguration()
{
// HibernatingRhinos.NHibernate.Profiler.Appender.NHibernateProfiler.Initialize();
//查看HQL生成的SQL
//configuration = new Configuration().Configure(System.Web.HttpContext.Current.Request.PhysicalApplicationPath + "Configuration\\hibernate.cfg.xml");
configuration = new Configuration().Configure();
}
public static Configuration Configuration
{
get
{
lock (locker)
{
if (configuration == null)
{
CreateConfiguration();
}
return configuration;
}
}
set { configuration = value;
}
}
internal static ISessionFactory SessionFactory
{
get
{
if (sessionFactory==null)
{
if (Configuration == null)
{
CreateConfiguration();
}
lock (locker)
{
sessionFactory = Configuration.BuildSessionFactory();
}
}
return sessionFactory;
}
}
public static ISession CreateSession()
{
ISession s = sessionStorage.Get();
if (s == null)
{
s = SessionFactory.OpenSession();
sessionStorage.Set(s);
}
return s;
}
}
}
注意configuration = new Configuration().Configure(); 这句读取NHibernate的配置文件,配置文件的位置必须放在网站(DemoHisSite)根目录,且命名为hibernate.cfg.xml,如果你想把这个配置文件放在别的位置或取其他名字,请看代码里注释掉的那行。
NHibernate配置文件
位置:
内容:
<configuration>
<configSections>
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
</configSections>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >
<session-factory>
<property name="connection.driver_class">NHibernate.Driver.OracleClientDriver
</property>
<property name="connection.connection_string">
User ID=dawnhis;Password=his;Data Source=dawhisdb
</property>
<property name="show_sql">true
</property>
<property name="dialect">NHibernate.Dialect.Oracle10gDialect
</property>
<property name="use_outer_join">true</property>
<property name="command_timeout">10</property>
<property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
<property name="proxyfactory.factory_class">NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle</property>
<mapping assembly="Infrastructure.Repositories"/>
</session-factory>
</hibernate-configuration>
</configuration>
具体配置节的含义,自己找资料看吧这里就不罗嗦了
在Global.asax指定ISessionStorage具体实例:
{
public class DemoHISApplication : HttpApplication
{
protected virtual void OnStart()
{
initSessionBuilder();
}
private void initSessionBuilder()
{
SessionBuilder.sessionStorage = new HttpSessionStorage();//这里创建Session实例
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default",
// Route name
"{controller}/{action}/{id}",
// URL with parameters
new { controller = "Home", action = "Index", id = "" }
// Parameter defaults
);
}
protected void Application_Start()
{
OnStart();
}
}
}
注意我这里更改了下Global.asax的引用:
源码下载:HISDemo-2.rar