关系数据库快速入门

引言

本教程的目的

本教程介绍如何使用 Entity 框架来构造和执行关系数据库的查询.
它旨在作为基于范例的快速入门指南,而不是完全的参考。 它涵盖了最常见的用例,并通过具体示例进行说明.
通过学习本教程,用户应该会对 Entity 框架查询的构造和执行有核心切实的了解。 为了使本教程的篇幅和介绍水平保持合理水平,有意省略了一些更高级的内容、细节和特殊情况。 但是,大多数核心查询构建机制都通过范例予以介绍和说明,作为入门教程,这些范例应该足以.

关系数据库连接的作用

关系数据库是信息存储、处理和查找的重要且广泛使用的方法。 在过去的几十年中,相关技术已经变得非常成熟,并且现代关系数据库的后端具有出色的功能,可以存储和处理从小型到大型数据量的结构化数据。 因此,能够将关系数据库无缝集成到自己的工作流中,以便在从数据科学到Web应用程序的许多领域中进行工作,这一点非常重要.
其中一个重要方面是能够在内核之外"数据库端处理非常大的数据集。 大多数常见的关系数据库后端都已进行了充分优化,并应用了尖端技术,从而能够非常有效地处理这样的大型数据集.
对于 Wolfram 语言而言,将大型数据集上的计算卸载到数据库引擎的能力尤其重要,因为许多此类计算并不需要 Wolfram 语言的全部计算能力,而所得数据集的大小可以大大减小,以便在 Wolfram 语言中轻松地进行进一步处理.

Wolfram 语言中的关系数据库连接

一般注意事项

出于种种原因,将关系数据库直接集成到工作中可能会比较复杂。首先,各个领域的专家并不一定具备直接使用关系数据库所需的SQL知识. 其次,对于通用的不同数据库后端,SQL语言与支持的功能之间存在(通常是细微的)差异. 第三,必须手动建立数据管道,将数据库连入自己的编程环境. 由于这些原因,将关系数据库直接紧密集成到该语言中非常有益. 用户可以专注于解决问题,而不是提供技术基础结构.
Wolfram 语言的高级符号性质对将关系数据库集成到该语言的框架有严格的要求. 它必须是高级的、符号式的和惯用的,同时又要显示出足够的结构,以免相对于原始SQL产生过多的查询和其他功能的限制. 特别地,惯用的 Wolfram 语言编程倾向于函数式编程风格,功能组合和不变性,因此,连接框架需要包含和支持这些功能.

Entity 框架的作用

事实证明,Entity 框架确实满足了上列要求. 虽然在 Wolfram 语言中没有 OOP 意义上的对象,但是在关系数据库的语境中,Wolfram 语言的 Entity 框架在许多方面类似于其他语言的对象关系映射器(ORM). 有点简化地讲,实体对应于数据库表的行,实体类对应于数据库表(现有表或虚拟表),而实体属性对应于数据库列.
在需要查询数据库的大量情况下,可以实现不离开 Wolfram 语言,通过由 Entity 框架功能查询语言构造的高级 Entity 框架查询语句与数据库进行通信. 然后,框架负责将查询转换为适当的SQL方言,在数据库侧执行查询,并将结果以适当的格式返回用户.

Entity 框架和 DatabaseLink

长期以来,使用 Wolfram 语言与关系数据库进行交互的主要工具是 DatabaseLink. 它建立在 J/Link 和JDBC 之上,并提供了一个功能齐全的工具箱来与关系数据库进行交互.
重要的一点是了解 DatabaseLink 工具箱与本教程中讨论的技术之间的差异. 最重要的区别在于这些工具提供的抽象级别以及与数据库交互所支持的功能.
DatabaseLink 提供的工具箱是比较低级的:虽然它具有一些功能,可以在简单的情况下以符号方式构造数据库查询,但是当与 DatabaseLink 一起使用时,大多数实际查询都必须用 SQL 字符串编写. 这对用户有许多影响,比如用户要熟悉 SQL(以及与所使用的特定数据库后端相对应的特定 SQL 方言),以及无法使用Wolfram 语言的符号式功能和高级抽象功能等. 所有这些都会导致数据库与 Wolfram 语言的不完整、低级集成.
基于 Entity 框架的技术是从零开始设计的,旨在实现关系数据库与 Wolfram 语言的高级无缝集成,并包含很多功能(例如符号查询语言,关系,自动 SQL 生成和后端专业化, 类型检查等),从而使 Wolfram 语言中涉及关系数据库的功能更强大更高级.
另一方面,DatabaseLink 对涉及数据库的常见工作流所需的许多核心功能提供支持,例如写操作(SQL INSERT / UPDATE / DELETE)、事务支持、超时控制,连接池等. 它还提供了出于效率目的对数据库结果集的低端访问接口. 所有这些都是 Entity 框架技术目前所缺乏的,因此可以说 DatabaseLink 目前具有更齐全的功能.
当前,除了将两种技术都集成到 Wolfram 语言中之外,这两种技术之间还没有深层的互操作性. 在将来的 Wolfram 语言版本中,互操作性的水平可能会更高.

教程结构

该材料的简要分类如下.
第一节解释标准工作流程,为使用关系数据库做准备:建立连接,创建和注册数据库支持的 EntityStore 对象.
第二节回顾并说明关系数据库中 Entity 框架的主要功能.
第三节介绍实体框架提供的核心查询构建块,用于构建更复杂的查询.
第四节简要介绍一些其他有用的构造和工具,它们可能被认为更高级,但在实践中却非常有用.
第五节说明 Entity 框架支持的各种查询构建原语通常可以生成哪种SQL.
第六节讲解如何以编程方式生成查询,并举例说明这样做的好处.
第七节对一些查询构造的实用技术进行说明.
第八节简要介绍错误处理,以及在构造和运行查询时可能遇到的典型错误.
最后一节提供一些更复杂的查询示例,讨论如何使用不同的查询构建块来构造更复杂的查询.

样本数据库

本教程中的示例基于公有领域样本数据库的经典模型(尤其是其SQLite版本),该模型是经典汽车数据库比例模型的零售商.
该数据库包含八个表:
它具有以下实体关系图(请注意,图中的数据类型对应于数据库的MySQL版本):
如图所示,表之间存在以下关系:
连接到关系数据库

连接到数据库

要开始使用关系数据库,必须首先完成以下步骤:
这是本教程所用的示例数据库的完成方式:
这将创建与数据库的连接:
使用该连接构造 RelationalDatabase 对象:
This creates a database-backed EntityStore:
注册 EntityStore
现在即可使用了.
快速检查:显示有关 "offices" 表的信息:
可以将前面的步骤进一步简化.
首先,取消注册 EntityStore
以下是精简后的步骤:
对于不同的数据库后端,用于构造数据库连接的特定语法可能有所不同.

使用 RelationalDatabase 对象

如上一节所述,创建数据库支持的 EntityStore 所需的步骤之一是创建 RelationalDatabase 对象. 但是,此对象本身也很有用. 它包含有关数据库模式的信息(表,列,约束等),并且可以用于直观检查和以编程方式提取一部分用户感兴趣的信息.
这将创建一个 RelationalDatabase 对象:
RelationalDatabase 对象的格式使您可以使用分层群组打开器轻松直观地检查数据库结构,可以展开给定的表或列,以检查更多细节:
也可以通过编程方式提取此信息.
下面列出了给定 RelationalDatabase 对象的所有表名:
以下测试是否存在给定名称的表:
以下列出给定表的所有列名:
以下测试是否存在具有给定名称的列:
下面列出了 RelationalDatabase 对象可以识别的给定表列的所有属性:
可以按如下方式获得其值:
有关属性和方法的更多详细信息,请参考 RelationalDatabase 对象的参考页面.
Entity 框架和关系数据库

引言

本节介绍 Entity 框架在关系数据库方面所支持的重要核心操作. 查询构造并不是本节的主要关注点,对此后面将有专门一节进行介绍. 相反,它侧重基础,介绍一些其他重要方面,其中许多方面是通过 Entity 框架有效处理关系数据库的前提条件.
要在关系数据库方面有效地使用 Entity 框架,对 Entity 框架的概念和构造如何对应于关系数据库的核心概念和构造有一个基本的了解是很重要的. 首先讨论该主题.
随后将讨论查询执行和所谓的解析器(通过启动查询编译和执行过程并返回结果,将符号表示的查询实际转换为特定结果的函数).
接下来,将介绍计算属性和 EntityFunction. 这些构造块非常重要,使用户可以动态定义和计算新属性,而这些新属性可能需要在数据库端执行复杂的计算.
在许多情况下,能够使用单个实体很重要. 这对于各种目的都是有用的,无论是为了对结果进行更好的可视化和了解,还是查询的构造(对于调试和原型设计,能够在单个实体上执行部分查询非常重要). 接下来简要介绍该主题.
对于关系数据库,数据库表主键是一个中心概念. 与此对应的 Entity 框架是单个实体的 CanonicalName 属性. 与规范名称一起定义单个实体的是该实体的类型. 这些主题很重要,值得用单独一节进行介绍.
尽管大多数涉及关系数据库的现代工作流程所用的数据库表都确实具有主键,但一个表不存在主键,或者存在主键但在数据库模式级别上没有强制执行的情况也并非鲜见. 在 Entity 框架方面,这些甚至更为重要,因为对于此类表/实体类型,仅 Entity 框架功能的一部分起作用. 接下来讨论的是这个问题.
尽管本教程并不关注与关系数据库交互中隐式涉及的类型和类型系统,但对于有效的工作来说,要理解非常重要的一种类型区别. 即,实体类型可以包含实体值和实体类值的属性. 在本教程中,此类属性称为关系,并由框架添加到每种实体类型的核心属性集合(直接对应于数据库表列的属性)中.
本节的最后一个主题涉及缺失值,和在关系数据库的情况下在结果中遇到缺失值的方式.

