冬Blog

醉心技术、醉心生活
posts - 88, comments - 674, trackbacks - 9, articles - 1
  博客园 :: 首页 :: 新随笔 ::  :: 订阅 订阅 :: 管理

上一篇中,我们完成了数据库的访问,今天我们来看看系统设计的最后一部分——UI层。加入了UI层之后,系统设计会变成这个样子:

system_design

这也就是系统最终的结构图。这个图上新添加的两个项目,一个是UI,一个是WebSite。其中前者依赖于业务逻辑和数据访问,提供统一的界面处理,而WebSite仅包含Aspx页面。需要注意的是,上图中箭头表示依赖或调用,而这个关系是具有传递性的,比如UI依赖于Business,而Business依赖于Common,则UI自然就依赖于Common。

将UI和WebSite分写在两个项目中,是我个人的一个习惯。其中UI主要包含页面基类和Helper类,还有数据库的获取方法,对于多种界面(比如既提供Web也提供WinForm)的系统,这一层次的抽象是必要的,而对于只提供Web访问的系统,把UI层放在WebSite中的App_Code文件夹下也无不可。

UI层中,目前就有两个核心类,一个是所有页面的基类,另一个是数据库生成类,其类结构图如下:

UI

其中DatabaseGateWay的作用就是读取Web.Config中数据库链接节点的连接字符串,并调用DataAccess层的方法,构造新的数据库:

   1:  namespace DongBlog.UI
   2:  {
   3:      /// <summary>
   4:      /// 数据库
   5:      /// </summary>
   6:      public class DatabaseGateWay
   7:      {
   8:          private const string DatabaseConnectionConfigurationName = 
"DongBlogDatabaseConnectionString";
   9:   
  10:          /// <summary>
  11:          /// 取得新的数据库
  12:          /// </summary>
  13:          /// <returns>新数据库</returns>
  14:          public static IDatabase GetNewDatabase()
  15:          {
  16:              return Database.New(ConfigurationManager
.ConnectionStrings[DatabaseGateWay.DatabaseConnectionConfigurationName]
.ConnectionString);
  17:          }
  18:      }
  19:  }

该类的GetNewDatabase()静态方法,是类型安全的,也是唯一使用到了DataAccess层具体实现的地方,系统的其它部分都只了解IDatabase而不了解其实现,更换不同的DataAccess只需要修改此方法。

PageBase类是所有Aspx页的基类,该类在该系统中只提供了数据库访问功能,即一个页面对应一个数据库访问,代码如下:

  11:  namespace DongBlog.UI
  12:  {
  13:      /// <summary>
  14:      /// 所有页面的基类
  15:      /// </summary>
  16:      public class PageBase : Page
  17:      {
  18:          private IDatabase _Database = DatabaseGateWay.GetNewDatabase();
  19:   
  20:          /// <summary>
  21:          /// 取得数据库访问
  22:          /// </summary>
  23:          protected IDatabase Database
  24:          {
  25:              get { return _Database; }
  26:          }
  27:      }
  28:  }

一个页面对应一个IDatabase,对于Linq来说,就是一个页面对应一个DataContext,这保证页面生存周期中的所有业务实体都来源或依附于同一个DataContext,避免了跨DataContext传递实体的问题。在Linq中,一个DataContext产生的Entity交由另一个DataContext中使用是一件非常麻烦的事情,必须保证实体必须使用Attach方法附加到新的DataContext上,如果不附加,则新的DataContext会认为该Entity是new出来的,这会导致再数据库中插入一条新的记录,而不是与现存记录建立关联,这个Bug很难调试,因为不会显示任何错误。我们举个例子说明这个问题。

我们经常会在用户登录的时候,在Session中储存登陆的用户:

User user = GetLoginUser(username, password);
if (user != null)
    Session["CurrentUser"] = user;
else
    Response.Redirect("Login.aspx");

这样以后我们就可以用Session["CurrentUser"]取得当前用户了。当用户新建一篇日志时,我们会使用类似下面的代码:

Blog blog = new Blog();
blog.Creator = Session["CurrentUser"] as User;
blog.Save(Database);

问题来了:上面两个代码通常来说不是发生在一个页面中,也就是说,从数据库中读取该user的DataContext早已不存在了,而新的、用于存储Blog的DataContext会认为这个blog相关的Creator是一个“新”的User,所以会在数据库中插入一条新的User记录,而不是和原来的User记录建立关联!为了避免这个情况,我们必须用Attach方法将Session["CurrentUser"]附加到新的DataContext中,上面的代码要这么改:

Database.Users.Attach(Session["CurrentUser"] as User);
Blog blog = new Blog();
blog.Creator = Session["CurrentUser"];
blog.Save(Database);

