NewLife.XCode是一个有10多年历史的开源数据中间件,支持nfx/netcore,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode。

整个系列教程会大量结合示例代码和运行日志来进行深入分析,蕴含多年开发经验于其中,代表作有百亿级大数据实时计算项目。

开源地址:https://github.com/NewLifeX/X (求star, 754+)

 

本文之前请先阅读

此处为文档,点击链接查看:https://newlifex.com/xcode/entity


内置查询


扩展查询

前文《实体类详解》中有讲到扩展查询,XCode生成实体类代码时,在模型类有一个region叫“扩展查询”,一般是FindByAbc/FindAllByAbc的形式。

扩展查询以数据表索引为依据来生成:

  • 唯一索引(含主键)生成FindByAbc方法(如FindByName),返回单个对象;
  • 非唯一索引生成FindAllByAbc方法(如FindAllByClassID),返回对象列表(非null);

如上图,可知Entity实体基类内部,查询方法分为单对象查询的Find和对象列表的查询FindAll。

实际上,Find最终调用FindAll方法查一行。

 

Find/FindAll有多个重载,最主要的地方都是构造where查询条件。

下划线_是每个实体类都有的内嵌类,它包含了每一个字段的Field引用,借助运算符重载,可以很方便的构造查询条件,例如上面的_.Name == name最终会生成 where Name='Stone'

因为是内嵌类,在实体类内部使用的时候非常方便。但要是想要实体类外部使用,就麻烦很多了,需要带上实体类类名。

 

原则:XCode是充血模型,不管多么简单的查询,建议都封装Find/FindAll/Search等方法供外部使用。

 

高级表达式查询

 仅靠一两个字段的简单查询,肯定无法满足各种业务要求,我们需要更强大的查询支持,特别是根据不同条件拼接不同语句。

上面是两个非常典型的业务查询。

这里请出了条件表达式WhereExpression,实际上它只有两个功能,&表示And,|表示Or,根据表达式级别支持括号运算。

exp&=xxx 是最常用的写法,右边一般是各种Field表达式。

上面第一个例子,生成的查询语句可能是 select * from Student where classid=?classid and name like '%?key%'

为什么说“可能”?因为classid为0,或者key为空时,并不会参与拼接查询语句。

 

第二个例子稍微复杂一些,首先对key进行精确查询,找到了就返回,若是没找到,则开启模糊查询。 

这里遇到了等于、包含、区间等判断操作,后面会详解所有支持的操作。

 

如非必要,建议保留select * 的查询方式,而不是指定列。 

码农法则:数据库压力小于100qps时不要考虑指明select列来优化,大多数系统活不到需要优化的明天!

 

高级分页 

两个例子都出现了一个PageParameter参数page,这是分页参数,包含分页查询以及排序所需要的数据。

  • PageIndex和PageSize指定页序号和每页大小,这是内部建立分页查询的核心依据;
  • Sort 指定排序字段,Desc 指定是否降序(默认升序);
  • RetrieveTotalCount 指定是否获取总记录数,若为true,则在查询记录集之前,先查询满足条件的总行数TotalCount,用于分页PageCount。此时等于执行两次数据库查询;
  • RetrieveState 指定是否获取统计 State,若为true,则在查询记录集之后,执行聚合查询,对数字型字段使用Sum聚合。此时最多可能执行3次数据库查询;

在执行FindAll查询时,若有传入 PageParameter 且 RetrieveTotalCount 为true,则先查询满足条件的记录数,大于0时才查某一页数据。

如果 Meta.Count 评估认为本表总行数超过100万,且FindAll查询没带有条件,则page.TotalCount直接取Meta.Count(少量偏差),以避免极大的FindCount耗时。

100万行以上数据表,如若不带条件或者条件没有命中索引,select count 将会极其的慢,在1000万以上甚至查不出来,这是XCode能对100亿表进行分页查询的关键所在

Meta.Count 的初始值来自于数据库元数据索引表,里面有该表主键的总行数,取得该值后如果小于100万再异步select count一次。

10多年前博客园ORM大战的时候,我们常说,等你支持千万级分页的时候再来比,就是钻了select count很慢的这个空子,很多人count出来总数再分页 ^_^

上图4亿数据,查询第10000页,在SQLite单表上,阿里云1C1G服务器。

 

FindCount 分页

在早期版本,不支持RetrieveTotalCount ,只能通过 FindCount 取得满足该条件的总记录数,然后进行分页,至今仍然支持传统方法。

因此可以看到,FindAll 和 FindCount 都是成对出现,参数一摸一样。

并且 FindCount 方法也会带有分页参数,虽然用不到,但.NET2.0时代的 ObjectDataSource 要求两者的参数名称和顺序必须一致。

所有 FindCount 方法,将会得到 select count 查询语句,因此千万级大表需要慎用。

 

PageSplit 分页

内置支持的各种数据库,都有实现普通查询语句转为分页语句的 PageSplit(sql, start, maxNums) 方法。

MySql/SQLite/PostgreSQL 能够很好支持,只需要在 sql 后加上 limit start, maxNums 即可;

Oracle/SqlServer/Access/SqlCe 则要麻烦一次,其中SqlServer最复杂,不同版本的分页方法还不同,早期版本还要求有主键字段;

因此,sql 必须是简单的单表查询语句,PageSplit 才能把任意查询拆开并转换为分页查询。

 

大表分页优化

大表分页查询,开头会很快,越是往后越慢!

XCode采用倒置优化法,对于超过100万行(借助Meta.Count评估)的表,如果查询页超过中线,则从另一个方向查询,然后再把结果倒置回来。

 

 