Entity 框架和 SQL 之间的近似映射

由于 Entity 框架在 Wolfram 语言中使用,尤其用于表示关系数据库,因此在 Entity 框架与关系数据库/SQL之间显然必须存在核心概念和构造的对应关系.
对于 Entity 框架的许多功能,这种映射是相当直接的. 但在某些方面,Entity 框架表示更丰富的数据模型. 例如,对于在内存中使用 Entity 框架,实体属性可以用任意 Wolfram 语言表达式作为它们的值,如果直接使用,甚至不符合关系数据库的第一个范式(例如当实体属性为 List 值时). 作为另一个示例, Entity 框架能够用无限数量的实体表示类型,这是关系数据库无法轻松实现的. 而且,在更容易进行可能计算的集合方面,关系数据库比 Wolfram 语言受到更多限制. 后者自然具有更丰富的开箱即用的计算功能.
关系模型的限制也具有其优点,例如为数据一致性、事务和 ACID 兼容数据库的其他相关功能提供有力保障. 这些限制还意味着类似的限制也作用于数据库支持的实体存储的 Entity 框架之上.
也就是说,核心关系和 Entity 框架构造之间的映射相对简单. 下表是一个汇总.
Database (schema)
实体类型的集合(数据库表)
Database table
Entity type
具有相似属性集(表行)的实体集合(的句柄)
Database table row
Entity (single entity)
一组属性/值(的句柄),代表具有唯一标识的单个事物(表行)
Database table field (column name)
特定属性,通常具有特定类型
Primary key
一个属性或一组属性,对于给定的实体类型(数据库表),保证是唯一的(当一起使用一组属性时)
Foreign key
Entity-valued property
实体属性,其值为单个实体(可以具有相同或不同的实体类型). 对于数据库,指向表中唯一行的字段(可以是相同或不同的表).
(Derived) table
相同类型(由查询注册或定义)的实体的(句柄)集合. 在数据库方面,派生表是在 FROM 子句中使用的虚拟表/子查询.
如前所述,此映射具有一定的不对称性. 它使将现有关系数据库表示为实体存储变得很简单. 但是,并非所有现有的内存中实体存储都可以轻松地映射到关系数据库而无需创建额外的抽象层. 以下是一些存在的限制:
尽管这些考虑因素在需要通过关系数据库支持现有的内存中实体存储更为重要,而 Entity 框架当前不支持该功能,但记住这些因素仍然有用,以便更好地了解整体情况.
也有一些限制在相反方向起作用. 例如,从技术上说,没有主键的数据库表可以与数据库端的表一起使用. 但是,在 Entity 框架方面,每个实体都必须具有在给定实体类中唯一的 CanonicalName. 这意味着无法为此类数据库表定义单个实体,因此,Entity 框架的所有与单实体相关的功能均不适用于此类数据库表/类型. 特别是,在某些情况下,带有某些修饰符的函数如 EntityListEntityValue(例如 "EntityPropertyAssociation")将无法使用. 该节将进一步讨论此问题.

使用 EntityValue 和 EntityList 执行查询

Entity 框架中只有两个解析器(可用于执行查询的命令): EntityValueEntityList.
主要和最常用的一种是 EntityValue. 它通常用于执行给定的查询并以各种形式获取结果. 在关系数据库上下文中,查询通常表示(虚拟)数据库表,这些表在 Entity 框架中对应于(虚拟)实体类型. 在这种情况下,EntityValue 的作用是将给定的查询编译为合适的SQL方言,在数据库上执行该查询,并以各种形式(列表或关联)为一组特定的实体属性(数据库项中的表列)提取值.
以下是一些使用 EntityValue 的简单示例.
下面为注册类型"offices"的多个属性提取值:
可以以其他形式获得结果,例如,以关联列表的形式保留属性名称:
对于提取的属性,可以使用完整的 EntityProperty 形式代替短字符串名称,在这种情况下,数据关联中的结果键也将是 EntityProperty[]表达式:
也可以以 Dataset 的形式获取结果:
可以在 EntityValue 的文档中找到 EntityValue 第三个参数的所有可能修饰符.
在某些情况下,可能想要获取包含在给定实体类型/类中的实体的列表。 这可以通过 EntityList 完成.
以下代码返回"offices"实体类型中包含的实体的列表:
请注意,除了某些特殊情况将在后续章节中讨论,对于重复调用 EntityListEntityValue的情况,不能保证结果中实体的顺序是相同的.

计算属性和 EntityFunction

除了提取现有属性外,还可以提取计算的属性,即动态创建的属性,这可能需要在数据库端进行复杂的计算. 此类属性必须使用 EntityFunction 表示. 在这种情况下,EntityFunction的语义与 Function 相似,在后续章节中将涉及一些重要的区别. 可以将 EntityFunction 视为类似于 EntityProperty 的构造,该构造(在本节的上下文中)接受单个实体,并返回对该实体执行的某些计算结果. 重要的是,目前 EntityFunction 只能返回标量,而不能返回例如值列表、实体或实体类等.
以下是使用 EntityFunction 检索需要在数据库端进行计算的属性的示例.
以下查询为"offices"类型中的每个办事处提取属性 "officeCode" 和计算属性,代表完整的字符串办事处地址:
EntityFunction 具有 HoldAll 属性,如 Function 一样,可防止过早运算其变量和主体. EntityFunction的主体是一个表达式,可以使用EntityFunction 可以理解并编译为SQL的有限原语集. 此类原语的完整列表可以在 EntityFunction 文档中找到,而在后续各节中可以找到更多关于 EntityFunction 的实际使用示例.

使用单一实体

单一实体是 Entity 框架的重要概念和组成部分. 能够使用单个实体增加了工作流程的交互性,也常常可以使人们更好地理解数据,即使在最终结果应该是适应于多个实体(实体类)的查询的情况下.
重要的是要理解单个实体本质上是实际数据的句柄(或引用). 除了类型和规范名称外,它们不包含数据. 因此,它们很懒:每当需要从实体中提取数据时,就必须对其进行新查询. 实体的这种惰性是 Entity 框架的一项非常有用的功能,因为它允许人们以一种更加抽象的方式使用实体. 但是,也有几个注意事项. 例如,如果某个特定实体的某些属性在同一查询的两次连续执行之间发生更改,则返回的结果通常也将有所不同,以反映这些更改. 也可能会发生某个时刻在给定实体类中存在的实体已被删除并且不再存在的情况. 当然,这些复杂情况只会发生在随时间变化的数据中.
以下是几个如何查询单个实体的范例.
考虑一个特定的单一实体:
可以使用 EntityValue 提取属性值:
还可以使用特殊的查找语法:
对于实体类,可以使用修饰符来控制结果的形状:
重要的是,要认识到可以使用 EntityFunction 提取单个实体的计算属性. 这对于快速查找和对更复杂的查询进行原型设计都非常有用.
例如,这将在数据库端,从给定办事处的 "city""state" 属性中计算出格式化的字符串:
同样也可以这样实现:
后一个示例还说明了 EntityFunction 的类似属性的性质.

实体类型,单个实体的唯一性,和 CanonicalName

在 Entity 框架中,每个实体在其实体类型/实体类中必须是唯一的. 换句话说,每个实体必须具有唯一的标识,并且不允许任何实体类/实体类型包含重复的实体.

实体类型和规范名称

语法上,实体由包含两部分的表达式表示:实体类型和实体唯一标识符,称为规范名称.
查看实体的 InputForm
也可以使用 CanonicalName 提取实体的规范名称.
下面提取单个实体的规范名称:
函数 CanonicalName 也可用于实体列表:
规范名称通常是数字,字符串或这些的列表.
类型为 "orderdetails" 的实体具有成对的整数订货号和字符串产品代码作为其规范名称:
实体的实体类型始终是 Entity[]的第一个参数,但并不总是字符串. 特别是,在类型不是注册类型而是 Entity 框架查询隐式定义的运行时类型的情况下,查询本身就是该类型.
以下查询定义了一个复杂类型:
看一下该类型的单个实体:
该实体的类型由查询定义:
对于与数据库中数据库表相对应的实体类型,实体的规范名称恰好是相应数据库表的主键.

规范名称何时是一个列表?

在关系数据库的上下文中,实体的规范名称在以下情况(或其组合)之一时可以是列表:
前面的范例属于最后一个类别,其中分组属性为 "city""country",而更前面的范例具有 "orderdetails" 类型的实体属于第一类,其 "orderdetails" 表的主键由属性 "orderNumber""productCode"复合而成.
这是第二类的范例.
下面定义了一个实体类,该实体类组合了类型"employees""offices". 此类的实体具有规范名称,该规范名称是成对的员工编号和办事处代码的列表:

无主键的单个实体和数据库表