但是假设User还有关联的实体,比如Role、Power之类的东西,就需要全部附加上!否则就会插入新的关联记录!这几乎是不可能的,所以我们要保证每一个页面周期中所有的实体来源于同一个DataContext,对于上面这个例子,解决办法是不保存User,而保存UserID:

User user = GetLoginUser(username, password);
if (user != null)
    Session["CurrentUserID"] = user.ID;
else
    Response.Redirect("Login.aspx");

这样我们就可以在添加日志时这样写:

Blog blog = new Blog();
blog.Creator = Database.Users.GetByID(Convert.ToInt32(Session["CurrentUserID"]));
blog.Save(Database);

进一步,我们可以在PageBase中加载User,考虑在PageBase中存在以下代码:

private User _CurrentUser;
public User CurrentUser
{
    get
    {
        if(Session["CurrentUserID"] == null)
            return null;

        if(_CurrentUser == null)
            _CurrentUser = Database.Users.GetByID(Convert.ToInt32(Session["CurrentUserID"]));
        return _CurrentUser;
    }
}

这样我们就可以使用以下的代码建立日志了:

Blog blog = new Blog();
blog.Creator = CurrentUser;
blog.Save(Database);

关于PageBase的用处还有很多,包括但不限于以下这些:

  1. 构造Database。
  2. 构造当前User。
  3. 判断当前用户是否具有该页面的访问权限。
  4. 根据当前用户定制页面:加入不同的Css等。

另外,基于PageBase的扩展方法,将对所有页面有效,这点常常用于执行JavaScript。

下一篇中,我们开始编写我们第一个页面。

代码下载

Feedback

#1楼    回复  引用  查看    

2008-05-17 20:29 by brightwang      
database难道不应该用单例模式吗?如果是用NHibrenate的话,sessionfactory每一次都NEW就不好了,LINQ没实战过。。。

#2楼 [楼主]   回复  引用  查看    

2008-05-17 20:34 by 冬冬      
@brightwang
Linq中的DataContext(也就是这里说的Database),相当于NHibernate中的Session,而不是SessionFactory,呵呵。Linq中貌似没有NHibernate中类似SessionFactory的东西。

#3楼    回复  引用  查看    

2008-05-17 22:41 by Atpking      
很菜,只能理解部分。。。
另外想请问下,您用的UML工具是什么?

#4楼    回复  引用  查看    

2008-05-17 23:49 by yzlhccdec      
我只用LINQ做查询。。LINQ本身设计出来就是一种查询语言吧。
更新和删除老老实实用SP或者T-SQL

#5楼    回复  引用  查看    

2008-05-18 00:06 by KymoWang      
@Atpking
没看前面的几节吗?系统设计图用的是Vs.Net的分布式系统设计。类图用的Vs.Net的Class Diagram。

#6楼    回复  引用  查看    

2008-05-18 18:02 by 锦瑟无端五十弦      
实体还是尽可能简单吧。
Blog实体就保存一个userid不就好了,为什么一定要保存一个user实体,然后user实体再保存什么role,power,完全没有必要,全都保存id,需要的时候再通过id去取相应实体。

另外,当我save一个blog的时候,你说相应的user实体以及user的role等实体,数据库会全部进行一次级联更新(或插入)?这个逻辑是linq自带的,还是你的代码中手动实现的?

#7楼 [楼主]   回复  引用  查看    

2008-05-18 18:10 by 冬冬      
@锦瑟无端五十弦
Blog.Creator表示一个User是典型的ORM的用法,你可能SQL用的比较熟练,不太习惯这种用法,但是其实很方便,也是一个趋势。

Linq的级联插入和更新是自带的,通过数据上下文跟踪实体变化,然后通过Submit方法持久化所有的变化进数据库。

建议你先找实际写写Linq的代码。

#8楼    回复  引用  查看    

2008-05-18 19:23 by airwolf2026      
总算看的出一点眉目了..Linq应该不是玩具?

#9楼 [楼主]   回复  引用  查看    

2008-05-18 19:29 by 冬冬      
@airwolf2026
呵呵,Linq还是很好用的,特别是复杂的查询,非常方便。现在这九篇中其实还没有发挥出Linq的威力来。
不过有种说法是:微软的东西要到第二版才好用,不知道Linq第二版会是啥样子~~~:)

#10楼    回复  引用  查看    

2008-05-19 02:54 by 锦瑟无端五十弦      
我只用linq做过一些简单的linq to object和linq to sql的测试,但我感觉对于复杂的查询,它比起sql语句来还是有点力不从心。比如sql中很方便的exists和in,特别是用于两表自关联的查询,以及批量操作,linq实现起来还是比较困难,而且性能上也有很大的问题。

