Rafy 领域实体框架发布后,虽然有帮助文档,许多朋友还是反映学习起来比较复杂,希望能开发一个示例程序,展示如何使用 Rafy 领域实体框架所以,本文通过使用 Rafy 领域实体框架来改造一个传统的三层架构应用程序——“服装进销存”系统,来讲解如何使用 Rafy 领域实体框架进行数据库应用程序的快速开发,以及替换为使用 Rafy 框架后带来的一些新功能。
完整示例包下载地址:http://pan.baidu.com/s/1AB9TL,其中包含本次改造前、改造后的源代码,以及转换说明文档。(下载该示例代码后,只需要修改 app.config 文件中的连接字符串中的用户名和密码后,就可以直接运行示例,程序即会自动创建数据库并成功运行!还没有下载 Rafy 框架的同学,可以在《Rafy 框架发布》文中下载完整安装包。)
接下来,将说明如何进行代码转换,使用 Rafy 来开发一个典型的数据库应用程序。(以下内容拷贝自示例包中的 PDF 文档。)
原程序说明
考虑到要更好地演示如何使用 Rafy 框架来开发一个传统的管理系统,决定挑选一个开源系统进行改造,而这个系统应该是简单、常见的三层架构,这种系统大家都比较熟悉,这样就可以更加快速的理解框架的使用了。
在开源网站上挑选了很久,免费的三层架构系统挺多,但是许多系统并不规范。一些系统虽然写着使用三层架构,但是金玉其外,败絮其中,看上去非常正式的系统,一打开源码,界面层代码中就可以看到直接编写的 SQL 语句。最终,我选用了《知名度服装进销存管理系统》,源代码下载地址:http://www.51aspx.com/Code/ZhiMingDuClothesSys。该系统三层间的调用比较严格,业务也非常简单。
系统功能描述:
- 人员:操作员管理,供应商管理,顾客管理
- 库存:库存管理,库存盘点
- 销售:服装销售,服装退货
- 服装:服装类别,服装登记
- 销售:销售统计,利润统计
技术特点:使用了三层架构设计程序,更换底层数据库类型方便。系统使用了 SqlLite 作为数据库,下载后可以直接运行。
界面截图 :
程序转换
转换方案
原系统是简单的三层架构:
而我们会使用 Rafy 推荐的架构,来改造整个系统:
对于一个依赖关系较为严格的三层系统来说,要使用 Rafy 框架来替换其中的数据访问层、业务逻辑层以及界面查询的功能,是比较简单的。本次转换,我按照以下步骤进行:
1. 理解系统需求,使用 UML 画出领域实体间的关系。
2. 添加 Rafy 领域实体项目。
3. 根据实体的关系图,在实体程序集中添加对应的实体及实体间的关系;同时也可以把旧表中的属性添加到实体中。
4. 把所有跨多表的业务逻辑转换为领域服务。
5. 依次把历史的实体删除,转而使用新的 Rafy 实体,以及其对应的实体查询、领域服务。
接下来,就正式对代码进行转换:
1. 使用 UML 进行领域建模
经分析,原系统拥有以下领域模型:
User:用户;
Company:供应商;
Customer:顾客;
GoodCategory:商品类别;
Good:商品(服装);
Stock:入库信息;
Regood:返库信息;
Bill 及 Sell:销售单据及销售明细。
它们的关系如下:
(虽然原系统中一些实体的名称取得并不合理,但是为了简化系统的转换工作,新系统中的类命名还是保持和原系统一致。)
关于哪些关系应该使用组合关系来进行设计,大家可以查看 Rafy 用户向导文档中的“领域实体框架/领域实体/实体关系”章节。
2. 升级 .NET 版本
在开始转换代码前,由于原程序使用的是 .NET 2.0 的运行时,而 Rafy 要求必须使用 .NET 4.0。所以我们需要把解决方案中的每个项目都转换为 .NET 4.0 版本。
需要注意的是,由于原程序使用的 SqlLite 只支持 2.0 版本。同时,需要把 SqlLite 替换为 .NET 4.0 的版本。
3. 添加 Rafy 领域实体项目
在解决方案中添加一个 Rafy 领域实体项目,命名为 CS(为原系统名 ClothesSys 的缩写)。
点击确定后生成的项目如下:
接下来,我们将会在这项目中添加领域实体与领域服务,来替换原程序中除界面项目以外的其它几个项目:
4. 实体转换
接下来,依次把历史的实体删除,转而使用新的 Rafy 实体。这一步,需要按照依赖关系,尽量先转换不依赖其他实体的实体,即按照以下顺序进行转换:User、Company、Customer、GoodCategory、Good、Stock、Regood、Bill 和 Sell。由于 Bill 和 Sell 有强聚合关系,所以放到最后一起转换。
(在变更每一个实体时,原代码中所有的 BLL 查询,都需要在实体仓库中编写相关的代码支持;业务逻辑则需要编写领域服务)
实体的转换分为以下几类:
- 无关系实体的转换
- 有关系实体的转换
- 组合实体的转换
5. 简单实体的转换
简单实体没有复杂的关系,只是映射一个简单的表。在转换为 Rafy 实体时,只需要把表中的所有属性都添加到实体中就可以了。在编写时,需要注意的是:
标识
转换为 Rafy 实体后,所有的实体都统一继承自 Entity 类型。Entity 类声明了 int 类型的 Id 属性作为所有实体的标识属性,这个属性会在数据库中生成一个自增长的主键列。
旧实体类上的所有主键列、唯一列,在新实体中都变成了普通列。实体属性的唯一性验证,需要放到实体之上的业务逻辑层中来完成。
属性
原实体的所有属性,在 Rafy 实体中都使用属性代码段来生成同名的实体属性代码即可。
6. BLL、DAL 层代码转换
- 转换查询数据的代码
在原代码中 BLL、DAL 两层中,都有许多的查询方法。这些方法都需要转换为新代码中对应实体的实体仓库中的查询方法。例如,原程序中通过顾客编号查询顾客的查询方法:
1: public static Customer GetCustomerById(string id)
2: {
3: Customer ct = null;
4: SQLiteParameter[] sqlparams = new SQLiteParameter[]{
5: new SQLiteParameter("@customerId",id)
6: };
7: SQLiteDataReader sdr = SQLiteHelper.GetReader("select * from T_customer where customerid=@customerId", sqlparams);
8: if (sdr.Read())
9: {
10: ct = new Customer();
11: ct.CustomerID = sdr.GetValue(0).ToString();
12: ct.CustomerName = sdr.GetValue(1).ToString();
13: ct.Socre = Convert.ToInt32(sdr.GetValue(2));
14: ct.Remark = sdr.GetValue(3).ToString();
15: }
16: sdr.Close();
17:
18: return ct;
19: }
需要转换为 Rafy 实体仓库中的新方法:
1: public Customer GetByCustomerId(string id)
2: {
3: return this.FetchFirst(new PropertiesMatchCriteria
4: {
5: { Customer.CustomerIDProperty, id },
6: });
7: }
- 转换业务逻辑代码
BLL、DAL 中,除了查询方法以外,剩下的还有一些简单对实体的增、删、改操作。这些操作已经在实体仓库基类中实现了,所以可以不用转换。
除了简单的 CRUD 操作外,系统中还有一些需要同时操作多个表的事务操作,原系统把这些业务逻辑都写到了数据层中。例如 ReGoodService.ReGoodSumbit 方法:
1: public static bool ReGoodSumbit(ReGoods regoods)
2: {
3: SQLiteParameter[] sqlparams = new SQLiteParameter[]{
4: new SQLiteParameter("@regoodsId",regoods.ReGoodsID),
5: new SQLiteParameter("@regoodsNum",regoods.ReGoodsNum),
6: new SQLiteParameter("@regoodsPrice",regoods.ReGoodsPrice),
7: new SQLiteParameter("@reNeedPay",regoods.ReNeedPay),
8: new SQLiteParameter("@reRealpay",regoods.ReRealPay),
9: new SQLiteParameter("@regoodsResult",regoods.ReGoodResult),
10: new SQLiteParameter("@userId",regoods.UserId),
11: new SQLiteParameter("@sellId",regoods.SellId),
12: new SQLiteParameter("@regoodsTime",regoods.RegoodsTime.ToString("yyyy-MM-dd HH:mm:ss"))
13: };
14:
15: string sql = "insert into T_regoods(regoodsid,regoodsNum,regoodsPrice,reNeedPay,reRealPay,regoodsResult,userId,regoodsTime,sellId) ";
16: sql+=" values(@regoodsid,@regoodsNum,@regoodsPrice,@reNeedPay,@reRealPay,@regoodsResult,@userId,@regoodsTime,@sellId)";
17: try
18: {
19: SQLiteConnection con = SQLiteHelper.GetConnection();
20: SQLiteTransaction trans = con.BeginTransaction();
21:
22: SQLiteHelper.ExecuteNonQuery(trans, sql, sqlparams);
23:
24: CustomerService.DecreaseCustomerScore(trans, regoods);
25: StockService.IncreaseStocskNumByGoodId(regoods.ReGoodsID, regoods.ReGoodsNum);
26:
27: trans.Commit();
28: return true;
29: }
30: catch (Exception ex)
31: {
32: return false;
33: throw ex;
34: }
35: }
可以看到,这段代码中,不但有业务逻辑的控制,还有数据库连接的控制,事务的控制,Sql 语句的拼装。显得非常混乱。而这种业务逻辑,在 Rafy 框架中,可以使用领域服务来实现。例如,刚才的逻辑,被替换为以下代码:
1: [Serializable]
2: public class SubmitRegoodService : Service
3: {
4: public Regood Regood { get; set; }
5:
6: protected override Result ExecuteTransaction()
7: {
8: if (Regood == null) throw new ArgumentNullException("Regood");
9: if (!Regood.IsNew) throw new ArgumentNullException("Regood");
10:
11: RF.Save(Regood);
12:
13: //修改库存
14: Regood.Good.StockSum += Regood.ReGoodsNum;
15: RF.Save(Regood.Good);
16:
17: //减少客户的积分
18: var ct = Regood.Sell.Customer;
19: if (ct != null)
20: {
21: ct.Score -= Regood.ReRealPay;
22: RF.Save(ct);
23: }
24:
25: return true;
26: }
27: }
可以看到,使用 Rafy 领域服务来实现后有以下好处:
- 整个代码非常直接地表现了业务逻辑,没有一点多余的代码。
- 使用了引用实体属性的懒加载功能,使得程序可以直接使用如 Regood.Sell.Customer 这样的强引用关系。
- 方便通用代码的封装。例如,事务的控制已经交给了服务基类来处理。
- 业务逻辑独立封装。每一个单独的业务都是一个服务对象,方便管理。为 SOA 提供了架构基础。
- 同时,使用领域服务还可以方便地直接使用 C/S 架构来部署。
7. 外键关系的转换
旧表中的外键引用关系,除了 Bill(销售单) 与 Sell(销售明细) 两个表间的关系,在设计 UML 时,都设计为实体间的引用关系。先区分清楚引用关系的可空性,然后就可以在相应实体中编写引用实体属性了。例如,Stock(库存)到 Good(商品)的关系,被转换为下面这个引用实体属性:
1: public static readonly RefIdProperty GoodIdProperty =
2: P<Stock>.RegisterRefId(e => e.GoodId, ReferenceType.Normal);
3: public int GoodId
4: {
5: get { return this.GetRefId(GoodIdProperty); }
6: set { this.SetRefId(GoodIdProperty, value); }
7: }
8: public static readonly RefEntityProperty<Good> GoodProperty =
9: P<Stock>.RegisterRef(e => e.Good, GoodIdProperty);
10: public Good Good
11: {
12: get { return this.GetRefEntity(GoodProperty); }
13: set { this.SetRefEntity(GoodProperty, value); }
14: }
8. 使用组合实体
Bill 和 Sell 分别表示销售订单、销售明细项。设计为组合实体后,在使用时,可以直接以组合实体的方式构造、保存、更新、删除,非常方便。
例如,在添加销售信息界面中的代码如下:
1: var bill = new Bill();
2: bill.UserId = userId;
3: bill.BillTime = now;
4:
5: foreach (Sell sell in list)
6: {
7: sell.CustomerId = customerId;
8: sell.SellTime = now;
9:
10: bill.SellList.Add(sell);
11: }
12:
13: var svc = new AddBillService { Bill = bill };
14: svc.Invoke();
15: if (svc.Result)
16: {
17: MessageBox.Show("提交订单成功!", "提示");
18: this.Close();
19: ucGSM.bindDgvSellRecordToday();
20: }
先构造了组合对象,然后提交给领域服务 AddBillService以执行添加销售信息逻辑。此服务代码如下:
1: [Serializable]
2: public class AddBillService : Service
3: {
4: public Bill Bill { get; set; }
5:
6: protected override Result ExecuteTransaction()
7: {
8: if (Bill == null) throw new ArgumentNullException("Bill");
9: if (!Bill.IsNew) throw new ArgumentException("Bill");
10:
11: //调用仓库保存整个销售单
12: var repo = RF.Concrete<BillRepository>();
13: repo.Save(Bill);
14:
15: //修改库存
16: foreach (var sell in Bill.SellList)
17: {
18: sell.Good.StockSum -= sell.SellNum;
19: RF.Save(sell.Good);
20: }
21:
22: //添加客户的积分
23: var ctRepo = RF.Concrete<CustomerRepository>();
24: foreach (var sell in Bill.SellList)
25: {
26: var ct = sell.Customer;
27: if (ct != null)
28: {
29: ct.Score += sell.RealPay;
30: ctRepo.Save(ct);
31: }
32: }
33:
34: return true;
35: }
36: }
转换后实体项目结构
待每一个实体修改并替换完毕后,再删除原来的传统三层项目后,解决方案中就只剩下了两个项目,一个 Rafy 领域实体项目“CS”;一个原程序中的界面层项目 “ClothesSys”。
截止到现在,已经完成了 ClothesSys 的完整转换。转换后的系统已经可以正常的运行,实现了与原系统一致的功能。
下载该示例代码后,只需要修改 app.config 文件中的连接字符串中的用户名和密码后,就可以直接运行示例,程序即会自动创建数据库并成功运行!
下一篇,将展示转换为使用 Rafy 实体框架后,带来的新功能。