在数据库模式中,某些数据库表没有主键约束的情况可能并不是典型的,但在实践中很重要. 请注意,并非所有此类情况都是表中缺少具有唯一值的列. 但是,目前尚无办法为 Entity 框架指示这样一个列用作此类表的主键:有关主键的所有信息当前都是在数据库检查时(构造RelationalDatabase 对象时)获得的,并且 完全不受现有数据库的限制.
对于基于实体框架的工作流,这意味着此类表无法定义单个实体,因为没有明确的方法将规范名称附加到此类表中的数据库行. 这并不意味着我们对这些表完全无能为力,但 Entity 框架功能的某些部分将无法适应于这些表.
这里使用 Titanic 数据库对这些表在 Entity 框架功能方面的局限性加以说明. 该数据库是 Wolfram Cloud 中托管的一个 SQLite 数据库,并基于以下 Titanic 数据集:
从该数据集的结构可以看出,没有任何字段或字段组合可以自然地用作主键. 甚至不能保证数据集中没有重复的记录,因为数据中没有乘客的姓名,而只是乘客的舱等、年龄、性别以及他们是否幸存. 尽管如此,该数据集还是一个有用的信息来源.
样本工作流对某些 Entity 框架操作进行说明,这些操作对于此类数据集是不可能的.
第一步是创建 RelationalDatabase 对象:
此步骤应该没有任何问题; 数据库及其具有的单个表均已正确识别.
下一步是基于该数据库创建和注册 EntityStore 对象:
可以看到,EntityStore 发出警告,单个实体无法用于此类型/数据库表. 特别是,EntityList 和某些形式的 EntityValue 将不起作用.
EntityList 不适用于无 CanonicalName 的类型(无主键的表):
EntityValue 对于某些修饰符也不起作用:
但是,不涉及单个实体的其他形式可以工作.
只要结果数据的形式不涉及单个实体,仍然可以提取特定属性的值:
许多有用的查询也是如此.
以下查询选择年龄<= 15的头等舱中所有幸存的乘客:
以下查询引入了一个粗粒度乘客年龄组,其中第一组的年龄介于0至20岁之间,第二组的年龄介于20至40岁之间,依此类推. 然后计算出每个舱等、年龄段和性别的幸存乘客比例,然后按降序对结果进行排序:
总结本节:

实体值和实体类值的属性

属性可以是实体值或实体类值. 此类属性不对应于相应数据库表的任何数据库表列,而是由框架添加并表示相关表.
以下是 "employees" 实体类型的此类属性的一些范例.
以下获取 "employees" 属性的值,该属性是(隐式)实体类:
可以使用 EntityList 将此实体类扩展为实体列表:
考虑其中一名员工作为有关实体值属性的示例.
这将选择给定办事处的第一位员工,并提取该员工的实体属性和值:
除了 "officeCode" 属性(在数据库中是 "offices" 表的外键)之外,还有一个生成的属性 "offices",其值表示该员工所在办事处的实体:
请注意,此类具有实体和实体类值的属性不是直接源于数据库列,而是由框架基于数据库架构(关系/外键信息)生成的.
此时要注意的一个重要观察是,实体类值属性不会自动解析为实体列表,因此,如果要获取结果的实体列表,则必须将 EntityList 显式地应用于它们的值.
生成的属性将在关系一节中进一步讨论.

结果中的缺失值和无效属性名称

对于某些实体,某些属性在数据库表中可能没有值(值可能为 NULL). 这在 Wolfram 语言一端对应于 Missing[]值.
一个例子将说明这一点.
再次考虑其中一个办事处:
该办事处位于法国巴黎,因此不存在 "state" 字段. 像这样的缺失值对应于数据库端的 NULL 值,并在结果中由Missing["NotAvailable",...] 表示:
导致出现 Missing[]值的另一种输入类型是无效属性名称的请求值. 但是,在这种情况下,缺少该值的原因有所不同: Missing["UnknownProperty",...].
以下是请求不存在的"foo"的属性值的范例:
核心查询构建块

引言

本节介绍 Entity 框架提供的用于构建更复杂查询的核心原语.
首先通过许多示例来解释和说明类似于属性的查询. 这些示例涉及如何使用计算的属性显着扩展可用的实体属性集,以及如何使用EntityFunction 表示那些计算的属性.
本节的其余部分专注于采用实体类(和其他参数)并返回新的(转换)实体类的各种原语. Entity 框架为下列典型的数据处理操作提供了此类原语:
这些实体类转换原语可以被组合并嵌套在另一个原语中,从而构造更复杂的查询.

EntityFunction 和类属性查询

类属性查询是用于构建 Entity 框架查询部分的 Wolfram 语言表达式,在查询编译和执行过程中被编译成 SQL 表达式. 用于构造类属性查询的主要结构是 EntityFunction.
类属性查询用途非常广泛,例如定义计算的属性、筛选数据的谓词、排序标准、聚合等等. 本教程将考虑其中的一些典型示例.
可以对数字量使用标准算术运算: TimesPlusSubtractDivideMod 等.
下面计算出每个订购商品的总付款额,考虑到订购商品的数量:
也支持许多字符串操作函数.
下面的示例为 "offices" 类型的每个实体提取 "state" 属性的值,并计算布尔表达式,该表达式检查此属性值是否以字母 "C" 开头:
要测试缺失值,可以使用 MissingQ 谓词.
以下示例显示如何使用 MissingQ 谓词检查缺失值(在示例情况下,是对于类型为 "offices" 的属性 "state"):
还支持标准比较运算符. 请注意,EqualSameQ 以及 UnequalUnsameQ 可以互换使用.
下面的示例显示在字符串比较的上下文中使用比较运算符 EqualSameQUnequalUnsameQ
可以构造相当复杂的数字和其他类型的表达式,这些表达式将转换为 SQL 并在数据库端执行.
以下查询计算厂商建议零售价(MSRP)和购买价格之间的差额与MSRP之间的比率,即如果商店以MSRP出售商品,该商店从出售该商品中获得的收入的百分比:
这是一个涉及 PowerSqrt 的更复杂的指标,仅作说明:
布尔表达式特别重要,因为布尔值 EntityFunction 表达式经常用作过滤谓词、CombinedEntityClass 的条件等.
以下查询计算一个布尔属性,对于 "orderdetails" 类型的每个实体,如果订购商品的数量 >30且单件价格 >= $200,则结果为 True,其中 Wolfram 语言端后处理仅选择结果为 True 的商品:
以下查询计算一个属性,仅当订单号能被100整除时为 True
以下查询计算一个布尔属性,对于所在城市包含字母 "a" 或其所在州存在(不存在 Missing[])并且其城市包含字母 "o" 的所有办事处生成 True
以下查询计算一个布尔值属性,该属性对于从下订单之日起八天内需要发货的所有订单产生 True
在以下示例中,布尔值查询表达式用作 FilteredEntityClass 内部的是筛选条件,以查找订购数量大于30的商品的所有订单:

实体类转换器的注意事项

以下各节介绍的操作,即 FilteredEntityClassSortedEntityClassSampledEntityClassExtendedEntityClassAggregatedEntityClassCombinedEntityClass,都可以称为实体类转换器. 它们都接受一个实体类(对于 CombinedEntityClass,则为两个),并返回一个新的实体类.
重要的一点是,这些构造是完全符号性和惰性的. 当其中一个应用于实体类参数时,不会执行任何实际工作. 得到的查询仍为符号式,并且需要调用其中一个解析器函数(EntityValueEntityList)以执行实际查询.
下面是一个查询示例,包含多个相互嵌套的转换器.
根据付款历史选择五个付款最多的客户,并按总金额递减顺序列出它们:
当被运算时,符号 Entity 框架查询将对其自身进行运算,保持为惰性符号表达式. 但是,仍然可以用它做一些有用的事情,例如,找到此查询表示的实体类的所有实体属性(不需要调用数据库).
以下返回由查询定义的实体类的实体属性:
使用 EntityValue 执行实际查询.
通过使用 EntityValue 执行该查询:
Entity 框架查询的符号式和惰性为程序式查询构建开辟了道路,因为查询本身是惰性的 Wolfram 语言表达式,可以通过较小的结构块手动或以编程方式构造.

使用 FilteredEntityClass 筛选

最常用的操作之一是根据某些条件过滤数据库数据. 在 Entity 框架中,FilteredEntityClass 用于此目的,过滤条件使用 EntityFunction 表示,并作为第二个参数传递给 FilteredEntityClass.
以下示例说明了 FilteredEntityClass 的典型用法.
以下列出了所有职务不是 "Sales Rep" 的员工的名字、姓氏和职务:
以下查询选择电子邮件名较短(字符长度<= 5)的所有员工:
以下查询查找名字以"M""P""D"开头的所有员工:
使用以下查询也可能获得相同的结果:

使用 SortedEntityClass 排序