关于一个实体保存另一个实体作为自己的成员,我知道很多orm中都是这么做的
,但看你的代码:
blog.Creator = Database.Users.GetByID(Convert.ToInt32(Session["CurrentUserID"]));

这里要到数据库里取一次user是否毫无必要,是否有点为了orm而orm的感觉?
如果我只保存userid,那么这里直接赋个int就是,什么时候需要取到user的数据了,我再手动去调一次,虽然代码上繁琐了,但避免了很多不必要的数据连接。而且把级联更新交由linq自己去解决,我如何根据业务需要去控制数据并发和事务处理的逻辑?好像不如存储过程灵活?愿与您探讨。

#11楼 [楼主]   回复  引用  查看    

2008-05-19 09:06 by 冬冬      
@锦瑟无端五十弦
你说的很有道理。

其实通常Blog不仅仅有Creator,还会有CreatorID,因此,可以用blog.Creator = Database.Users.GetByID(Convert.ToInt32(Session["CurrentUserID"]));,也可以用blog.CreatorID=Convert.ToInt32(Session["CurrentUserID"]),他们之间只等效的。

而我之所以(也是大多是ORM框架)使用blog.Creator,是从面向对象的方面考虑的,“日志的作者是当前用户”翻译过来不正好是“blog.Creator = CurrentUser;”吗?相比较,“blog.CreatorID = CurrentUserID;”似乎更像是是一个实现的细节,有点儿背离ORM的初衷。

另外,使用对象关系在查询的时候非常方便,比如典型的两表联合查询:“找出所有性‘袁’的用户的日志”可以这么写:
Database.Blogs.Where(b=>b.Creator.Name.StartWith("袁")).ToList();这种查询可以写的非常复杂,省去了手工拼写SQL的麻烦,更重要的是,它是强类型的,由编译器检查字段和类型,很少出错。这也是Linq最好用的地方——Linq毕竟设计用来查询的,不是吗?:)

必须承认的是,Linq在批量操作上的不足,比如“更新某一个用户的所有Blog的分类”,很难有高效率的实现,通常还是使用SQL。但这应该是系统优化的任务。而优化常常是不利于系统结构,会是结构变差。

希望再次听到您的看法。

#12楼    回复  引用    

2008-05-20 10:48 by dddd [未注册用户]
更新太慢了!快点!

#13楼    回复  引用  查看    

2008-05-21 10:28 by 锦瑟无端五十弦      
如果是linq自己生成的实体类,blog有2个属性CreatorId和Creator,当我新增一篇blog时,我只需要取得当前用户id,直接给creatorid赋值,不设置creator,那么不管上下文如何,就不会插入新的creator,这样岂不是挺好?

不知道为什么您一定要给它的creator赋值呢?
blog.Creator = Database.Users.GetByID(Convert.ToInt32(Session["CurrentUserID"]));

期待您的这个系列早点写完,并能提供源代码让大家学习下.

#14楼 [楼主]   回复  引用  查看    

2008-05-21 10:55 by 冬冬      
@锦瑟无端五十弦
关于通过对象进行赋值,而不是用id的愿意,前面我解释过,可能没有说明白:

“之所以(也是大多是ORM框架)使用blog.Creator,是从面向对象的方面考虑的,“日志的作者是当前用户”翻译过来不正好是 “blog.Creator = CurrentUser;”吗?相比较,“blog.CreatorID = CurrentUserID;”似乎更像是是一个实现的细节,有点儿背离ORM的初衷。”

通常,生成的代码是不允许给blog.CreatorID赋值的,因为ID是和数据库相关的,只是为了Linq而加的,对于业务层,应该忽略它。除非你把“每个对象包含一个ID”看作是业务逻辑的一部分。呵呵,这里比较绕,而且仁者见仁智者见智。

另外,我不会用一般不会用:
Database.Users.GetByID(Convert.ToInt32(Session["CurrentUserID"]));
而是将CurrentUser抽象到PageBase中,使用:
blog.Creator = CurrentUser;
抽象到PageBase中还有其他含义,逻辑上讲:一个页面对应一个当前用户是符合设计思路的。

关于更新的问题,我也比较头疼,最近很忙,周一到周五都在加班,很难抽出时间静下来写,大概只有周六周日了。实在是不好意思,我会尽快的。:)

#15楼    回复  引用  查看    

2008-05-23 21:28 by KymoWang      
@冬冬
这个周末一定要完成呀!

#16楼    回复  引用  查看    

2008-05-26 07:16 by 廉毅      
为什么不更新了呢?

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-05-27 17:37 编辑过
 
另存  打印