从Row到Entity
数据对象是整个系统的基础,标准的DDD设计包含多种数据对象,基本上每层一种类型,如:

它的优势是各层之间可以自定义数据内容,降低各层之间的耦合,缺点是类型转换带来的资源消耗、系统的分裂感、代码及名称的繁琐,总体感觉太过于循规蹈矩。
本系统的数据对象需要满足以下使用场景:
- 支持UI层的绑定(ViewMode)
- 自动生成实体代码
- 自定义序列化内容
- 从db加载数据时高性能不使用反射
- 反序列化时自动转换实体类型
- 充血模式的实体对象
- 数据持久化时转换成Sql
为满足以上所有使用场景,采用Row作为基类,Row负责装载所有数据,内部通过数据项(Cell)集合的方式进行管理,Table是Row的集合类,并包含Columns属性,为满足领域中用到的实体对象,抽象类Entity继承自Row,所有实体类需要从Entity派生,泛型类Table<TEntity>继承自Table,泛型参数约束为实体类。

下面针对这些使用场景逐一说明:
UI绑定
Fv的数据源为Row时,FvCell数据源对应Cell,通过Row.Cells[ID]确定,绑定路径为固定的Val,Row实现接口INotifyPropertyChanged,确保双向绑定有效,如IsChanged属性绑定到保存按钮的IsEnable属性实现保存控制。

同样Cell也实现接口INotifyPropertyChanged,常用的绑定属性有:

Lv或Tv的数据源为Table时,Row对应UI中的ViewItem,实际绑定到LvRow或TvPanelItem,Cell对应具体的可视元素,Table继承自ObservableCollection<Row>,能将行数的变化实时反应到UI上。

生成实体代码
参见上述读写数据过程,业务开发时既可以使用Table/Row,也可以使用Table<Entity>/Entity,一般简单数据读写时使用Table/Row,包含业务逻辑时使用Table<Entity>/Entity,实体类代码分成两部分,一部分通过系统自动生成代码,主要为构造方法和属性,另一部分为业务逻辑,根据需求完成。为方便管理,自动生成的代码禁止修改,只修改业务逻辑的partial classs,参见业务逻辑。
自动生成实体代码需要提供实体类对应的表名,如下:

生成实体类代码如下:

该实体类主要包括构造方法和属性,公共构造方法用于手动创建实体对象,方法参数除主键外其他参数都有默认值,默认值是数据库表结构中字段的默认值;属性和字段一一对应,所以属性类型都是System命名空间的简单类型,如string long DateTime bool int byte等,但不包括枚举类型。
有两种类型需要特别关注:bool enum。
对于bool类型:
- 在mysql中自动对应
tinyint(1),无需额外处理; - oracle中自定义对应
char(1),值为1 0,需要平台特殊处理,生成实体类时自动将#bool#开始的注释当作bool类型; - sqlserver中自动对应
bit,无需额外处理; - postgresql中自动对应
bool,无需额外处理; - 增删改语句内部已特殊处理,但sql查询语句需要注意,除pg中只能当作字符进行比较,其余都可当作数字进行比较!
对于enum类型, 为了编码、查询、显示方便,平台自己增加并特殊处理了enum类型的情况:
在mysql中将无符号
tinyint(4)设计为enum类型,最多能存储256个成员,在程序中已够用,生成实体类时支持自动将#EnumName#开始的注释当作枚举类型,增加方便性和代码的可读性。如Gender(性别)枚举类型:

oracle中将
number(3)设计为enum类型;sqlserver中将
tinyint设计为enum类型;pg中将
int2设计为enum类型;
这样所有涉及对该实体类的sql查询、序列化反序列化,底层都会自动将数据转为对应的enum类型,减少很多重复的工作量,如sql查询中不再需要case语句,程序用于显示的地方显示enum成员名,保存时为byte tinyint number 或 int2值,编辑时下拉选项为所有enum成员。
oracle中number类型的c#类型的对应关系
number(1-4)Int16/shotnumber(5-9)Int32/intnumber(10-19)Int64/longnumber(20-38)decimal
postgresql中类型和c#类型的对应关系
| |
自定义序列化
数据对象需要在客户端与服务端之间、服务与服务之间传输,序列化时需要保证数据的完整、简洁,以下为Table Row序列化为json时的结构:
| |
高性能加载数据
普通ORM从数据库读取数据、加载数据时一般通过反射创建实体对象、进行属性赋值,本系统采用的方式不同,因基类Row通过数据项集合的方式管理数据,实体类属性并不直接对应变量,所以添加数据时不需要通过反射的方式对属性赋值,而是向集合增加数据项即可,参见以下:
| |
自动类型转换
为避免数据对象在客户端与服务端之间、各层之间的耦合性,采用降型传输的方式,即将实体类型降低为Row传输,反序列化时再根据实际情况创建新的实体对象,比如客户端实体类UserA,在保存实体数据时以Row类型传输到服务端,反序列化时创建实体对象UserB,类型UserA和UserB分属两端,各自独立使用,无任何代码上的耦合,但数据内容是相同的或相交的,这样既降低实体类型的耦合性也减少实体类型转换带来的冗余操作。

由上图可见,两个实体类型不论数据相同还是相交,Row都应该提供两个实体的完整数据。
转换成Sql
在生成实体类型代码时通过Tbl标签已指定映射的表名/视图名,实体对象在使用过程中自动记录数据变化及状态(如IsAdded, IsChanged),在传输过程未丢失任何信息,客户端和服务端都可以将实体对象生成要执行的Sql语句,生成Sql过程请参见TableSchema.cs文件,客户端公共Api的“写数据”就是在客户端生成的Sql语句。
insert update delete 的sql语句,增删改的sql语句都由系统自动生成。
