关系数据库快速入门
本教程的目的
通过学习本教程,用户应该会对 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 目前具有更齐全的功能.
教程结构
第二节回顾并说明关系数据库中 Entity 框架的主要功能.
第三节介绍实体框架提供的核心查询构建块,用于构建更复杂的查询.
第四节简要介绍一些其他有用的构造和工具,它们可能被认为更高级,但在实践中却非常有用.
第五节说明 Entity 框架支持的各种查询构建原语通常可以生成哪种SQL.
第六节讲解如何以编程方式生成查询,并举例说明这样做的好处.
第七节对一些查询构造的实用技术进行说明.
第八节简要介绍错误处理,以及在构造和运行查询时可能遇到的典型错误.
最后一节提供一些更复杂的查询示例,讨论如何使用不同的查询构建块来构造更复杂的查询.
样本数据库
本教程中的示例基于公有领域样本数据库的经典模型(尤其是其SQLite版本),该模型是经典汽车数据库比例模型的零售商.
- offices 表与 employees 表之间存在1对N的关系:通常有很多员工在同一办事处工作. 该关系是通过 employees 表的外键 officeCode 对应于 offices 表的主键 officeCode 来实现的.
- employees 表与 customers 表之间存在1到N的关系:每个员工可以负责0个、1个或多个客户,而每个客户最多只能由一个员工负责. 该关系是通过 customers 表的外键 salesRepEmployeeNumber 来实现的,该表对应于 employees 表的主键employeeNumber.
- customers 表与 payments 表之间存在1到N的关系:每个客户可能已付款0次、1次或更多次. 该关系是通过 payments 表的外键 customerNumber 来实现的,该表对应于客户表的主键 customerNumber.
- customers 表与 orders 表之间存在1到N的关系: 每个客户可能有0、1或多个订单. 该关系是通过 orders 表的外键 customerNumber 来实现的,该表对应于 customers 表的主键 customerNumber.
- orders 表与 orderdetails 表之间存在1到N的关系: 每个订单包含1个或多个项目,每个具有不同产品类型的项目在 orderdetails 表中都有一个记录. 该关系通过 orderdetails 表的外键 orderNumber 来实现,该表对应于 orders 表的主键 orderNumber.
- orderdetails 表与 products 表之间存在1到N的关系: 每个产品可能被订购0次、1次或更多次. 该关系是通过 orderdetails 表的外键 productCode 来实现的,该表对应于 products 表的主键 productCode.
- products 表与 productlines 表之间存在N到1的关系:每个产品线可能有一个或多个属于它的产品. 该关系通过 products 表的外键 productLine 实现,该表对应于 productlines 表的主键 productLine.
连接到数据库
- 使用 DatabaseReference 建立与数据库的连接.
- 使用该连接(此操作将检查数据库并检索有关数据库架构的数据库元数据等),创建 RelationalDatabase 对象.
- 从 RelationalDatabase 对象创建一个数据库支持的 EntityStore 对象.
- 向 Entity 框架注册该 EntityStore 对象.
使用该连接构造 RelationalDatabase 对象:
This creates a database-backed EntityStore:
注册 EntityStore:
首先,取消注册 EntityStore:
使用 RelationalDatabase 对象
如上一节所述,创建数据库支持的 EntityStore 所需的步骤之一是创建 RelationalDatabase 对象. 但是,此对象本身也很有用. 它包含有关数据库模式的信息(表,列,约束等),并且可以用于直观检查和以编程方式提取一部分用户感兴趣的信息.
这将创建一个 RelationalDatabase 对象:
RelationalDatabase 对象的格式使您可以使用分层群组打开器轻松直观地检查数据库结构,可以展开给定的表或列,以检查更多细节:
下面列出了给定 RelationalDatabase 对象的所有表名:
下面列出了 RelationalDatabase 对象可以识别的给定表列的所有属性:
有关属性和方法的更多详细信息,请参考 RelationalDatabase 对象的参考页面.
引言
本节介绍 Entity 框架在关系数据库方面所支持的重要核心操作. 查询构造并不是本节的主要关注点,对此后面将有专门一节进行介绍. 相反,它侧重基础,介绍一些其他重要方面,其中许多方面是通过 Entity 框架有效处理关系数据库的前提条件.
接下来,将介绍计算属性和 EntityFunction. 这些构造块非常重要,使用户可以动态定义和计算新属性,而这些新属性可能需要在数据库端执行复杂的计算.
在许多情况下,能够使用单个实体很重要. 这对于各种目的都是有用的,无论是为了对结果进行更好的可视化和了解,还是查询的构造(对于调试和原型设计,能够在单个实体上执行部分查询非常重要). 接下来简要介绍该主题.
对于关系数据库,数据库表主键是一个中心概念. 与此对应的 Entity 框架是单个实体的 CanonicalName 属性. 与规范名称一起定义单个实体的是该实体的类型. 这些主题很重要,值得用单独一节进行介绍.
尽管大多数涉及关系数据库的现代工作流程所用的数据库表都确实具有主键,但一个表不存在主键,或者存在主键但在数据库模式级别上没有强制执行的情况也并非鲜见. 在 Entity 框架方面,这些甚至更为重要,因为对于此类表/实体类型,仅 Entity 框架功能的一部分起作用. 接下来讨论的是这个问题.
尽管本教程并不关注与关系数据库交互中隐式涉及的类型和类型系统,但对于有效的工作来说,要理解非常重要的一种类型区别. 即,实体类型可以包含实体值和实体类值的属性. 在本教程中,此类属性称为“关系”,并由框架添加到每种实体类型的核心属性集合(直接对应于数据库表列的属性)中.
Entity 框架和 SQL 之间的近似映射
对于 Entity 框架的许多功能,这种映射是相当直接的. 但在某些方面,Entity 框架表示更丰富的数据模型. 例如,对于在内存中使用 Entity 框架,实体属性可以用任意 Wolfram 语言表达式作为它们的值,如果直接使用,甚至不符合关系数据库的第一个范式(例如当实体属性为 List 值时). 作为另一个示例, Entity 框架能够用无限数量的实体表示类型,这是关系数据库无法轻松实现的. 而且,在更容易进行可能计算的集合方面,关系数据库比 Wolfram 语言受到更多限制. 后者自然具有更丰富的“开箱即用”的计算功能.
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 框架方面,每个实体都必须具有在给定实体类中唯一的 CanonicalName. 这意味着无法为此类数据库表定义单个实体,因此,Entity 框架的所有与单实体相关的功能均不适用于此类数据库表/类型. 特别是,在某些情况下,带有某些修饰符的函数如 EntityList 和 EntityValue(例如 "EntityPropertyAssociation")将无法使用. 该节将进一步讨论此问题.
使用 EntityValue 和 EntityList 执行查询
主要和最常用的一种是 EntityValue. 它通常用于执行给定的查询并以各种形式获取结果. 在关系数据库上下文中,查询通常表示(虚拟)数据库表,这些表在 Entity 框架中对应于(虚拟)实体类型. 在这种情况下,EntityValue 的作用是将给定的查询编译为合适的SQL方言,在数据库上执行该查询,并以各种形式(列表或关联)为一组特定的实体属性(数据库项中的表列)提取值.
以下是一些使用 EntityValue 的简单示例.
也可以以 Dataset 的形式获取结果:
在某些情况下,可能想要获取包含在给定实体类型/类中的实体的列表。 这可以通过 EntityList 完成.
计算属性和 EntityFunction
除了提取现有属性外,还可以提取计算的属性,即动态创建的属性,这可能需要在数据库端进行复杂的计算. 此类属性必须使用 EntityFunction 表示. 在这种情况下,EntityFunction的语义与 Function 相似,在后续章节中将涉及一些重要的区别. 可以将 EntityFunction 视为类似于 EntityProperty 的构造,该构造(在本节的上下文中)接受单个实体,并返回对该实体执行的某些计算结果. 重要的是,目前 EntityFunction 只能返回标量,而不能返回例如值列表、实体或实体类等.
以下是使用 EntityFunction 检索需要在数据库端进行计算的属性的示例.
EntityFunction 具有 HoldAll 属性,如 Function 一样,可防止过早运算其变量和主体. EntityFunction的主体是一个表达式,可以使用EntityFunction 可以理解并编译为SQL的有限原语集. 此类原语的完整列表可以在 EntityFunction 文档中找到,而在后续各节中可以找到更多关于 EntityFunction 的实际使用示例.
使用单一实体
重要的是要理解单个实体本质上是实际数据的句柄(或引用). 除了类型和规范名称外,它们不包含数据. 因此,它们很懒:每当需要从实体中提取数据时,就必须对其进行新查询. 实体的这种惰性是 Entity 框架的一项非常有用的功能,因为它允许人们以一种更加抽象的方式使用实体. 但是,也有几个注意事项. 例如,如果某个特定实体的某些属性在同一查询的两次连续执行之间发生更改,则返回的结果通常也将有所不同,以反映这些更改. 也可能会发生某个时刻在给定实体类中存在的实体已被删除并且不再存在的情况. 当然,这些复杂情况只会发生在随时间变化的数据中.
可以使用 EntityValue 提取属性值:
重要的是,要认识到可以使用 EntityFunction 提取单个实体的计算属性. 这对于快速查找和对更复杂的查询进行原型设计都非常有用.
后一个示例还说明了 EntityFunction 的类似属性的性质.
实体类型,单个实体的唯一性,和 CanonicalName
实体类型和规范名称
查看实体的 InputForm:
也可以使用 CanonicalName 提取实体的规范名称.
函数 CanonicalName 也可用于实体列表:
规范名称何时是一个列表?
- 该实体属于“运行时”类型,由 CombinedEntityClass[...]表示.
- 该实体属于“运行时”类型,由 AggregatedEntityClass[...]表示,其中用于聚合的实体组是基于多个属性形成的.
前面的范例属于最后一个类别,其中分组属性为 "city" 和 "country",而更前面的范例具有 "orderdetails" 类型的实体属于第一类,其 "orderdetails" 表的主键由属性 "orderNumber" 和 "productCode"复合而成.
无主键的单个实体和数据库表
在数据库模式中,某些数据库表没有主键约束的情况可能并不是典型的,但在实践中很重要. 请注意,并非所有此类情况都是表中缺少具有唯一值的列. 但是,目前尚无办法为 Entity 框架指示这样一个列用作此类表的主键:有关主键的所有信息当前都是在数据库检查时(构造RelationalDatabase 对象时)获得的,并且 完全不受现有数据库的限制.
对于基于实体框架的工作流,这意味着此类表无法定义单个实体,因为没有明确的方法将规范名称附加到此类表中的数据库行. 这并不意味着我们对这些表完全无能为力,但 Entity 框架功能的某些部分将无法适应于这些表.
这里使用 Titanic 数据库对这些表在 Entity 框架功能方面的局限性加以说明. 该数据库是 Wolfram Cloud 中托管的一个 SQLite 数据库,并基于以下 Titanic 数据集:
从该数据集的结构可以看出,没有任何字段或字段组合可以自然地用作主键. 甚至不能保证数据集中没有重复的记录,因为数据中没有乘客的姓名,而只是乘客的舱等、年龄、性别以及他们是否幸存. 尽管如此,该数据集还是一个有用的信息来源.
第一步是创建 RelationalDatabase 对象:
下一步是基于该数据库创建和注册 EntityStore 对象:
EntityValue 对于某些修饰符也不起作用:
- 数据库中没有主键约束的数据库表对应于未定义 CanonicalName 的实体类型. 这意味着不能为此类型定义单个实体.
- 在 EntityStore 对象中注册这种类型是可能的; 但是,并非所有的 Entity 框架功能都适用.
- 尽管单个实体对于此类类型无效(诸如 EntityList 和 EntityValue 之类的函数对于某些修饰符也无效),但许多形式的 EntityValue 以及所有不涉及该类型单个实体的查询仍然有效.
实体值和实体类值的属性
可以使用 EntityList 将此实体类扩展为实体列表:
此时要注意的一个重要观察是,实体类值属性不会自动解析为实体列表,因此,如果要获取结果的实体列表,则必须将 EntityList 显式地应用于它们的值.
生成的属性将在关系一节中进一步讨论.
结果中的缺失值和无效属性名称
引言
首先通过许多示例来解释和说明类似于属性的查询. 这些示例涉及如何使用计算的属性显着扩展可用的实体属性集,以及如何使用EntityFunction 表示那些计算的属性.
EntityFunction 和类属性查询
类属性查询是用于构建 Entity 框架查询部分的 Wolfram 语言表达式,在查询编译和执行过程中被编译成 SQL 表达式. 用于构造类属性查询的主要结构是 EntityFunction.
要测试缺失值,可以使用 MissingQ 谓词.
以下查询计算一个布尔属性,对于 "orderdetails" 类型的每个实体,如果订购商品的数量 >30且单件价格 >= $200,则结果为 True,其中 Wolfram 语言端后处理仅选择结果为 True 的商品:
以下查询计算一个属性,仅当订单号能被100整除时为 True:
以下查询计算一个布尔值属性,该属性对于从下订单之日起八天内需要发货的所有订单产生 True:
在以下示例中,布尔值查询表达式用作 FilteredEntityClass 内部的是筛选条件,以查找订购数量大于30的商品的所有订单:
实体类转换器的注意事项
以下各节介绍的操作,即 FilteredEntityClass、SortedEntityClass、SampledEntityClass、ExtendedEntityClass、AggregatedEntityClass 和 CombinedEntityClass,都可以称为实体类转换器. 它们都接受一个实体类(对于 CombinedEntityClass,则为两个),并返回一个新的实体类.
重要的一点是,这些构造是完全符号性和惰性的. 当其中一个应用于实体类参数时,不会执行任何实际工作. 得到的查询仍为符号式,并且需要调用其中一个解析器函数(EntityValue 或 EntityList)以执行实际查询.
使用 EntityValue 执行实际查询.
通过使用 EntityValue 执行该查询:
Entity 框架查询的符号式和惰性为程序式查询构建开辟了道路,因为查询本身是惰性的 Wolfram 语言表达式,可以通过较小的结构块手动或以编程方式构造.
使用 FilteredEntityClass 筛选
最常用的操作之一是根据某些条件过滤数据库数据. 在 Entity 框架中,FilteredEntityClass 用于此目的,过滤条件使用 EntityFunction 表示,并作为第二个参数传递给 FilteredEntityClass.
以下示例说明了 FilteredEntityClass 的典型用法.
使用 SortedEntityClass 排序
根据某些条件对实体(数据库表中的行)进行排序也是一种常用的操作. 在数据库端,这通过使用 SQL 的 ORDER BY 语句实现. 在 Entity 框架端,可以使用 SortedEntityClass 来实现.
在最简单的情况下,需要根据单个现有实体属性(数据库列)的值以升序排序. 在这种情况下,字符串字段名称将作为第二个参数传递给 SortedEntityClass.
可以按多个属性排序. 在这种情况下,第二个属性值在组中各项的第一个属性值均相等的情况下起作用,相同的逻辑推广到第三个排序属性,依此类推. 可以为每个排序属性分别附加 "Ascending" 或 "Descending" 限定词,以控制实体的(子)组内的排序顺序.
可以通过利用 SortedEntityClass 的第三个参数来限制返回的结果数.
使用 SampledEntityClass 进行子设置
在某些情况下,从实体类中仅选择特定数量的实体很有用(或者,从数据库的角度来讲,仅从表或虚拟表中选择特定的行子集). 在 Entity 框架中,执行此操作的方法是使用 SampledEntityClass.
请注意,只有将 SampledEntityClass 应用于 SortedEntityClass 时,结果的顺序才能被保证. 但是,即使在其他情况下,例如当一个人想要浏览一个大型数据集或仅使用一个小样本来构建查询原型时,仍然会非常有用.
在这种情况下,可以通过在设置子集之前对数据集进行排序来保证顺序。 使用带有三个参数的 SortedEntityClass 可以更经济地获得相同的结果:
通过 ExtendedEntityClass 引入新属性
将新的、计算得到的属性添加到给定类型/实体类的一组可用实体属性中是可用的。 结果是一个新的实体类。 新添加的属性可以在原始属性可以使用的任何地方使用,如在 EntityValue 中,甚至在查询的外层使用,等等。创建这种新扩展实体类的结构是 ExtendedEntityClass.
使用 AggregatedEntityClass 进行聚合
聚合是一个非常常用的操作. 它用于计算在多个实体(数据库中的表行)上聚合的值. 聚合的典型示例包括在一系列实体上计算某些属性或更复杂的表达式的总和、均值或最小和最大值等. 在 Entity 框架内,可用于执行聚合的结构是 AggregatedEntityClass.
顾名思义,其应用结果是生成一个新的实体类. 如果聚合是对整个原始实体类执行,则生成的实体类将仅包含一个实体,其属性为计算所得的聚合属性. 也可以先根据一组特定属性的值对实体进行分组,然后对每个此类组执行聚合,在这种情况下,新实体类所包含的新聚合实体将与此类组的个数一样多.
当前,对于数据库支持的实体存储,由 Entity 框架支持的核心聚合函数集有限且很小: Total,Length,Min,Max,Mean,Variance 和 StandardDeviation. 但是,我们可以使用标准算术函数和其他类似属性的查询构造块来组合这些核心操作并计算更复杂的表达式.
重要的是要注意,对于聚合的情况,EntityFunction 是绑定到要聚合的整个实体类,而不是该类的单个实体.
以下是使用 AggregatedEntityClass 计算单个聚合值的简单示例.
为了了解 EntityFunction 在这种情况下的语义,可以使用 Wolfram 语言在顶层计算相同的内容.
此代码使用 Wolfram 语言进行聚合计算相同的量,其中使用 Function 使类比最明显:
关于最后一个示例,重要的一点说明是,对于当前版本的 Entity 框架,当在顶级查询中用于聚合时,此 EntityValue 的三参数形式将在Wolfram 语言端而不是数据库端执行聚合(在子查询中使用此语法时则不会发生)。 相反,显式使用 AggregatedEntityClass 进行的所有聚合将始终在数据库端执行.
我们可以在某些实体组上而不是整个实体类上计算聚合。 在这种情况下,必须使用 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 产生的新实体类进一步用于更复杂的查询中.
请注意,在上面一个示例中,在 EntityValue 的属性列表中显式指示 EntityProperty["customers","customerNumber"]是必要的,因为"customers" 和 "payments" 这两种类型都具有使用此名称的属性,因此需要明确区分所请求的属性.
将类型与自身组合是可能的,并且通常最好这样做. 在关系数据库操作的上下文中,这种情况对应于自联接. 在这种情况下,需要更加谨慎地消除属性名称的歧义,必须为待组合的相同类型中至少一种引入(字符串)别名. 下面用示例说明这种情况.
在某些情况下,决定是否应将来自两个实体类的两个实体的组合合并到结果实体中,其测试条件与两个实体的某些(组合)属性之间的简单等式相比要复杂得多. 在这种情况下,可以使用带有两个参数的 EntityFunction,其中每个参数绑定到待组合的对应实体类的一个实体. 这种 EntityFunction 的主体必须返回布尔值,否则可以是任意复杂(可编译)的表达式.
以下是一个非常有趣的示例,因为它说明了 CombinedEntityClass 用法的几个不同方面. 此查询代表了查找拥有多个销售办事处的所有国家/地区的一种方法. 这个想法是将 "offices" 类型与其自身进行组合,并且作为 CombinedEntityClass 的条件,使用谓词来检查要合并的两个实体的国家/地区是否相同,而办事处代码不同. 两个 "office" 类型实体的任何此类组合的确对应于拥有多个办事处的国家. 因此,仅需提取所得合并实体的国家/地区名称,并删除可能的重复项:
在最后一个示例中,还必须在组合的 "offices" 类型中添加一个前缀,以消除属性的歧义,就像在前面的自连接示例中一样. 最后,DeleteDuplicates 用作 EntityValue 的第三个参数,以从结果中删除重复项(如前所述,重要的是要记住,在这种情况下,EntityValue 的第三个参数当前在 Wolfram 语言端执行, 而不是在数据库端).
子查询
但是,当此类查询用作较大查询中的子查询的情况下,则总是在数据库端执行,包括 EntityValue 的三参数短形式.
MemberQ
在由数据库支持的实体存储上下文中,MemberQ 谓词用于在 Entity 框架查询中公开 SQL IN 运算符. 以最简单的形式,它用于测试一个值在显式值列表中的成员资格.
也可以将 MemberQ 与子查询结合使用,其中 MemberQ 的第一个参数不是文字列表,而是子查询的结果. 在这种情况下,子查询应该返回一个列而不是标量,并且通常可以写为 class["property"] 或等效的 EntityValue[class,"property"],其中 class 是某个实体类(已注册或通过内部查询定义).
DeleteDuplicates 和 SQL DISTINCT
在某些情况下,只需要保留那些选定的值或具有不同值的组即可. SQL 特别为此提供了 DISTINCT 关键字. 在 Entity 框架中,可以将 DeleteDuplicates 用作 EntityValue 的第三个参数来实现类似的效果.
在前面的示例中,如前所述, DeleteDuplicates 当前是在 Wolfram 语言端执行. 但是,当使用诸如上述查询之类的查询作为子查询时,重复项将在数据库端被删除.
关系
除了与现有表列相对应的属性外,Entity 框架还为与其他表相关的表创建了新属性. 它们构成了一种机制,该机制提供了更高级别(相对于显式联接或子查询)的方式来使用查询中多个相关实体类型(数据库表)中的数据.
如果在查询中进一步需要该属性,则可以使用 ExtendedEntityClass 扩展具有该属性的给定实体类:
下面的示例计算每个员工所服务的客户总数,以及信用额度较高(>$50000)的客户总数,并将这些值作为新属性添加. 在这种情况下,e["customers"] 是一个实体类值关系,可以用于例如 FilteredEntityClass 中.
本教程的最后一节将提供更多如何使用关系来构建更复杂查询的示例.
引言
尽管本节介绍的许多 SQL 查询实际上与由 Entity 框架查询编译器的当前版本(用于 SQLite 后端)生成的 SQL 代码相对应,但生成的 SQL 不一定始终采用这种确切形式. 可能会随版本而有所不同. 从 Entity 框架的角度来看,生成的 SQL 的确切形式是内部执行细节,只要最终结果正确且效率合理即可. 因此,读者在自己的操作中,不应依赖本节生成的 SQL 代码的细节信息.
EntityValue 和 EntityList 调用
可以从一些基本的 EntityValue 调用开始:
SELECT officeCode, city, state, country
FROM offices
SELECT REGEXP(state, '^C', 0) AS synthetic_prop_9
FROM offices
SELECT officeCode
FROM offices
计算属性和 EntityFunction 表达式
计算属性是使用 EntityFunction 定义的,通常会编译成 SQL 表达式。 这里是一些例子.
orderdetails.priceEach * orderdetails.quantityOrdered
offices.state IS NULL
REGEXP(offices.city, 'a', 0) OR REGEXP(offices.city, 'o', 0) AND NOT (offices.state IS NULL)
(products.MSRP - products.buyPrice) / products.MSRP
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.quantityOrdered > 30 AND orderdetails.priceEach >= 200
orderdetails.orderNumber % 100 = 0
sum(orderdetails.quantityOrdered)
sum(orderdetails.quantityOrdered * orderdetails.priceEach) / CAST(count(orderdetails.orderLineNumber) AS REAL)
CASE
WHEN (1 > length(employees.firstName)) THEN NULL
WHEN 1 THEN substr(employees.firstName, 1, 1)
END IN ('M', 'P', 'D')
核心查询构建基元
FilteredEntityClass
SELECT employees.firstName, employees.lastName, employees.jobTitle
FROM employees
WHERE employees.jobTitle != 'Sales Rep'
例如,以下查询使用三个嵌套的 FilteredEntityClass 结构来查找位于美国加利福尼亚州以下城市之一的所有客户:旧金山,洛杉矶或圣何塞:
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
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
SELECT
"payments_T316"."customerNumber" AS "customerNumber",
"payments_T316".amount AS amount
FROM payments AS "payments_T316"
LIMIT 10 OFFSET 10
ExtendedEntityClass
SELECT
"employees_T328"."employeeNumber" AS "employeeNumber",
("employees_T328"."firstName" || ' ') || "employees_T328"."lastName" AS "fullName"
FROM employees AS "employees_T328"
或者可能很复杂,包含相关子查询等,例如,以下查询使用关系:
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 聚合函数.
SELECT
max("orderdetails_T403"."quantityOrdered") AS "maxOrdered",
min("orderdetails_T403"."quantityOrdered") AS "minOrdered",
avg("orderdetails_T403"."quantityOrdered") AS "avgOrdered"
FROM orderdetails AS "orderdetails_T403"
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
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
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"
)
其中在 EntityFunction 内部定义 "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 相似.
关系
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"
引言
要记住的重要一点是 EntityFunction 是 HoldAll,因此,如果需要使用的部分查询存储在 EntityFunction 主体的变量中,则必须使用 With(或类似的结构)来将该查询部分注入 EntityFunction 的主体.
重用不同查询中的查询部分
在同一查询中重用查询部分
在某些情况下,在同一个或者在较大的查询中,可能需要多次重复使用同一查询. 这在 SQL 中可以使用 WITH 关键字实现. 而 Entity 框架将来可能会获得对此结构的本地支持,但实现起来仍然相当容易.
也可以用于查找订购次数最多的所有商品,在这种情况下,它将在查询中出现两次. 请注意,在这种情况下,使用 With,因为应该将 totalOrderDetails 注入 EntityFunction 的主体中:
生成程序式查询
首先,生成 AggregatedEntityClass:
符号查询转换
例如,考虑常见查询构件的运算符形式. 当前,由于各种原因,对于核心 Entity 框架查询构件尚无直接支持. 然而,用户通常更喜欢这种编写查询的样式,并且在某些情况下,它可以通过减少查询中的嵌套数量来使查询更具可读性.
引言
逐步建立查询
此外还需要客户名称. 获取客户名称的一种方法是使用 CombinedEntityClass,如下所示:
注意,在上一个查询的属性列表中,要使用完整的 EntityProperty["customers","customerNumber"],而不是仅使用 "customerNumber",这一点非常重要,因为 CombinedEntityClass[...] 现在包含两个 "customerNumber" 属性 ,有必要消除歧义,确定真正要求的那个.
下一步是通过使用带有正确说明的 SortedEntityClass 按总付款额对结果进行排序:
接下来,使用 SampledEntityClass 选出五个最大客户:
使用单个实体对复杂的嵌套查询进行原型设计
在上一个查询中引入了 EntityFunction 的附加层,该层允许将特定的员工实体传递到查询中,而不是在内部对其进行硬编码.
这一逻辑倒过来也有效:给定一个无法正常运行并且包含复杂的内部结构(涉及嵌套的 EntityFunction 表达式等)的查询,可以将其分解成多个部分,并在单个实体上测试查询的内部部分,以快速定位并修复有问题的地方.
软错误
在这种情况下,返回的 Missing["UnknownProperty",…] 值对应于不存在的属性.
在这种情况下,还将返回 Missing[]值.
硬错误
EntityFunction 中的属性无效
此类错误的一个例子是在 EntityFunction 中使用无效的属性.
EntityFunction 中的表达式不可编译
只要 EntityFunction 的主体包含不可编译的部分,查询编译就会失败.
此处出现了相同的情况,但是这次的原因是当前不支持数据库端 BesselJ 计算:
EntityFunction 中表达式的不兼容类型
硬错误的另一种常见来源是在 EntityFunction 中使用的表达式类型错误.
在以下查询中,Greater 运算符不能采用字符串参数:
查询中存在不支持类型的值
EntityFunction 中的返回类型不兼容
某些以 EntityFunction 作为参数的运算需要特定的返回类型. 例如,当在 FilteredEntityClass 或 CombinedEntityClass 中使用时,EntityFunction 应该具有布尔返回类型.
在以下示例中,过滤谓词 EntityFunction 的返回类型是整数,而需要的是布尔类型:
尝试从 EntityFunction 返回非标量
聚合时使用不当的关系查询
没有对 AggregatedEntityClass 使用 EntityFunction 中的聚合函数
对于聚合的情况,用于在 EntityFunction 中计算聚合属性的表达式必须使用聚合函数之一. 构造不进行查询的查询在技术上是可行的,在这种情况下将无法编译.
操作错误
示例:按客户的支付总金额对客户排序
通过使用 CombinedEntityClass 和分组聚合也可以达到相同目的(请注意,此处必须将聚合后希望使用的 "customers" 类型的那些属性添加到用于分组的属性列表中. 即使存在一个单一的 "customerName" 值与 "customerNumber" 的特定值相对应 ,查询也不知道这一点,除非我们明确地告诉它).
以下版本使用 CombinedEntityClass:
示例:各办事处接待的客户总数
解决此类问题的一种方法是使用 CombinedEntityClass 将类型组合在一起,然后在组合的类型上进行汇总,对某些属性进行分组.
请注意,在上一个查询中,需要使用较长形式的 EntityProperty["offices","officeCode"] 而不是仅使用"officeCode" 来消除该属性的歧义,因为 CombinedEntityClass["offices","employees","officeCode"] 包含两个简称为 "officeCode" 的属性: EntityProperty["offices","officeCode"] 和EntityProperty["employees","officeCode"] (在这种情况下,使用任何一个均可).
以下是前面查询的一个更复杂的版本,计算每个办事处所服务并居住在该办事处所在国家/地区的客户总数. 在这种情况下,必须对 "country" 属性使用完整的 EntityProperty 来消除属性的歧义,因为 "offices" 和 "customers" 这两种类型都具有 "country" 属性:
示例:各办事处每个员工的最大客户数
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"] 用于每个订单. 然后,可以对给定客户的所有订单进行第二次汇总,以得到客户应支付的总金额.
以下查询是一种可能的方法,使用相关子查询和 CombinedEntityClass.
上面示例中的 With 用于提高可读性,并不是必需的;使用一个大型查询也是可能的. 但是请注意,With 与 := 一起用于范围变量的初始化,因此它们无需运算即可被注入到 EntityFunction 的主体中.
示例:每个员工前五个最高付费客户支付的总金额
以下查询使用关系. 它解释了如何遵循关系方便地计算总和(例如下面的Total[c["payments"]["amount"]]). 请注意,在这种情况下,使用关系允许我们以简洁和经济的方式使用来自三个不同数据库表("employees","customers" 和 "payments")的数据:
与前面的示例一样,With 的主要作用是提高可读性.