根据某些条件对实体(数据库表中的行)进行排序也是一种常用的操作. 在数据库端,这通过使用 SQL 的 ORDER BY 语句实现. 在 Entity 框架端,可以使用 SortedEntityClass 来实现.
在最简单的情况下,需要根据单个现有实体属性(数据库列)的值以升序排序. 在这种情况下,字符串字段名称将作为第二个参数传递给 SortedEntityClass.
下面的示例对此进行了说明.
下面根据办事处代码对员工进行排序:
对于要求结果按降序排列的情况,则可以使用 fieldName -> "Descending" 语法,如下所示.
以下查询根据办事处代码对员工进行降序排序:
也可以通过类属性的查询进行排序.
以下查询列出了所有员工的员工编号、名字和姓氏,按名字的长度升序排列:
可以按多个属性排序. 在这种情况下,第二个属性值在组中各项的第一个属性值均相等的情况下起作用,相同的逻辑推广到第三个排序属性,依此类推. 可以为每个排序属性分别附加 "Ascending""Descending" 限定词,以控制实体的(子)组内的排序顺序.
以下查询先按产品系列、再按库存数量对产品进行降序排序:
可以通过利用 SortedEntityClass 的第三个参数来限制返回的结果数.
这是先前的一个查询,其中列出了员工的有关信息,这些信息按员工名字的长度排序,而现在结果仅给出前七个记录:

使用 SampledEntityClass 进行子设置

在某些情况下,从实体类中仅选择特定数量的实体很有用(或者,从数据库的角度来讲,仅从表或虚拟表中选择特定的行子集). 在 Entity 框架中,执行此操作的方法是使用 SampledEntityClass.
以下示例说明了其典型用法.
以下查询从"payments"类型(表)中选择10笔付款:
以下操作相同,但是跳过前10个条目:
请注意,只有将 SampledEntityClass 应用于 SortedEntityClass 时,结果的顺序才能被保证. 但是,即使在其他情况下,例如当一个人想要浏览一个大型数据集或仅使用一个小样本来构建查询原型时,仍然会非常有用.
以下查询返回前五种最昂贵产品的名称和价格:
在这种情况下,可以通过在设置子集之前对数据集进行排序来保证顺序。 使用带有三个参数的 SortedEntityClass 可以更经济地获得相同的结果:

通过 ExtendedEntityClass 引入新属性

将新的、计算得到的属性添加到给定类型/实体类的一组可用实体属性中是可用的。 结果是一个新的实体类。 新添加的属性可以在原始属性可以使用的任何地方使用,如在 EntityValue 中,甚至在查询的外层使用,等等。创建这种新扩展实体类的结构是 ExtendedEntityClass.
以下用一个简单的实例来说明,将一个新的属性添加到实体类.
以下查询为所有员工添加新属性 "fullName":
可以添加多个属性,在这种情况下,应使用列表.
下列使用关系(在此处有更详细的描述,属性 "employees-reportsTo")为员工添加其经理的名字和姓氏(如果有的话):
新属性可以使用各种结构,包括条件逻辑等. 此功能允许进行非平凡计算.
以下查询为客户添加新的调整后的信用额度,将当前信用额度 >=$100,000 的客户的额度增加 $15000:

使用 AggregatedEntityClass 进行聚合

聚合是一个非常常用的操作. 它用于计算在多个实体(数据库中的表行)上聚合的值. 聚合的典型示例包括在一系列实体上计算某些属性或更复杂的表达式的总和、均值或最小和最大值等. 在 Entity 框架内,可用于执行聚合的结构是 AggregatedEntityClass.
顾名思义,其应用结果是生成一个新的实体类. 如果聚合是对整个原始实体类执行,则生成的实体类将仅包含一个实体,其属性为计算所得的聚合属性. 也可以先根据一组特定属性的值对实体进行分组,然后对每个此类组执行聚合,在这种情况下,新实体类所包含的新聚合实体将与此类组的个数一样多.
当前,对于数据库支持的实体存储,由 Entity 框架支持的核心聚合函数集有限且很小: TotalLengthMinMaxMeanVarianceStandardDeviation. 但是,我们可以使用标准算术函数和其他类似属性的查询构造块来组合这些核心操作并计算更复杂的表达式.
重要的是要注意,对于聚合的情况,EntityFunction 是绑定到要聚合的整个实体类,而不是该类的单个实体.
以下是使用 AggregatedEntityClass 计算单个聚合值的简单示例.
以下查询计算所有订购商品的总数:
为了了解 EntityFunction 在这种情况下的语义,可以使用 Wolfram 语言在顶层计算相同的内容.
此代码使用 Wolfram 语言进行聚合计算相同的量,其中使用 Function 使类比最明显:
计算聚合属性有更多简洁的方法.
可以使用一种较简单的语法,该语法使生成的聚合属性的名称与被聚合的属性名称相同. 这适用于一种简单的情况,即每一种情况下都聚合一个属性,但可以有多个聚合时:
但对于仅存在一个聚合的情况,还存在一种更为简洁的形式(然而返回的是标量而不是列表):
关于最后一个示例,重要的一点说明是,对于当前版本的 Entity 框架,当在顶级查询中用于聚合时,此 EntityValue 的三参数形式将在Wolfram 语言端而不是数据库端执行聚合(在子查询中使用此语法时则不会发生)。 相反,显式使用 AggregatedEntityClass 进行的所有聚合将始终在数据库端执行.
可以计算多个聚合属性.
以下查询定义了一个聚合实体类,该实体类具有三个聚合属性:所有订单的最大、最小和平均订购数量,拆分为单项订单(由 "orderdetails" 表提供):
我们可以在某些实体组上而不是整个实体类上计算聚合。 在这种情况下,必须使用 AggregatedEntityClass 的第三个参数来指示一个属性或属性列表,其值将定义要聚合的唯一实体组.
以下示例说明了这种典型的用法.
以下计算每个产品线中产品的平均购买价格:
以下计算每个办事处有多少员工:
可以不使用单个属性,而是使用一组属性来对实体进行分组.
以下查询计算来自同一城市和国家/地区的客户总数,并按客户编号降序排列:
在这种情况下,汇总是在具有相同城市和国家/地区组合的实体组上执行的.
除了使用聚合函数内部的单个属性之外,还可以使用更复杂的表达式.
以下查询计算所有已下订单的总付款额(在这种情况下, o["priceEach"]o["quantityOrdered"] 在语义上都表示数据库中的列. 由于它们属于同一个表(因此具有相同的长度),必须将其理解为向量化元素运算. 这种相乘的结果在语义上是另一列,该列传递给 Total):
以下查询以两种不同的方式计算每个订购商品的平均数量:通过显式计算和使用内置函数(Mean):

使用 CombinedEntityClass 组合实体类

在关系数据库中,数据通常存储在几个表中,这些表通过外键约束相互连接. 对于通常要写的现实查询,只有在极少数情况下,才只使用一个表,并且考虑到数据在标准化后,通常会被拆分并存储在(可能很大)数量的相关表中.
在 SQL 查询中使用多个表中的数据,最常见的方法是使用 SQL JOIN 操作. 与 SQL JOIN 相对应的 Entity 框架是 CombinedEntityClass 结构. 它需要两个实体类/类型,一个是说明如何将它们组合的规范,以及一个可选的 JOIN 类型. 结果是一个新的实体类,其中的新实体包含两个原始实体类的属性.
以下示例说明了 CombinedEntityClass 的典型用法.
以下查询通过"officeCode"属性将实体类 "employees""offices"组合在一起,然后提取属性"employeeNumber""firstName""lastName""city""country",前三个属性所属类型为"employees",后两个类型为"offices"
如果查看新类的完整属性列表,这一点将变得更加明显(实际上查看其 InputForm 更具指导性):
新类别的实体是新类型,可以通过使用 EntityList 查看:
像任何其他实体类一样,可以将应用 CombinedEntityClass 产生的新实体类进一步用于更复杂的查询中.
以下查询查找在法国或英国工作的所有员工,并列出其员工编号、名字和姓氏以及国家/地区:
以下返回信用额度大于$120,000的客户的付款信息,包括客户编号、姓名、付款金额和付款日期(此处使用 Wolfram 语言后处理步骤是必须的,因为日期存储在数据库端的 Unix 时间上):
请注意,在上面一个示例中,在 EntityValue 的属性列表中显式指示 EntityProperty["customers","customerNumber"]是必要的,因为"customers" "payments" 这两种类型都具有使用此名称的属性,因此需要明确区分所请求的属性.
以下显示来自法国的客户的客户名称以及其销售代表的名字和姓氏:
将类型与自身组合是可能的,并且通常最好这样做. 在关系数据库操作的上下文中,这种情况对应于自联接. 在这种情况下,需要更加谨慎地消除属性名称的歧义,必须为待组合的相同类型中至少一种引入(字符串)别名. 下面用示例说明这种情况.
以下查询列出了员工的名字和姓氏,以及他们的经理的姓名. 引入了第二个 "employees" 类型的 "manager" 别名,然后将其用于消除实体属性的歧义:
在某些情况下,决定是否应将来自两个实体类的两个实体的组合合并到结果实体中,其测试条件与两个实体的某些(组合)属性之间的简单等式相比要复杂得多. 在这种情况下,可以使用带有两个参数的 EntityFunction,其中每个参数绑定到待组合的对应实体类的一个实体. 这种 EntityFunction 的主体必须返回布尔值,否则可以是任意复杂(可编译)的表达式.
以下示例说明了这种情况.
以下是一个非常有趣的示例,因为它说明了 CombinedEntityClass 用法的几个不同方面. 此查询代表了查找拥有多个销售办事处的所有国家/地区的一种方法. 这个想法是将 "offices" 类型与其自身进行组合,并且作为 CombinedEntityClass 的条件,使用谓词来检查要合并的两个实体的国家/地区是否相同,而办事处代码不同. 两个 "office" 类型实体的任何此类组合的确对应于拥有多个办事处的国家. 因此,仅需提取所得合并实体的国家/地区名称,并删除可能的重复项:
在最后一个示例中,还必须在组合的 "offices" 类型中添加一个前缀,以消除属性的歧义,就像在前面的自连接示例中一样. 最后,DeleteDuplicates 用作 EntityValue 的第三个参数,以从结果中删除重复项(如前所述,重要的是要记住,在这种情况下,EntityValue 的第三个参数当前在 Wolfram 语言端执行, 而不是在数据库端).
其它工具

