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 实体,以及其对应的实体查询、领域服务。
接下来,就正式对代码进行转换:
经分析,原系统拥有以下领域模型:
User:用户;
Company:供应商;
Customer:顾客;
GoodCategory:商品类别;
Good:商品(服装);
Stock:入库信息;
Regood:返库信息;
Bill 及 Sell:销售单据及销售明细。
它们的关系如下:
(虽然原系统中一些实体的名称取得并不合理,但是为了简化系统的转换工作,新系统中的类命名还是保持和原系统一致。)
关于哪些关系应该使用组合关系来进行设计,大家可以查看 Rafy 用户向导文档中的“领域实体框架/领域实体/实体关系”章节。
在开始转换代码前,由于原程序使用的是 .NET 2.0 的运行时,而 Rafy 要求必须使用 .NET 4.0。所以我们需要把解决方案中的每个项目都转换为 .NET 4.0 版本。
需要注意的是,由于原程序使用的 SqlLite 只支持 2.0 版本。同时,需要把 SqlLite 替换为 .NET 4.0 的版本。
在解决方案中添加一个 Rafy 领域实体项目,命名为 CS(为原系统名 ClothesSys 的缩写)。
点击确定后生成的项目如下:
接下来,我们将会在这项目中添加领域实体与领域服务,来替换原程序中除界面项目以外的其它几个项目:
接下来,依次把历史的实体删除,转而使用新的 Rafy 实体。这一步,需要按照依赖关系,尽量先转换不依赖其他实体的实体,即按照以下顺序进行转换:User、Company、Customer、GoodCategory、Good、Stock、Regood、Bill 和 Sell。由于 Bill 和 Sell 有强聚合关系,所以放到最后一起转换。
(在变更每一个实体时,原代码中所有的 BLL 查询,都需要在实体仓库中编写相关的代码支持;业务逻辑则需要编写领域服务)
实体的转换分为以下几类:
简单实体没有复杂的关系,只是映射一个简单的表。在转换为 Rafy 实体时,只需要把表中的所有属性都添加到实体中就可以了。在编写时,需要注意的是:
标识
转换为 Rafy 实体后,所有的实体都统一继承自 Entity 类型。Entity 类声明了 int 类型的 Id 属性作为所有实体的标识属性,这个属性会在数据库中生成一个自增长的主键列。
旧实体类上的所有主键列、唯一列,在新实体中都变成了普通列。实体属性的唯一性验证,需要放到实体之上的业务逻辑层中来完成。
属性
原实体的所有属性,在 Rafy 实体中都使用属性代码段来生成同名的实体属性代码即可。
在原代码中 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 领域服务来实现后有以下好处:
旧表中的外键引用关系,除了 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: }
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 实体框架后,带来的新功能。