目录
介绍
问题
问题1
问题2
问题3
解决方案
问题1——智能序列化
问题2——反向引用
问题3——映射更新
说明
- 下载源3.4 MB
- 在Codeplex上下载最新版本
- 参见工作示例
让我们尝试使用最现代的技术创建一个简单的Web应用程序,看看我们可能会遇到什么问题。我将使用带有WebAPI的最新ASP.NET MVC和最新的NHibernate。请不要担心,所有技术也都适用于Entity Framework(可下载的ZIP档案也包含Entity Framework示例)。我将充分利用WebAPI的潜力——即浏览器和服务器之间的所有交互都将在JSON中异步进行。为了实现这一点,我将使用MVC JavaScript库——AngularJS。
请注意,本文仅关于后端。如果您对前端方面感到好奇,请访问此链接:带有Upida/Jeneva的ASP.NET MVC单页应用程序(前端/AngularJS)。
假设我们有一个包含两个表的简单数据库:Client 和Login,每个客户端可以有一对多的登录名。我的应用程序将包含三个页面——“客户端列表”,“创建客户端”和“编辑客户端”。“创建客户端”和“编辑客户端”页面将能够编辑客户端数据以及管理子登录列表。您可以在此处查看其工作方式。
首先,让我们定义域(或模型)类(映射在hbm文件中定义):
public class Client
{
public virtual int? Id { get; set; }
public virtual string Name { get; set; }
public virtual string Lastname { get; set; }
public virtual int? Age { get; set; }
public virtual ISet Logins { get; set; }
}
public class Login
{
public virtual int? Id { get; set; }
public virtual string Name { get; set; }
public virtual string Password { get; set; }
public virtual bool? Enabled { get; set; }
public virtual Client Client { get; set; }
}
现在,我可以创建数据访问层。首先,我必须将其注入与基础DAO类NHibernate的 SessionFactory,并且定义了基本的DAO操作:Save,Delete,Update,Load,Get等。
public class Daobase : IDaobase
{
public ISessionFactory SessionFactory { get; set; }
public void Save(T entity)
{
this.SessionFactory
.GetCurrentSession()
.Save(entity);
}
public void Update(T entity)
{
this.SessionFactory
.GetCurrentSession()
.Update(entity);
}
public ITransaction BeginTransaction()
{
return this.SessionFactory
.GetCurrentSession()
.BeginTransaction();
}
/* others basic methods */
}
我只有一个DAO类-—— ClientDao。
public class ClientDao : Daobase, IClientDao
{
public Client GetById(int id)
{
return this.SessionFactory
.GetCurrentSession()
.CreateQuery("from Client client left outer join fetch
client.Logins where client.Id = :id")
.SetParameter("id", id)
.SetResultTransformer(Transformers.DistinctRootEntity)
.UniqueResult();
}
public IList GetAll()
{
return this.SessionFactory
.GetCurrentSession()
.CreateQuery("from Client client left outer join fetch client.Logins")
.SetResultTransformer(Transformers.DistinctRootEntity)
.List();
}
}
DAO完成后,我们可以切换到服务层。服务通常负责打开和关闭事务。我只有一个服务类。它以受尊重的DAO类注入。
注意,Save和Update方法接受一个Client对象及其子级Logins,因此使用NHibernate级联(同时保留父级和子级)执行保存或更新。
public class ClientService : ICLientService
{
public IClientDao ClientDao { get; set; }
public Client GetById(int clientId)
{
Client item = this.ClientDao.GetById(clientId);
return item;
}
public List GetAll()
{
List items = this.ClientDao.getAll();
return items;
}
public void Save(Client item)
{
using(ITransaction tx = this.clientDao.BeginTransaction())
{
/* TODO: assign back-references of the child
Login objects - for each Login: item.Login[i].Client = item; */
this.ClientDao.Save(item);
tx.Commit();
}
}
public void Update(Client item)
{
using(ITransaction tx = this.clientDao.BeginTransaction())
{
Client existing = this.clientDao.GetById(item.getId());
/* TODO: copy changes from item to existing (recursively) */
this.ClientDao.Merge(existing);
tx.Commit();
}
}
}
让我们谈谈控制器。我将有两个控制器——一个用于HTML视图(MVC控制器),另一个用于JSON请求(API控制器)。它们都将被称为ClientController,但是将驻留在不同的命名空间中。MVC控制器将来自System.Web.Mvc.Controller,而API控制器将来自System.Web.Http.ApiController。MVC控制器将负责显示正确的视图。外观如下:
public class ClientController : System.Web.Mvc.Controller
{
public ActionResult Index()
{
return this.View();
}
public ActionResult Create()
{
return this.View();
}
public ActionResult Edit()
{
return this.View();
}
}
API控制器稍微复杂一点,因为它负责与数据库的交互。它被注入相应的服务层类。
public class ClientController : System.Web.Http.ApiController
{
public ClientService ClientService { get; set;}
public IList GetAll()
{
return this.ClientService.GetAll();
}
public Client GetById(int id)
{
return this.ClientService.GetById(id);
}
public void Save(Client item)
{
this.ClientService.Save(item);
}
public void Update(Client item)
{
this.ClientService.Update(item);
}
}
现在,我们几乎拥有了所需的一切。MVC控制器将为我们提供HTML和JavaScript,它们将与API控制器进行异步交互并从数据库中获取数据。AngularJS将帮助我们将获取的数据显示为漂亮的HTML。我假设您熟悉AngularJS(或KnockoutJS),尽管本文并不那么重要。您唯一必须知道的是——每个页面都以静态HTML和JavaScript加载(没有任何服务器端脚本),在加载后,它与API控制器进行交互,以通过JSON从JSON异步加载数据库中所需的所有数据。而AngularJS有助于显示JSON作为美丽的HTML。
问题现在,让我们谈谈当前实现中面临的问题。
问题1第一个问题是序列化。从API控制器返回的数据被序列化为JSON。您可以在以下两种API控制器方法中看到它:
public class ClientController : System.Web.Http.ApiController
{ ....
public IList GetAll()
{
return this.ClientService.GetAll();
}
public Client GetById(int id)
{
return this.ClientService.GetById(id);
}
Client类是一个域类,它是包裹着NHibernate的包装器类。因此,对其进行序列化可能会导致循环依赖并导致StackOverflowException。但是还有其他一些小问题。例如,有时候我只需要Id和Name 字段可以在JSON中出现,有时候我需要所有的字段。当前的实现不允许我做出决定,它将始终序列化所有字段。
问题2如果看一下ClientService类方法Save,您会发现缺少一些代码。
public void Save(Client item)
{
using(ITransaction tx = this.clientDao.BeginTransaction())
{
/* code that assigns back-references of the child Login objects */
this.ClientDao.Save(item);
tx.Commit();
}
}
这意味着,在保存Client对象之前,您必须设置子Login对象的反向引用。每个Login类都有一个字段——Client,它实际上是对父Client对象的反向引用。因此,为了节约Client与Logins一起使用级联保存,你必须建立这些领域的实际父实例。当Client从JSON反序列化,它不具有反向引用。在NHibernate用户中,这是一个众所周知的问题。
问题3如果查看一下ClientService类方法Update,您将看到也缺少一些代码。
public void Update(Client item)
{
using(ITransaction tx = this.ClientDao.OpenTransaction())
{
Client existing = this.ClientDao.GetById(item.getId());
/* code that copies changes from item to existing */
this.ClientDao.Merge(existing);
}
}
我还必须实现将字段从反序列化的Client对象复制到相同的Client对象的现有持久性实例的逻辑。我的代码必须足够聪明才能通过子级Logins。它必须将现有的登录名与反序列化的登录名匹配,并分别复制字段。它还必须追加新添加的Logins,并删除丢失的内容。完成这些修改后,该Merge()方法将所有更改持久化到数据库。因此,这是相当复杂的逻辑。
在下一节中,我们将使用Jeneva.Net解决这三个问题。
解决方案 问题1——智能序列化让我们看看Jeneva.Net如何帮助我们解决第一个问题。在ClientController中有两个方法返回Client的对象——GetAll()和GetById()。该GetAll()方法返回Clients的列表,该列表显示为网格。我不需要Client在JSON 中显示对象的所有字段。该GetById()方法在“编辑客户端”页面上使用。因此,此处需要完整的Client信息。
为了解决此问题,我必须遍历返回对象的每个属性,并将NULL值分配给不需要的每个属性。这似乎很艰苦,因为在每种方法上我都必须做不同的事情。Jeneva.Net为我们提供了可以为我们做的Jeneva.Mapper类。让我们使用Mapper类来修改服务层。
public class ClientService
{
public IMapper Mapper { get; set; }
public IClientDao ClientDao { get; set; }
public Client GetById(int clientId)
{
Client item = this.ClientDao.GetById(clientId);
return this.Mapper.Filter(item, Leves.DEEP);
}
public List GetAll()
{
List items = this.ClientDao.getAll();
return this.Mapper.FilterList(items, Levels.GRID);
}
.....
它看起来非常简单,Mapper可以获取目标对象或对象列表并生成它们的副本,但是每个不需要的属性会被设置为NULL。第二个参数是代表序列化级别的数值。Jeneva.Net具有默认级别,但您可以自由定义自己的级别。
public class Levels
{
public const byte ID = 10;
public const byte LOOKUP = 20;
public const byte GRID = 30;
public const byte DEEP = 40;
public const byte FULL = 50;
public const byte NEVER = byte.MaxValue;
}
最后一步是用相应的级别装饰域类的每个属性。我将使用DtoAttribute中的Jeneva.Net装饰Client和Login类属性。
public class Client : Dtobase
{
[Dto(Levels.ID)]
public virtual int? Id { get; set; }
[Dto(Levels.LOOKUP)]
public virtual string Name { get; set; }
[Dto(Levels.GRID)]
public virtual string Lastname { get; set; }
[Dto(Levels.GRID)]
public virtual int? Age { get; set; }
[Dto(Levels.GRID, Levels.LOOKUP)]
public virtual ISet Logins { get; set; }
}
public class Login : Dtobase
{
[Dto(Levels.ID)]
public virtual int? Id { get; set; }
[Dto(Levels.LOOKUP)]
public virtual string Name { get; set; }
[Dto(Levels.GRID)]
public virtual string Password { get; set; }
[Dto(Levels.GRID)]
public virtual bool? Enabled { get; set; }
[Dto(Levels.NEVER)]
public virtual Client Client { get; set; }
}
装饰完所有属性后,可以使用Mapper类。例如,如果我使用Levels.ID调用Mapper.Filter()方法,那么将仅包含标记为ID的属性。如果我用Levels.LOOKUP调用Mapper.Filter()方法 ,则将包括标记为ID和LOOKUP的属性,因为ID小于LOOKUP(10
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?