子查询

子查询是在更复杂的查询中用作构建块的查询. 当子查询的结果是标量时,它们采用最简单的形式. 在本节中,将通过几个子查询示例来说明其一般用法.
首先,我们来考虑一个常见任务从某个实体类获取单个聚合属性. 这还不是子查询. 以下示例说明了执行此操作的各种方法.
以下聚合查询返回所有产品的 "MSRP" 属性的最大值:
使用以下替代语法可以获得相同的结果:
重要的一点是,尽管结果相同,但第一个(较短的)版本实际上是用 Wolfram 语言执行聚合的,而后面两个版本则是在数据库端聚合.
但是,当此类查询用作较大查询中的子查询的情况下,则总是在数据库端执行,包括 EntityValue 的三参数短形式.
以下是一个包含子查询的查询的简单示例.
以下查询将前一个查询用作子查询. 根据价格范围,它会选择建议零售价在前10%最昂贵产品之内的所有产品:
上一个示例中的子查询称为不相关子查询,因为它们不引用查询外层的实体(表行). 我们也可以构建所谓的相关子查询,其中确实包含此类引用(因此,不经修改就无法单独执行).
以下示例说明了构建涉及更复杂和/或相关子查询的查询通常需要执行的步骤.
以下查询计算办事处代码为 "4" 的办事处员工总数:
也可以对办事处的实体执行相同的计算,其中硬编码的 "4" 已从条件中删除,现在有了嵌套的 EntityFunction 表达式,内部的表达式引用了外部的变量:
现在很容易理解以下代码,该代码计算每个办事处的员工人数,并根据员工人数对办事处进行降序排序:
最后一个示例是相关子查询的示例. 相关性由内部 EntityFunction 的主体引用外部 EntityFunction 的变量反映.
请注意,在许多情况下,用联接替换相关子查询非常简单.
最后一个查询示例的联接版本要简单得多:
子查询是一个强大的工具,但很容易被滥用. 它们可能是解决特定问题的最佳方法,也可能不是,这样视情况而定. 高度相关的子查询可能会导致性能降低,因此应谨慎使用此功能强大的工具.

MemberQ

在由数据库支持的实体存储上下文中,MemberQ 谓词用于在 Entity 框架查询中公开 SQL IN 运算符. 以最简单的形式,它用于测试一个值在显式值列表中的成员资格.
以下示例说明了这些用法:
EntityFunction 定义的以下属性对于办事处代码为 "2""4""7"之一的办事处生成 True,对于其它则生成 False
也可以将 MemberQ 与子查询结合使用,其中 MemberQ 的第一个参数不是文字列表,而是子查询的结果. 在这种情况下,子查询应该返回一个列而不是标量,并且通常可以写为 class["property"] 或等效的 EntityValue[class,"property"],其中 class 是某个实体类(已注册或通过内部查询定义).
以下示例说明了这些用法.
以下查询选择在美国境内设有办事处的某些城市的客户:

DeleteDuplicates 和 SQL DISTINCT

在某些情况下,只需要保留那些选定的值或具有不同值的组即可. SQL 特别为此提供了 DISTINCT 关键字. 在 Entity 框架中,可以将 DeleteDuplicates 用作 EntityValue 的第三个参数来实现类似的效果.
以下查询返回类型为 "orders""status" 属性的所有不同值的列表:
在前面的示例中,如前所述, DeleteDuplicates 当前是在 Wolfram 语言端执行. 但是,当使用诸如上述查询之类的查询作为子查询时,重复项将在数据库端被删除.
略有不同的一个使用情形是当需要汇总不同的值时. 同样,在这种情况下,可以使用 EntityFunction 内部的 DeleteDuplicates 实现此目的.
下面计算不同的确切价格的数目以及取整价格,同时计算不同平均价格和平均取整价格(忽略价格多重性):

关系

在本教程中,由 Entity 框架为相关实体类型(数据库表)生成的实体值和实体类值的实体属性称为关系.
除了与现有表列相对应的属性外,Entity 框架还为与其他表相关的表创建了新属性. 它们构成了一种机制,该机制提供了更高级别(相对于显式联接或子查询)的方式来使用查询中多个相关实体类型(数据库表)中的数据.
此类属性以前已经考虑过,但是在最基本的级别上.
重要的是要认识到,没有什么是能用关系实现而无法用核心原语实现的. 但是,在许多情况下使用关系可以使查询更简洁,构建和理解查询所要求的精力要少得多.
以下示例对关系加以说明.
考虑 "employees" 类型的属性:
在此示例中,存在四个与现有数据库列不对应的属性:"offices""customers""employees-reportsTo""employees-reverse".
在单个实体的语境中理解关系是一种最简单的方法.
考虑一些特定的员工:
要研究的属性分别对应于此员工服务的客户,该员工工作所在的办事处,该员工的经理以及以该员工为经理的所有员工:
关系可以是实体值或实体类值.
以下查询查找给定员工的所有同事的实体类:
以下是一个更有趣的查询,该查询返回与给定员工在同一个办事处工作并向同一位经理报告的所有员工:
可以使用关系生成简洁有效的查询. 下面的示例对此进行了说明.
这是使用关系来计算每个办事处的员工人数的方法:
如果在查询中进一步需要该属性,则可以使用 ExtendedEntityClass 扩展具有该属性的给定实体类:
关系的一个重要特征是,人们可以多次遵循这些关系,特别是对于实体值关系而言.
以下查询通过向每个订单添加两个新属性(下订单的客户名称和为该客户提供服务的员工的全名)来演示关系的用法。 注意如何遵循关系来提取属于相关类型(数据库表)的数据:
实体类值关系可以在查询中需要实体类的任何地方使用.
下面的示例计算每个员工所服务的客户总数,以及信用额度较高(>$50000)的客户总数,并将这些值作为新属性添加. 在这种情况下,e["customers"] 是一个实体类值关系,可以用于例如 FilteredEntityClass 中.
本教程的最后一节将提供更多如何使用关系来构建更复杂查询的示例.
Entity 框架和 SQL

引言

本节的目的是介绍一种 SQL 代码类型的基本概念,这类代码通常是由框架为各种受支持的结构所生成的. 此处提供的生成的 SQL 主要出于说明目的.
尽管本节介绍的许多 SQL 查询实际上与由 Entity 框架查询编译器的当前版本(用于 SQLite 后端)生成的 SQL 代码相对应,但生成的 SQL 不一定始终采用这种确切形式. 可能会随版本而有所不同. 从 Entity 框架的角度来看,生成的 SQL 的确切形式是内部执行细节,只要最终结果正确且效率合理即可. 因此,读者在自己的操作中,不应依赖本节生成的 SQL 代码的细节信息.

EntityValue 和 EntityList 调用

可以从一些基本的 EntityValue 调用开始:
这将对应一个简单的 SQL SELECT 语句,如下所示:
SELECT officeCode, city, state, country
FROM offices
如果在 EntityValue 中使用基于 EntityFunction 的计算属性,则会在 SQL 端使用自动生成的属性名称作为别名.
例如,以下查询:
转换成这样的 SQL 代码:
SELECT REGEXP(state, '^C', 0) AS synthetic_prop_9
FROM offices
生成的诸如 "synthetic_prop_9" 之类的属性对用户不可见,因为当请求基于 EntityFunction 的属性时,EntityValue 仅提取属性值.
对于 EntityList,数据库调用实际上会提取对给定类型的实体(对应数据库表的主键)计算 CanonicalName 所需的属性值.
例如,对于 "offices" 类型:
这将是 "officeCode" 属性:
SELECT officeCode
FROM offices

计算属性和 EntityFunction 表达式

