之前,XCode总是若隐若现,耐性好的同学想知道它还有啥特点,沉不住气的则认为不过是CURD耳!
XCode开发模式是灵魂,XCode组件通过具体实现对其支持!
XCode的特点如下:
0、基本的CURD功能
实在想不出来不支持CURD的ORM算不算ORM;也实在想不出来仅有CURD的ORM算不算ORM。因而,这是0号功能!
XCode的CURD通过反射实体类生成查询和操作SQL实现,数据库结构信息通过特性附在实体类上。之所以选择SQL而不是DbCommand,因为XCode的实体层和数据访问层是分开的,目前是为了实现一级缓存,将来会在这里实现分布式数据访问。
1、完美支持ObjectDataSource
XCode实现充血模型(胀血模型)的实体类,提供ObjectDataSource需要的所有方法和参数,特别支持分页和排序功能!
2、全面分页支持
只有从小处开始培养分页的思想,任何查询都指定所需获取数据范围,才能保证系统数据变大时系统不会拓机。
XCode的分页以任意查询语句为基础,支持统计等非常复杂的查询分页。并且会根据当前数据库类型以及版本选择最佳分页方案。
详见《撬动千万级数据》
3、实体集合支持
实体集合EntityList<TEntity>继承自List<TEntity>,提供了实体的批量操作。实际上还是通过遍历集合逐个进行实体操作,因为充血模型的实体类可能是通过重载修改CURD的行为,所以不能使用一个SQL语句操作一批实体,XCode不会做这种可能会影响使用的小把戏。
实体集合还提供了一些方便查询和排序的简便方法,实体缓存中将会大量使用。
4、万能的一级缓存
一级缓存由数据访问层实现,以查询SQL为键,返回的数据集为值,查询的表名数组为依赖项,进行缓存。执行SQL时同样需要指定影响的表名数组,从而清空所有影响到的缓存。
缓存生命周期分为请求级、定期和永久三种。如果只有当前应用系统使用该数据库,并且服务器内存足够大,可以开启永久缓存,在数据没有更新时,基本缓存在内存中,适用于网站;一般设定一个缓存过期期限,定期清理缓存,适用于内存不是很足,或者允许数据更新有一定延后的分布式系统;如果上面两种均不合适,而又需要提高系统响应速度时,可以采用请求级缓存,在该次页面请求生命周期内对数据进行缓存,特别适用于在不同地方进行相同查询的场合(有时候是程序员功力不够写错的)。
XCode的开发模式建议使用尽可能简单的单表查询,实际大部分查询都是简单SQL语句,缓存命中率很高!
5、漂亮的实体缓存
实体缓存又成为二级缓存。尽管有了一级缓存,但它只是缓存了数据集而已,使用的时候还是要加载数据集成为实体集合。
实体缓存通过指定一个查询实体集合的方法,一般是查询本表所有实体的FindAll()方法,把查询返回的实体集合缓存起来(默认缓存一分钟),供上层代码使用。取数据的过程完全是隐式进行,实体缓存通过提供一个静态实体集合供上层代码查询,实体集合属性内部进行查询数据和缓存过期检查等操作。查询方法通过委托传递,还有参数可以指定是否异步获取缓存数据。
总之,使用实体缓存就是使用一个静态的实体集合属性(大多数时候使用默认配置,所以不需要配置),进行查询排序等操作,无需关心缓存的具体实现。当然,对实体进行修改操作时将会清空缓存,保证数据的新鲜性。
单表数据量不大(建议1000以下,不超过10000),并且极少改动的数据表使用实体缓存。比如权限、角色、菜单、系统参数等使用非常频繁的数据。实体缓存的命中率可以高达99.98%
6、飘逸的单对象缓存
单对象缓存又层三级缓存,因为它一般构建于二级缓存之上。对于数据量大(大概几万到几十万),并且查询又非常频繁的数据表,任意两行数据之间关系不大时,可以酌情使用单对象缓存。比如会员表,一般会根据账号进行查找,并且很频繁,此时可以以账号为键,会员对象为值,对数据进行缓存。设置与实体缓存类似。取数据时先去缓存中找,有则直接返回,没有则调用预设的方法进行查询,并且缓存起来。
单对象缓存里面的实体对象,修改数据时,如非必要,不要手工调用更新方法,单对象缓存有自动保存的功能。该特性适用于更新非常频繁的场合,比如在线用户表,可以让多次更新积累在一起,然后最后自动更新一下。
7、出色的性能
XCode不支持多表查询,一般的多表关联查询都可以拆分成为1+X的多次单表查询。比如学生和班级的关联查询,可以先查10个学生信息,再分别查他们的班级信息,就成了1+10=11次单表查询。每一次单表查询肯定会比多表关联查询要快,但是11次单表查询很多时候都会比一次多表关联查询慢。
回过头来看看上面的缓存,如果这10个学生是同班,那么在一级缓存的作用下,实际查询数据库将会是1+1=2,后面9次班级查询被一级缓存拦截了。在高并发的系统中,后面这个1就趋向于0了,因为缓存是全局共享的。
再来看看实体缓存,一个学校的班级不会很多,符合条件使用实体缓存。也就是一次性读取所有班级信息,缓存到实体集合中。即使在最糟糕的情况下,10个学生都处于不同班级,实体缓存也是百分百命中,实际查询仅仅是对学生表的单表查询,此时肯定比多表关联查询快。
在学生资料界面等地方,学生表查询是非常频繁的。显然,这是一个非常适用单实体缓存的场合。学生附属属性(关联表)等信息,可以通过扩展属性“挂”在学生实体对象上,“享受”到缓存的待遇。
数据库层面也有一个缓存,可以算是0级缓存吧。我们所有的查询都是单表查询,对数据库而言非常简单,同时,因为简单的SQL,数据库缓存命中率极高,并且非常便于建立索引进行优化。
基于分页和缓存,XCode提供了一套高性能的解决方案,这种方案远胜于传统的多表关联查询,并且是系统并发越高,这种优势越明显。
8、脏数据支持
在更新数据的时候,往往业务需求是只更新我们修改过的数据。比如会员资料修改表单,可以设置会员信息等资料,但是不能修改最后登录时间。这个时候,我们就需要知道哪个属性的数据被我们修改过!
XCode的实体类中,每个数据属性的set方法,都会先调用OnPropertyChange方法,其实就是为了设置该字段的脏属性,说明这个字段的数据曾经被修改过。生成Update语句的时候,只修改带有脏属性的字段。
实体类中,除了直接修改属性外,还可以通过索引器进行修改,两种的区别就在于通过索引器修改属性时,不影响脏数据设置。实际上,加载数据行到实体类中,使用的正是索引器,所以刚加载完成数据的情况下脏数据是空的。
9、多数据库支持
(MSSQL2000、MSSQL2005/2008、Oracle、MySQL、Access、SQLite)
与大多数ORM一样,XCode通过接口的方式支持多种数据库。在XCode中,为每一个数据库实现了一个数据库操作类,继承自数据库接口。数据访问层DAL根据数据库连接的配置识别是哪一种数据库,然后创建该数据库操作类的实例,并通过操作接口来操作数据库。
数据库操作类以Access数据库为蓝本,设计了一个基类,其它数据库操作类仅需要继承该类,重载功能点不一致的属性和方法,大大减小了操作类的大小。
数据库操作接口包含的功能有:查询、执行、分页、事务、获取架构、DDL操作、数据库版本等。实际上,各个数据库的差异点都可以设计在操作接口中,而上层代码根本不需要改动。
很多ORM都为各个数据库的差异大而苦恼,XCode开发模式则不然。我们的原则是一切从简,只使用SQL,不适用DbCommand和存储过程。而所使用的SQL,基本上也是标准SQL,不会使用数据库特性,并且都是单表操作。当然,这种方法也不是万能的,不得已的时候,可以在业务层判断当前数据库类型,根据不同数据库编写不同的SQL,但自XCode使用以来,还没需要这样做过。
10、获取数据库架构
(DAL.Tables)
在XCode中,数据库架构主要包含XTable和XField类,顾名思义,它们代表着表和字段信息。数据访问层DAL中有个成员属性Tables可以取得该数据库连接的所有表信息,也就是一个XTable集合。同理,每一个XTable中,都会有一个XField集合。
这样设计,简单明了,使用者可以很容易的找到自己需要的东西。我们的代码生成器XCoder就是依赖于XCode来获取数据库架构的。有了这个功能,人人都可以写自己的代码生成器了!
11、反向生成数据库架构
(DatabaseSchema)
这是一个很另类的功能,极少有ORM提供。在开发和维护的过程中,难免需要修改表结构,重新生成实体类(仅生成实体类数据文件部分)。在团队开发的时候,如果不是共用数据库,则还需要通知队员做相应的修改。维护的时候,还需要到生产环境做更新,如果客户不允许直接操作数据库,那就更麻烦了。
在数据库操作接口中,其中一个功能就是DDL操作,各个数据库进行重载后,可以使用DDL语句操作数据库的结构。常用的功能有:创建表、修改字段属性、添加字段、删除字段等。
XCode除了会从数据库生成数据库架构外,还会从实体类生成一套数据库架构,然后进行对比,发现存在差异后,直接修改或者写日志提醒(由设置决定)。修改开关没打开时,仅仅写日志提醒,日志中写出了完整的用于修正数据库架构的DDL语句,拿到数据库中执行即可。
基于XCode开发的系统,在发布的时候,从来不带数据库,因为XCode会自动根据连接字符串创建数据库、数据表和字段。即使开发使用的是A数据库,发布的时候修改数据库连接字符串为B数据库,XCode将会按照B数据库的规范来创建数据库。如果系统发布还需要附带数据,那就不可能做到发布时更换数据库了,除非发布多个数据库的版本。
也许有人会说,不带数据库的话,初始化的数据怎么办?在XCode的开发理念中,建议在实体类增加静态构造函数,用于检测数据表数据,如果没有数据时,是否需要创建一个默认数据,比如在管理员表创建一个用户名和密码都是admin的管理员。
12、动态修改连接和表名
(Meta.ConnName,Meta.TableName)
数据量增大到一定程度时,很多企业的方案都是给数据表改名或者把该表迁移到别的数据库中去,仅用于查询统计。
在XCode中,生成实体类时,就指定了实体类所对应的表名,但是我们并不需要为多个具有相同表结构的表生成多个实体类,因为实体类可以动态修改所指向的表名,使得操作的目标表发生改变。为了避免多线程环境所带来的影响,该修改仅影响当前线程。
连接名的修改方式与表名相同。
13、弱类型访问
(IEntity,IEntityOperate)
有时候,我们并不知道需要操作的是哪一个实体类,只有在运行时才能确定下来。常用的做法就是反射!
为了避免不必要的性能损耗,以及避免很不美观的编码设计方式,XCode提供了弱类型访问的能力。可以通过指定类型或者表名,反射找到实体类,创建一个IEntityOperate操作对象,从而完成对实体类的各种操作。
IEntityOperate所提供的方法跟实体类的静态方法基本一致,在使用上不会遇到任何困难。IEntityOperate进行数据查询时,返回的是IEntity集合,因为每一个实体类,都实现了IEntity接口,足以完成基本的CURD操作。
14、动态生成代码
(CodeDOM,内存实体)
在弱类型访问的支持下,有些简单的数据库操作并不一定需要生成实体类,XCode在找不到实体类时,将会根据表架构在内存中生成一个实体类,然后编译使用。
动态生成的另一目标是让使用者通过调用一些方法来生成实体类代码,而不是一定要通过XCoder来生成。
15、扩展加载
(把查询中的字段映射到扩展属性)
XCode支持的是充血模型,从面向对象的角度上来讲,这个对象的所有特点(属性)和能力(方法)都应该在实体类上实现。除了基本的与数据库字段对应的数据属性外,还有一些跟别的实体对象关联的属性,我们称之为扩展属性。比如:Article.Board.Manager.UserName,可以直接得到一个帖子的版主的用户名。
XCode不支持多表关联查询,但是它有扩展属性,所有的多表关联查询,都可以通过扩展属性来编码实现。Article中的Board扩展属性,是在使用的时候才去加载的,加上Board可以使用实体缓存,基本上没有数据库操作。Manager不能使用实体缓存,但是它作为扩展属性“挂”在Board上,间接“享受”了缓存。
还可以编写一个普通的属性作为扩展属性,然后执行查询的时候,通过selects参数把数据映射到该扩展属性上。比如:增加一个Total的整型属性,然后执行查询Article.FindAll("BoardID=123",null,"Count(*) as Total",0,1),该查询是取得栏目编号为123的所有帖子数,然后把结果映射到Total属性上,返回的记录集只有一个实体对象,该实体对象的Total属性就是所要查询的帖子数,此时别的属性没有意义。
该功能一般用于查询统计中。使用一个实体来表现数据,比数据集方便多了。
扩展属性是充血模型所特有的东西,也是相对于贫血模型(含失血模型)的最大优势所在!
16、泛型基类模型
(Entity<TEntity>)
XCode从v1.2起,就进入了第二代,关键点就在于泛型基类Entity<TEntity>的使用。
在第一代XCode中,因为充血模型,实体类上要附带大量的方法,而当它们的返回类型是实体类或者实体类集合时,这些方法就必须实现于实体类的代码中,实际上是通过代码生成器来生成。
在第二代XCode,引入了泛型基类技术,实体类通过泛型参数TEntity指定最终返回类型,编写查询方法的时候,返回类型使用泛型参数TEntity即可。所以,第二代实体类只有属性和索引器,基本不需要生成查询和操作的方法,因为它们都在泛型基类里面实现了。
大多数情况下,实体类指定的基类泛型参数就是它自己,因为它需要以它自己作为返回类型。但XCode开发模式是面向对象的,包括实体类,也希望能够继承,增加一些功能,该功能可以通过改变泛型参数来实现。
17、实体类的继承与重载
(NewLife.CommonEntity)
通过改变泛型参数的具体类型,实现实体类的继承和重载,是XCode进入第三代的标志。它标志着我们可以封装一些基础数据实体来供多个项目使用。
通用实体组件NewLife.CommonEntity就是该功能的代表,封装了地区、管理员、角色、菜单、授权等实体类。具体项目可以直接使用它们,也可以通过继承与重载来实现扩展。
以封装的地区表为例,它在静态构造函数中检测数据表行数,当然,在这之前XCode会自动检测并创建地区表。如果地区表中没有数据,则会调用一个方法进行数据初始化操作。地区表业务类代码通过硬编码方式,内置了全国几千个地区的区域编码和名称。一旦国家修改区域划分,只需要修改该类,所有使用该组件的项目都将使用上全新的区域数据。如果这些功能代码都复制到每一个使用的项目中去,将形成一个非常难以维护的状况。
作者:水木