XCode要求数据查询必须考虑分页,没有分页的系统一般死在100万行以内。

 

Field扩展

内嵌类_引用的字段是Field,它继承自FieldItem。

Field/FieldItem全部功能:

  • Equal 等于,操作符==
  • NotEqual 不等于,操作符!=
  • 大于操作符>,大于等于>=
  • 小于操作符<,小于等于<=
  • StartsWith 字符串开始,like '{0}%'。(支持索引)
  • EndsWith 字符串结束,like '%{0}'
  • Contains 字符串包含,like '%{0}%'
  • In 集合包含,支持列表集合、字符串子查询和SelectBuilder子查询,集合只有一个元素时转为相等操作
  • NotIn 集合不包含,支持列表集合、字符串子查询和SelectBuilder子查询,集合只有一个元素时转为不相等操作
  • IsNull 是否空
  • NotIsNull 不是空
  • IsNullOrEmpty 字符串空或零长度
  • NotIsNullOrEmpty 字符串非空非零长度
  • IsTrue 是否True或者False/Null,参数决定两组之一
  • IsFalse 是否False或者True/Null,参数决定两组之一
  • Between 时间区间,大于等于开始,小于结束,如果开始结束都只有日期而没有时分秒,则结束加一天,如(2019-04-17,  2019-04-17)查 time>='2019-04-17' and time<2019-04-18'

排序字句/分组聚合

  • Asc,升序
  • Desc,降序。order by name desc
  • GroupBy,分组。group by name
  • As,聚合别名
  • Count,计数
  • Sum,求和
  • Min,最小
  • Max,最大
  • Avg,平均

 

查询的本质

查询的本质是五参数版FindAll(where, order, selects, start, maxnums),其它查询方式都由它转化而来!

Entity实体基类封装了各种常用的查询方法:

对于单表查询的XCode来说,五参数版FindAll很容易得到 select [selects] from [table] where [where] order by [order] limit [start], [maxnums] 语句,根据这个理念,FindAll可以支持任意复杂查询!

最终查询语句,由SelectBuilder类承载。 

 

多表子查询

XCode不支持多表Join关联,这在前面《扩展属性》中提到过。

扩展属性固然可以解决关联多表字段的问题,并且借助缓存性能还不错,但是需要同时在两张表上设置条件的时候,就行不通了。

于是,需要用到高级查询,可以用子查询 来替代,正是前面说到的FieldItem.In扩展。

 

要查询名为“992班”的所有学生,一般这样写:


select * from student s inner join class c on s.classid=c.id where c.name='992班'

XCode从2008年起,就放弃支持多表关联,自然也就不支持这样的写法。

在一般系统里面,班级表数据不多,可以借助实体缓存或者对象缓存:


// Class.FindByName 内部用缓存
var cls = Class.FindByName("992班");
var list = Student.FindAll(Student._.ClassID==cls.ID);
// select * from student where classid=1234

但如果主表从表都是百万级大表,或者从表查询条件比较复杂,缓存就有点难以为继了。

于是有了子查询:

调用方法:

var list = Student.Search(SexKinds., "992班", p);

得到结果:

select * from student where sex=2 and classid in(select id from class where name='992班')


至此,绝大部分多表关联复杂查询语句,可以转化为子查询 !


跨库大表关联查询

实际项目开发中,还可能遇到更为复杂的场景。


场景:汽车表CarInfo有UserId字段,用户表User有DepartmentId字段,要求查询当前用户所在部门所有人的汽车信息,但是汽车表和用户表位于不同数据库(甚至可能异构MySql+Oracle)。


初级解法

var list = CarInfo.FindAll(_.UserId.In(User.FindSQLWithKey(User._.DepartmentId==@user.DepartmentId)));

这是子查询写法,由于两个表在不同数据库,所以它无法正常工作。


高级解法

var users = User.FindAll(_.DepartmentId==@user.DepartmentId); // 先把本部门的人都查出来
var list = CarInfo.FindAll(_.UserId.In(users.Select(e=>e.Id))); // 再查汽车信息

这是跨库表的标准做法,如果用户表信息不到一万,第一行甚至可能在内存中就已经完成;

缺点也很明显,如果用户表过大(我们这里就过百万),本部门的人员就可能有数千,二次查询时In会直接失败(一般不支持超过1000项)


神级解法

(针对亿级数据表CarInfo关联百万级基础表User)

// CarInfo ( * + UserId + DeparmentId)
// 业务层只需要给UserId赋值,在CarInfo添加或更新时执行Valid,自动补上DeparmentId,等于不改变原有逻辑
// 当然,如果业务层方便直接给DeparmentId赋值,那是再好不过了,双保险吧
public override void Valid(Boolean isNew)
{
    // 如果业务层没有给DeparmentId赋值,这里补上
    if (UserId > 0 && !Dirty[__.DeparmentId])
    {
        var user = User.FindById(UserId); // User.FindById 走对象缓存
        if (user != null) DeparmentId = user.DeparmentId;
    }
}

// 最终直接查询
var list = CarInfo.FindAll(_.DeparmentId == @user.DeparmentId);

该方法通用性强,实力强大,但也不推荐随意使用,毕竟工作量增加了很多很多。

解法的选择,取决于数据量,在数据量固定的情况下,选择简单的解法更好。



如果对你有帮助,给个赞呗!

image.pngimage.png


分组与统计请阅读

此处为文档,点击链接查看:https://newlifex.com/xcode/stat