计算属性是使用 EntityFunction 定义的,通常会编译成 SQL 表达式。 这里是一些例子.
考虑 "orderdetails" 类型. 表达式:
将编译成类似于以下内容的 SQL 表达式:
orderdetails.priceEach * orderdetails.quantityOrdered
考虑 "offices" 类型. 下式:
将编译为:
offices.state IS NULL
逻辑运算符转换起来非常简单.
例如:
将转换为以下内容:
REGEXP(offices.city, 'a', 0) OR REGEXP(offices.city, 'o', 0) AND NOT (offices.state IS NULL)
在简单的情况下,算术运算将编译为类似的 SQL 级算术运算. 例如,对于 "products" 类型:
将编译成类似以下内容:
(products.MSRP - products.buyPrice) / products.MSRP
但是,在某些情况下,生成的 SQL 代码可能涉及类型转换/强制.
例如,下式:
可能会编译成这样的内容(在数据库端执行实数转换,以保留此处涉及的 Wolfram 语言操作的语义):
power(CAST((power(CAST(products.MSRP AS REAL), 2) - power(CAST(products.buyPrice AS REAL), 2)) / (power(CAST(products.MSRP AS REAL), 2) + power(CAST(products.buyPrice" AS REAL), 2)) AS REAL), 0.5)
比较运算符直接转换.
例如,对于 "orderdetails" 类型,下式:
将转换为类似以下内容:
orderdetails.quantityOrdered > 30 AND orderdetails.priceEach >= 200
而例如这个:
将转换为:
orderdetails.orderNumber % 100 = 0
聚合表达式的转换类似.
例如,下式:
将被转换为:
sum(orderdetails.quantityOrdered)
而这里:
将转换成类似以下内容:
sum(orderdetails.quantityOrdered * orderdetails.priceEach) / CAST(count(orderdetails.orderLineNumber) AS REAL)
更复杂的表达式将转换为更复杂的 SQL 表达式.
例如,对于 "employees" 类型,下式:
将转换成类似以下内容:
CASE 
    WHEN (1 > length(employees.firstName)) THEN NULL
    WHEN 1 THEN substr(employees.firstName, 1, 1)
END IN ('M', 'P', 'D')

核心查询构建基元

FilteredEntityClass

此构造通常对应于 SQL WHERE 子句,其第二个参数(作为 EntityFunction 表达式)对应于 WHERE 子句 SQL 表达式.
例如,这个查询:
将转换为:
SELECT employees.firstName, employees.lastName, employees.jobTitle
FROM employees
WHERE employees.jobTitle != 'Sales Rep'
如果可以,查询编译器将尝试优化查询. 特别是,连续应用多个条件通常不会导致在生成的 SQL 代码产生多个 SELECT 嵌套层.
例如,以下查询使用三个嵌套的 FilteredEntityClass 结构来查找位于美国加利福尼亚州以下城市之一的所有客户:旧金山,洛杉矶或圣何塞:
生成的 SQL 如下所示:
SELECT 
    customers.customerNumber,
    customers.customerName,
    customers.creditLimit,
    customers.city
FROM customers
WHERE customers.country = 'USA' AND customers.state = 'CA' AND customers.city IN ('San Francisco', 'Los Angeles', 'San Jose')
其中所有三个不同条件都被折叠为一个条件.

SortedEntityClass

此结构对应于 SQL ORDER BY 子句.
对于此示例:
结果查询可能如下所示:
SELECT employees.employeeNumber, employees.lastName, employees.officeCode 
FROM employees
ORDER BY employees.officeCode DESC
SQL 与 Entity 框架排序工具之间的一个显著区别是,并非所有 SQL 后端都直接支持 ORDER BY 子句中的表达式,而对于 SortedEntityClass 而言,与排序相关的属性可以是简单实体属性或 EntityFunction 表达式.
例如,以下查询按雇员的名字长度对雇员进行排序:
在这种情况下,生成的查询会变得更加复杂:
SELECT "T308"."employeeNumber", "T308"."firstName", "T308"."lastName" 
FROM (
    SELECT
        "employees_T306"."employeeNumber" AS "employeeNumber",
        "employees_T306"."firstName" AS "firstName",
        "employees_T306"."lastName" AS "lastName",
        length("employees_T306"."firstName") AS synthetic_prop_17
    FROM employees AS "employees_T306"
) AS "T308"
ORDER BY "T308".synthetic_prop_17

SampledEntityClass

与该结构最直接对应的 SQL 是 LIMITOFFSET SQL 关键字. 但是,子集的实际策略和内部实现可能会有所不同,因为在某些情况下,其他策略可能会更有效地获得相同的结果.
对于以下查询:
生成的 SQL 可能如下所示:
SELECT 
    "payments_T316"."customerNumber" AS "customerNumber",
    "payments_T316".amount AS amount
FROM payments AS "payments_T316"
LIMIT 10 OFFSET 10

ExtendedEntityClass

在 SQL 方面,每当待计算的属性比数据库表或查询中的原始字段更复杂时,这种结构对应于使用 SQL 表达式来定义 SELECT 列表中的字段.
这样的表达式可能相对简单,例如以下查询:
生成与此类似的SQL:
SELECT 
    "employees_T328"."employeeNumber" AS "employeeNumber",     
    ("employees_T328"."firstName" || ' ') || "employees_T328"."lastName" AS "fullName"
FROM employees AS "employees_T328"
或者可能很复杂,包含相关子查询等,例如,以下查询使用关系
在生成的 SQL 中,SELECT 列表中的最后两个字段由(相关的)标量子查询表示:
SELECT 
    "employees_T352"."employeeNumber" AS "employeeNumber",
    "employees_T352"."firstName" AS "firstName",
    "employees_T352"."lastName" AS "lastName",
    (
        SELECT "employees_T355"."firstName" AS "firstName_1"
        FROM employees AS "employees_T355"
        WHERE "employees_T355"."employeeNumber" = "employees_T352"."reportsTo"
    ) AS "managerFirstName",
    (
        SELECT "employees_T358"."lastName" AS "lastName_1"
        FROM employees AS "employees_T358"
        WHERE "employees_T358"."employeeNumber" = "employees_T352"."reportsTo"
    ) AS "managerLastName"
FROM employees AS "employees_T352"

AggregatedEntityClass

当不带第三个参数使用时,AggregatedEntityClass 表示对整个第一个参数(实体类)执行的聚合,并且对应于 SQL 聚合查询. 在这种情况下,通常没有对应于 AggregatedEntityClass[...]的特殊 SQL 端关键字,但 SELECT 列表中的字段必须全部使用 SQL 聚合函数.
例如以下查询:
将转换为类似于以下内容的 SQL:
SELECT 
    max("orderdetails_T403"."quantityOrdered") AS "maxOrdered",
    min("orderdetails_T403"."quantityOrdered") AS "minOrdered",
    avg("orderdetails_T403"."quantityOrdered") AS "avgOrdered"
FROM orderdetails AS "orderdetails_T403"
当使用 AggregatedEntityClass 的第三个参数时,这对应于 SQL GROUP BY 子句.
例如,查询:
将转换为以下内容:
SELECT
    "customers_T434".city AS city,
    "customers_T434".country AS country,
    count("customers_T434"."customerNumber") AS "customerCount"
FROM customers AS "customers_T434"
GROUP BY "customers_T434".city, "customers_T434".country
重要的一点是,对于可能要在 AggregatedEntityClass 上进一步执行的操作,通常所生成的 SQL 将具有额外的 SELECT 层.
例如,如果希望按 "customerCount" 字段的值对上一个查询产生的实体进行排序:
现在,生成的 SQL 查询包含额外的 SELECT 层:
SELECT 
    "T497".city,
    "T497".country,
    "T497"."customerCount"
FROM (
    SELECT
        "customers_T495".city AS city,
        "customers_T495".country AS country,
        count("customers_T495"."customerNumber") AS "customerCount"
    FROM customers AS "customers_T495"
    GROUP BY "customers_T495".city, "customers_T495".country
) AS "T497"
ORDER BY "T497"."customerCount" DESC

CombinedEntityClass

这个结构对应于 SQL JOIN.
这是此类型的简单查询示例,该查询组合了类型 "employees""offices"
生成的 SQL 可能如下所示:
SELECT 
    "employees_T529"."employeeNumber" AS "employeeNumber",
    "employees_T529"."firstName" AS "firstName",
    "employees_T529"."lastName" AS "lastName",
    "T534".city,
    "T534".country
FROM employees AS "employees_T529"
JOIN (
    SELECT
        "offices_T532".city AS city,
        "offices_T532".country AS country,
        "offices_T532"."officeCode" AS "officeCode"
    FROM offices AS "offices_T532"
) AS "T534"
ON "employees_T529"."officeCode" = "T534"."officeCode"
这是前面考虑过的一个查询示例,该示例使用自联以及更复杂的联接条件,查找所有拥有至少两个不同办事处的国家/地区:
它将转换成与此类似的 SQL(请注意,当前最顶层的 EntityValue 中的 DeleteDuplicates 当前在 Wolfram 语言端执行,因此 SQL 查询中没有 DISTINCT 关键字):
SELECT "offices_T571".country AS country 
FROM offices AS "offices_T571"
JOIN (
    SELECT
        "offices_T574".country AS country_1,
        "offices_T574"."officeCode" AS "officeCode"
    FROM offices AS "offices_T574"
) AS "T576"
ON "offices_T571".country = "T576".country_1 AND "offices_T571"."officeCode" != "T576"."officeCode"

子查询

在本教程中,子查询通常是较大查询的一部分,可以使用 EntityValue 表示. 在 SQL 端,大多数情况下,这对应于带有单个字段的内部 SELECT 语句,通常返回标量(原因是仅有一行或正在执行聚合),但有时还返回一个列(将用于 IN 子句,这与 Entity 框架端的 MemberQ 相对应).
下面的示例(在子查询一节已经出现过,返回昂贵的产品)表示一个简单的不相关的子查询:
将转换为类似以下内容:
SELECT 
    "products_T583"."productName" AS "productName",
    "products_T583"."MSRP" AS "MSRP"
FROM products AS "products_T583"
WHERE "products_T583"."MSRP" >= 0.9 * (
    SELECT max("products_T586"."MSRP")
    FROM products AS "products_T586"
)
其中产品内部的查询是标量子查询.
这是前面查询的一个更复杂的版本,根据 MSRP,将每个产品扩展为当前产品的价格在$15之内的产品数量:
其中在 EntityFunction 内部定义 "closelyPricedProductsCount" 扩展属性的子查询是一个相关子查询,由于产品的筛选现在取决于当前产品的价格,因此必须从该筛选/聚合子查询中引用该价格.
这将导致包含相关子查询的查询,其中相关子查询位于内部查询的 SELECT 列表中,并以 "closelyPricedProductsCount" 作为别名,从而成为新的扩展属性:
SELECT 
    "T637"."productName",
    "T637"."MSRP",
    "T637"."closelyPricedProductsCount"
FROM (
    SELECT
        (
            SELECT "T640".count
            FROM (
                SELECT count("products_T638"."productCode") AS count
                FROM products AS "products_T638"
                WHERE abs("products_T638"."MSRP" - "products_T635"."MSRP") <= 15
            ) AS "T640"
        ) AS "closelyPricedProductsCount",
        "products_T635"."MSRP" AS "MSRP",
        "products_T635"."productName" AS "productName"
    FROM products AS "products_T635"
) AS "T637"
ORDER BY "T637"."closelyPricedProductsCount" DESC, "T637"."MSRP" DESC
当前没有自动优化生成的 SQL 代码,该代码涉及由 Entity 框架查询编译器执行的子查询(例如尝试将其转换为 JOIN 等). 应该认识到在 Entity 框架中使用相关子查询的性能影响,这与 SQL 相似.

关系

关系提供了一种高级方式,来查找与给定对象(相关数据库表)相关的实体类/类型中的属性,而无需执行显式联接. 它们的内部实现可以利用不同的工具来达到此目的,例如子查询和/或联接,但这些对于用户是隐藏的.
作为从使用关系的查询中生成的 SQL 类型的示例,请考虑以下查询. 该查询计算每个办事处员工所服务的最大客户数量(在本教程的最后一节还将会提及):
对于当前版本的查询编译器,为该查询生成的 SQL 可能看起来像这样(确实易读性差了一些):
SELECT 
    "offices_T652"."officeCode" AS "officeCode",
    (
        SELECT "T662".synthetic_prop_20
        FROM (
            SELECT max("T657"."customerCount") AS synthetic_prop_20
            FROM (
                SELECT
                (
                    SELECT "T660".synthetic_prop_21
                    FROM (
                        SELECT count("customers_T658"."customerNumber") AS synthetic_prop_21
                        FROM customers AS "customers_T658"
                        WHERE "employees_T655"."employeeNumber" = "customers_T658"."salesRepEmployeeNumber"
                    ) AS "T660"
                ) AS "customerCount"
                FROM employees AS "employees_T655"
                WHERE "offices_T652"."officeCode" = "employees_T655"."officeCode"
            ) AS "T657"
        ) AS "T662"
    ) AS "maxEmployeeCustomerCount"
FROM offices AS "offices_T652"
由于优化或内部实现的更改,使用关系的查询实际生成的SQL将来也可能会随之更改.
程序化查询的构造和生成

引言

Entity 框架查询的符号性质允许人们以编程方式生成此类查询. 此功能有许多用例.
一种用例是在多个地方重复使用查询的某些部分. 这可以是在几个不同的查询中,或在同一个查询中.
要记住的重要一点是 EntityFunctionHoldAll,因此,如果需要使用的部分查询存储在 EntityFunction 主体的变量中,则必须使用 With(或类似的结构)来将该查询部分注入 EntityFunction 的主体.

重用不同查询中的查询部分

在实践中,往往需要在几个较大的查询中重用惰性查询块. 由于查询是惰性的,因此可以轻松地做到这一点. 特别是,可以将查询的较小部分存储在变量中,并在较大的查询中使用这些变量.
以下示例说明了这种用法.
考虑以下查询,汇总付款并为所有客户生成总付款:
这些可以单独使用,例如查找前五个收入最高的客户:
但是,它也可以用于获取某一员工服务的所有客户的总付款:

在同一查询中重用查询部分

在某些情况下,在同一个或者在较大的查询中,可能需要多次重复使用同一查询. 这在 SQL 中可以使用 WITH 关键字实现. 而 Entity 框架将来可能会获得对此结构的本地支持,但实现起来仍然相当容易.
以下示例对此用法加以说明.
考虑以下查询,该查询通过汇总所有订单来计算每个特定产品的订购总次数:
可以单独使用:
也可以用于查找订购次数最多的所有商品,在这种情况下,它将在查询中出现两次. 请注意,在这种情况下,使用 With,因为应该将 totalOrderDetails 注入 EntityFunction 的主体中:

生成程序式查询

可以使用标准 Wolfram 语言技术以编程方式生成查询表达式.
下面的示例通过构建一个计算各种汇总数量的汇总查询来说明这一点.
首先,生成 AggregatedEntityClass
然后执行查询,生成字符串属性名称列表:
这个例子可能有些牵强,但它说明了重点,即可以使用标准 Wolfram 语言函数和习惯用法来实现查询构造过程的自动化.

符号查询转换

以编程方式访问查询及其符号性质,对于某些更高级的方案(例如非平凡的查询转换或生成)也可能很有用.
例如,考虑常见查询构件的运算符形式. 当前,由于各种原因,对于核心 Entity 框架查询构件尚无直接支持. 然而,用户通常更喜欢这种编写查询的样式,并且在某些情况下,它可以通过减少查询中的嵌套数量来使查询更具可读性.
以下是支持运算符形式的替代语法的简单实现.
首先可以定义一组新的符号,这些符号将用作 Entity 框架查询原语的代理,但将支持运算符形式:
下一步是定义 entityClassQ 谓词:
接下来,定义 "compile" 函数:
最后,定义运算符形式:
您现在就可以在一些示例查询中尝试此操作.
以下查询示例以操作符样式编写,并执行以下操作:选择五个收入最高的客户,并返回其客户编号、客户名称和已支付的总金额(按总金额递减的顺序排序):
诚然,查询的符号性质以及 Wolfram 语言对于符号表达式操作的出色性能开启了更多的可能性,这只是一个例子.
查询构建的实用技术

引言

本节通过各种示例,简要介绍查询构造的实用技巧,将其放在自己的工具箱中可能会很有用.
构建复杂查询的障碍之一是,该过程看上去类似于用编译语言编写代码,很难对中间结果或代码段进行测试,必须有完整的函数或程序才能进行编译和测试.
本节旨在说明,上述障碍对于 Entity 框架查询并非如此,并且通过适当的技术,可以使查询构建过程像 Wolfram 语言中的大多数其他任务一样具有交互性.

逐步建立查询

用于对本节技术进行说明的查询执行以下操作:选择五个付费最高的客户,并按递减的顺序返回其客户编号、客户名称和已支付的总额.
所显示的几个步骤从一个非常基本的查询开始,并在中间结果的指导下逐步构建您感兴趣的查询.
起点可以是仅查看 "payments" 表/实体类型的内容. 但是,出于原型制作目的,可以取 一小部分"payments" 类型的实体作为样本:
可以看到,对于一个给定的客户,通常会有几笔付款. 为了计算每个客户已支付的总金额,需要对这些值进行汇总,并按客户编号分组:
此外还需要客户名称. 获取客户名称的一种方法是使用 CombinedEntityClass,如下所示:
注意,在上一个查询的属性列表中,要使用完整的 EntityProperty["customers","customerNumber"],而不是仅使用 "customerNumber",这一点非常重要,因为 CombinedEntityClass[...] 现在包含两个 "customerNumber" 属性 ,有必要消除歧义,确定真正要求的那个.
下一步是通过使用带有正确说明的 SortedEntityClass 按总付款额对结果进行排序:
接下来,使用 SampledEntityClass 选出五个最大客户:
最后一步是将目前为止使用的样本 (samplePayments) 用完整的 "payments" 类型替换:
此示例说明了如何逐步构建查询,每次都以在上一步基础上构建的查询为起点,并检查中间结果.

使用单个实体对复杂的嵌套查询进行原型设计

在真正为整个实体类型构造查询之前,使用给定实体类型的单个实体对查询进行原型设计通常很有用. 本节使用具体示例说明此过程.
要研究的查询是,选择至少有两名雇员有客户的所有办事处.
作为起点,选择一个办事处:
在构建实际查询之前,获取使用顶层可获得的内容. 起点是查看哪些员工在该办事处工作:
接下来,查看这些员工为哪些客户提供服务:
请注意,这是一种非常低效的方法,仅在原型设计阶段才有意义,原因是它会导致执行大量查询.
下一步是构造一个查询,该查询计算每个员工的客户数量. 第一步,可以对特定员工编号进行硬编码. 例如,从前面看来,编号为1165的员工应具有六个客户.
以下查询确认了这一点:
下一步是删除硬编码的员工编号,方法如下:
在上一个查询中引入了 EntityFunction 的附加层,该层允许将特定的员工实体传递到查询中,而不是在内部对其进行硬编码.
现在,可以对给定办事处的所有员工测试得到的 EntityFunction ,产生的结果与上述基于 EntityList 的顶层分析所期望的结果一致:
下一步遵循相同的逻辑,但现在是针对办事处实体.
这个查询用于计算办事处具有客户的雇员的数量:
就像先前在 EntityList 中一样,以前构造的查询已用作内部构件,并且还使用了关系 o["employees"].
现在一切准备就绪,可以构建感兴趣的原始查询. 它可能如下所示:
请注意,如何使用单个实体逐步创建嵌套的复杂查询,以确保在每一层都能正常工作. 从技术上讲,此特定查询包含一个双重嵌套的子查询,该子查询的一个层是相关的.
这一逻辑倒过来也有效:给定一个无法正常运行并且包含复杂的内部结构(涉及嵌套的 EntityFunction 表达式等)的查询,可以将其分解成多个部分,并在单个实体上测试查询的内部部分,以快速定位并修复有问题的地方.
错误和错误处理
查询执行过程中可能发生的错误分为两大类:软错误和硬错误.

软错误

软错误是指查询的运行正常,但返回的结果与完全正确的查询所产生的结果不同.
例如,当尝试从类型中提取不存在的属性值时,将产生软错误:
在这种情况下,返回的 Missing["UnknownProperty",] 值对应于不存在的属性.
软错误的另一个示例是,当尝试对不存在的类型调用 EntityListEntityProperties 时:
在这种情况下,还将返回 Missing[]值.

硬错误

就本教程而言,硬错误是指那些返回 Failure 对象的错误. 因此,用户级的错误处理可能等同于检查 Failure 对象的 EntityValue 返回值.
以下列出了一些最常见的硬错误.

EntityFunction 中的属性无效

此类错误的一个例子是在 EntityFunction 中使用无效的属性.
以下尝试计算的表达式涉及不存在的属性 "foo"

EntityFunction 中的表达式不可编译

只要 EntityFunction 的主体包含不可编译的部分,查询编译就会失败.
以下查询失败的原因是查询中存在全局变量 y,该全局变量没有被赋值并且无法编译为 SQL:
此处出现了相同的情况,但是这次的原因是当前不支持数据库端 BesselJ 计算:

EntityFunction 中表达式的不兼容类型

硬错误的另一种常见来源是在 EntityFunction 中使用的表达式类型错误.
在以下查询中,Greater 运算符不能采用字符串参数:
在以下情况下,尝试将整数和字符串加在一起会导致出现类型错误:
通常,在这种情况下,错误消息对于确定错误原因相当有用.

查询中存在不支持类型的值

在关系数据库的背景下,Entity 框架当前不支持某些类型的 Wolfram 语言表达式.
以下查询失败,因为 10^100 的值太大而无法在数据库查询中使用:
但以下操作不会失败,因为此处的数值已明确转换为机器精度 double:
数据库后端不直接支持复数,因此以下情况也会导致错误:

EntityFunction 中的返回类型不兼容

某些以 EntityFunction 作为参数的运算需要特定的返回类型. 例如,当在 FilteredEntityClassCombinedEntityClass 中使用时,EntityFunction 应该具有布尔返回类型.
在以下示例中,过滤谓词 EntityFunction 的返回类型是整数,而需要的是布尔类型:

尝试从 EntityFunction 返回非标量

目前,不支持此类功能.
在这个示例中,EntityFunction 的结果在语义上是一个值列表,该类型不能从 EntityFunction 返回:

聚合时使用不当的关系查询

从技术上讲,使用关系构造无效的聚合查询是可能的,可以通过在同一聚合操作中组合来自不同表的列来实现. 此类查询无法编译.
以下查询失败,原因是这实际上是试图对不同表的不同列进行减法,这不是有效查询:

没有对 AggregatedEntityClass 使用 EntityFunction 中的聚合函数

对于聚合的情况,用于在 EntityFunction 中计算聚合属性的表达式必须使用聚合函数之一. 构造不进行查询的查询在技术上是可行的,在这种情况下将无法编译.
以下查询失败,因为 EntityFunction 的主体在本质上具有(值列表)的类型,而不是标量,这与前面讨论的另一个错误相似,但此处是在聚合的情况下:
以下查询不会失败,因为它包含一个聚合函数(在这种情况下为 Total),从而使 EntityFunction 的主体成为标量:

操作错误

操作错误是在数据库端发生的错误. 尽管 Entity 框架会努力尽早拦截查询中大多数与语法、类型相关的错误和其他错误,但在某些情况下,这要么尚未实现,要么可能不太容易做到.
下面的示例尝试使用一个实际上是实体类的新属性来扩展类型 "offices". 数据库支持的实体当前不支持此类操作,这种情况下错误恰巧发生在数据库端:
融会贯通:更复杂的查询示例
现实世界中的很多查询问题都要求以非平凡的方式组合多个查询构建原语. 本节通过几个有趣的示例对如何实现这一目标进行说明.

示例:按客户的支付总金额对客户排序

以下查询使用属性 "totalPaid" 扩展了 "customers" 类型,该属性给出了该客户支付的总金额,然后按 "totalPaid" 值降序排序.
一种解决方案是使用相关子查询.
这是使用相关子查询的版本:
通过使用 CombinedEntityClass 和分组聚合也可以达到相同目的(请注意,此处必须将聚合后希望使用的 "customers" 类型的那些属性添加到用于分组的属性列表中. 即使存在一个单一的 "customerName" 值与 "customerNumber" 的特定值相对应 ,查询也不知道这一点,除非我们明确地告诉它).
以下版本使用 CombinedEntityClass
可以通过使用关系,更经济地实现同一目的.
此版本使用关系:

示例:各办事处接待的客户总数

解决此类问题的一种方法是使用 CombinedEntityClass 将类型组合在一起,然后在组合的类型上进行汇总,对某些属性进行分组.
以下查询计算每个办事处所有员工服务的客户总数. 通过组合三种类型("offices""employees""customers")来实现,然后对办事处代码值进行汇总:
请注意,在上一个查询中,需要使用较长形式的 EntityProperty["offices","officeCode"] 而不是仅使用"officeCode" 来消除该属性的歧义,因为 CombinedEntityClass["offices","employees","officeCode"] 包含两个简称为 "officeCode" 的属性: EntityProperty["offices","officeCode"]EntityProperty["employees","officeCode"] (在这种情况下,使用任何一个均可).
以下是前面查询的一个更复杂的版本,计算每个办事处所服务并居住在该办事处所在国家/地区的客户总数. 在这种情况下,必须对 "country" 属性使用完整的 EntityProperty 来消除属性的歧义,因为 "offices""customers" 这两种类型都具有 "country" 属性:

示例:各办事处每个员工的最大客户数

对于这种情况,关系通常是非常简单有效的解决方案.
以下查询为每个办事处计算其每个员工服务的最大客户数. 它非常依赖关系:
为了解关系在这里为用户做了多少工作,以下是当前版本的查询编译器为此查询生成的 SQL:
SELECT 
    "offices_T652"."officeCode" AS "officeCode",
    (
        SELECT "T662".synthetic_prop_20
        FROM (
            SELECT max("T657"."customerCount") AS synthetic_prop_20
            FROM (
                SELECT
                (
                    SELECT "T660".synthetic_prop_21
                    FROM (
                        SELECT count("customers_T658"."customerNumber") AS synthetic_prop_21
                        FROM customers AS "customers_T658"
                        WHERE "employees_T655"."employeeNumber" = "customers_T658"."salesRepEmployeeNumber"
                    ) AS "T660"
                ) AS "customerCount"
                FROM employees AS "employees_T655"
                WHERE "offices_T652"."officeCode" = "employees_T655"."officeCode"
            ) AS "T657"
        ) AS "T662"
    ) AS "maxEmployeeCustomerCount"
FROM offices AS "offices_T652"

示例:欠款的客户

这是另一个展示关系力量的示例. 任务是选出欠款的客户,即当前订单总额超过总付款额的客户.
为了计算支付的总金额,可以从给定客户的所有订单开始,这由关系 c["orders"] 给出,并且是 "orders"类型的实体类. 由于每个订单可能包含多个商品,并且每个商品都可以订购多个数量,因此下一步是使用属性 "totalToPay" 扩展该类,该属性通过该订单相关的所有 "orderdetails" 实体计算商品价格和订购数量乘积的总和来计算每个订单要支付的金额. 注意,在这里关系 o["orderdetails"] 用于每个订单. 然后,可以对给定客户的所有订单进行第二次汇总,以得到客户应支付的总金额.
计算客户已经支付的总金额非常简单,因为它只需要一个关系查询 c["payments"]. 为了避免舍入误差,引入了小临界值 0.00001.
查询如下:
如果不使用关系,获得相同的结果将需要更多的工作.
以下查询是一种可能的方法,使用相关子查询和 CombinedEntityClass.
上面示例中的 With 用于提高可读性,并不是必需的;使用一个大型查询也是可能的. 但是请注意,With:= 一起用于范围变量的初始化,因此它们无需运算即可被注入到 EntityFunction 的主体中.

示例:每个员工前五个最高付费客户支付的总金额

此示例的目标是为计算每个员工所服务的五个最高付费客户要支付的总金额,并按总金额递减的顺序对员工进行排序(可用作绩效考量) .
在这里使用关系可以大大简化查询.
以下查询使用关系. 它解释了如何遵循关系方便地计算总和(例如下面的Total[c["payments"]["amount"]]). 请注意,在这种情况下,使用关系允许我们以简洁和经济的方式使用来自三个不同数据库表("employees""customers""payments")的数据:
以下版本不使用关系.
同样,在不使用关系的情况下获得相同的结果需要付出更多的努力:
与前面的示例一样,With 的主要作用是提高可读性.