我们设计了一种多范式编程语言。第2部分-PL / SQL,LINQ和GraphQL中的模型构建比较

在上一篇文章中我提出了一个问题,即现代信息系统的业务逻辑包含许多元素,这些元素的描述本质上是声明性的:概念的结构,它们之间的关系,条件,规则,从应用程序的一层移到另一层时的概念转换,从我的角度来看,就领域模型的软件实现便利性而言,功能和面向对象的样式不如逻辑样式。逻辑样式以更紧凑和自然的方式传达概念之间的关系。因此,我为自己设定了创建一种混合编程语言的目标,该语言将面向对象或功能范例与逻辑范例相结合。此外,逻辑组件应便于描述域模型-其概念的结构,以及它们之间的关系和依赖性。



在这篇文章中,我想谈谈一些流行的语言和技术,这些语言和技术包括声明式编程的元素-PL / SQLMS LINQGraphQL我将尝试找出使用声明式编程解决了哪些任务,声明式和命令式方法之间的紧密联系,它提供了哪些优势以及可以从中学习什么想法。



SQL过程扩展



让我们从这个关联早已成为行业标准的领域开始-数据访问语言。其中最著名的是PL / SQL,它是SQL语言的过程扩展。该语言允许您使用命令式(变量,控制语句,函数,对象)和声明式编程样式(SQL表达式)来处理关系数据库中的数据。使用SQL查询,我们可以描述我们需要的数据具有哪些属性-需要哪些字段,从哪些表中获取它们,它们如何相互关联,必须遵守哪些约束,必须如何汇总等等。数据库服务器将独立制定查询执行计划,并找到满足指定条件的所有可能的字段集。 PL / SQL的过程部分允许您实现这些任务难以或不可能以声明性形式表示-在循环中处理查询结果,执行任意计算,在函数和类中构造代码。



该语言的过程性和声明性成分紧密集成在一起。 PL / SQL允许您声明函数,在函数内部执行查询并返回结果,在查询内部使用函数,将表字段的值作为参数传递给它们。您可以使用游标访问查询结果,然后强制遍历所有检索到的记录。游标使您可以更好地控制表的内容,并使您可以实现比单独使用SQL更复杂的数据处理逻辑。可以将游标分配给游标变量,并将其作为参数传递给函数,过程,甚至是客户端应用程序。请求代码本身可以通过一系列命令命令动态生成。过程和查询的组合(使用一些调整)使您可以实现递归查询。 PL / SQL中甚至还有面向对象的功能,这些功能使您可以为表字段声明复合数据类型,在其中包含方法并通过继承创建类。



PL / SQL允许您在数据库服务器端实现业务逻辑。而且,领域模型的实现将非常接近其描述。领域模型的基本概念将映射到关系数据模型。这些概念将对应于表,属性-其字段。字段值的约束可以嵌入表说明中。可以使用外键设置与其他表的关系。在基本概念的基础上构建的抽象概念将与视图相对应。它们可以与表一起用于查询中,包括用于构建其他视图。视图基于查询构建,使您可以利用SQL的全部功能和灵活性。通过这种方式,从表和视图中,您可以完全以声明式样式构建相当复杂的多级域模型。可以使用过程和函数来实现所有不适合声明式样式的东西。



主要问题是PL / SQL代码仅在DB服务器端执行。这使得难以扩展这种解决方案。另外,结果模型将被严格地绑定到关系数据库,并且将其他来源的数据包含在其中将是一个问题。



语言综合查询



语言集成查询(LINQ)是.NET平台中流行的组件,它使您可以自然地在主要的面向对象的语言代码中包含SQL查询表达式。与PL / SQL向数据库服务器端的SQL添加命令式范式相反,LINQ将SQL带到了应用程序级别。因此,LINQ中的查询不仅可以用于从关系数据库中获取数据,而且还可以用于从对象,XML文档和其他LINQ查询的集合中获取数据。



LINQ体系结构非常灵活,查询定义已与OOP模型深度集成。 LINQ允许您创建自己的提供程序来访问新数据源。您还可以设置自己的执行查询的方式,例如,将查询的LINQ表达式树转换为对所需数据源的查询。您可以在请求正文中使用在应用程序代码中定义的lambda表达式和函数。的确,对于LINQ to SQL,查询将在数据库服务器端执行,这些功能将不可用,但可以使用存储过程代替。该请求是第一层语言的本质,您可以像处理普通对象一样使用它。编译器能够自动推断查询结果的类型并生成适当的类,即使尚未显式声明它也是如此。



让我们尝试使用LINQ将域模型构建为一组查询。原始事实可以放在应用程序侧的列表中,也可以放在数据库侧的表中,抽象概念可以格式化为LINQ查询。LINQ允许您通过在FROM子句中指定其他查询来构建查询。这使您可以在现有概念的基础上构造一个新概念,“选择”部分中的字段将对应于该概念的属性。WHERE部分将包含概念之间的依赖关系。带有以前出版物发票的示例将如下所示。



我们将在列表中放置带有帐户和客户信息的对象:



List<Bill> bills = new List<Bill>() { ... };
List<Client> clients = new List<Client>() { ... };


然后,我们将为他们建立查询以获取未付账单和债务人:



IEnumerable<Bill> unpaidBillsQuery =
from bill in bills
where bill.AmountToPay > bill.AmountPaid 
select bill;
IEnumerable<Client> debtorsQuery =
from bill in unpaidBillsQuery 
join client in clients on bill.ClientId equals client.ClientId
select client;


使用LINQ实现的域模型采用了一种非常奇怪的形式-一次涉及多种编程样式。该模型的顶层具有命令式语义。它可以表示为对象转换链,在集合之上构建对象的集合。查询对象是OOP世界的元素。需要创建它们,将它们分配给变量,并且必须将对它们的引用传递给其他请求。在中间层,查询对象实现了执行查询的过程,该查询在功能上使用lambda表达式进行了自定义,该表达式允许您在SELECT部分​​中形成结果结构并在WHERE子句中过滤记录。内部级别由查询执行过程表示,该过程具有逻辑语义并且基于关系代数。



尽管LINQ使得描述域模型成为可能,但是SQL语法主要针对获取和处理数据。它缺少一些对建模有用的构造。如果在PL / SQL中以表和视图的形式非常清楚地表示了基本概念的结构,那么在LINQ中,事实证明它已被呈现为OOP代码。另外,虽然可以按名称引用表和视图,但是可以以命令式引用LINQ查询。此外,SQL受关系模型的限制,并且在使用图形或树形式的结构时功能有限。



关系模型和逻辑编程之间的并行



您可以看到该模型的SQL和Prolog实现具有相似之处。在SQL中,我们基于表或其他视图构建视图,而在Prolog中,我们基于事实和规则构建规则。在SQL中,表是字段的集合,而Prolog中的谓词是属性的集合。在SQL中,我们使用谓词和布尔变量将谓词属性彼此链接,在WHERE子句中指定表字段之间的依赖关系,而在Prolog中,将它们指定为表达式。在这两种情况下,我们以声明方式设置解决方案规范,并且内置的查询执行引擎将找到的SQL记录或Prolog中变量的可能值返回给我们。



这种相似性并非偶然。尽管SQL的理论基础-关系代数是与逻辑编程并行开发的,但是后来揭示了它们之间的理论联系。它们具有共同的数学基础-一阶逻辑。关系数据模型描述了在数据表之间,逻辑编程(语句之间)之间建立关系的规则。两种理论使用不同的术语,应用于不同的领域,是并行发展的,但是它们具有共同的数学基础。



严格来说,关系演算是对一阶逻辑的一种改编,用于处理表格数据。这个问题在这里更详细地讨论...也就是说,可以将关系代数的任何表达式(任何SQL查询)重新构造为一阶逻辑的表达式,然后在Prolog中实现。但反之亦然。关系演算是一阶逻辑的子集。这意味着对于一阶逻辑中允许的某些类型的语句,我们无法在关系代数中找到类比。例如,SQL中的递归查询功能非常有限,并且传递关系的构造也不总是可用。 Prolog操作(例如目标析取和拒绝之类的拒绝操作)在SQL中很难实现。 Prolog的灵活语法为您处理复杂的嵌套结构提供了更大的灵活性,并支持对它们的模式匹配操作。这使得在处理复杂的数据结构(例如树和图)时非常方便。



但是你必须付出一切。与Prolog中的推理算法相比,关系数据库中的内置查询执行算法更简单,通用性更差。这样可以优化它们并获得更高的性能。 Prolog也无法快速处理关系数据库中的数百万行。另外,Prolog的推理算法完全不能保证程序执行的结束-某些语句的输出可能导致无限递归。



顺便说一句,在数据库和逻辑编程的交集处,还存在诸如演绎数据库以及规则和查询语言之类的技术Datalog。演绎数据库代替表中的记录,以逻辑样式存储大量事实和规则。Datalog看起来像Prolog,但是它专注于将事实合并为集合而不是单个事实。此外,其中的一阶逻辑的某些功能已被切除,以便优化推理算法,以快速处理大量数据。因此,逻辑语言表达较少的语法也有其优势。



API层描述的声明方法



SQL将模型构建绑定到数据访问层。但是在应用程序的另一端-在API层中,声明式编程也在积极开发中。它的特殊之处在于,使用该API的人员应该可以获取有关请求结构的信息。对请求和响应的结构进行正式描述是一种很好的形式。因此,期望将该描述与应用代码同步,例如,基于该请求生成请求和响应类。然后,您将需要在其中编写用于处理请求的逻辑。



GraphQL是一个用于构建API的框架,它远远超出了传统方法的范围,不仅提供查询语言,还提供查询执行环境。无需生成代码,运行时仍然可以理解请求描述。要使用GraphQL实现API,您需要:



  1. 描述作为请求和响应一部分的应用程序的数据类型(对象);
  2. 描述请求和响应的结构;
  3. 实现实现创建对象以获取其字段值的逻辑的函数。


数据类型是对象字段的描述。支持标量类型,列表,枚举和对嵌套类型的引用等类型。由于类型字段可以包含对其他类型的引用,因此整个数据模式可以表示为图形。该请求是对从API请求的数据结构的描述。请求的描述包括所需对象,它们的字段和输入属性的列表。每个数据类型及其每个字段都必须与解析器功能关联。类型(对象)解析器描述获取其对象的算法,字段解析器描述对象的字段值。它们以功能或面向对象的语言之一表示功能。 GraphQL运行时接收一个请求,确定所需的数据类型,调用其解析器,包括沿着嵌套对象链,收集响应对象。



GraphQL将声明性数据模式描述与命令性或功能性算法结合在一起以获取它们。数据模式已明确描述,并且是应用程序的核心。许多人指出,创建一个不重复数据源模式但符合域模型的数据模式是一种好习惯。这使得GraphQL成为集成各种数据源的流行解决方案。



因此,使用GraphQL语言,您可以以一种非常清晰的方式来表达领域模型,将其与其余代码区分开来,从而使模型及其实现更加紧密地结合在一起。不幸的是,该语言的声明性组件仅限于数据类型组成的描述;必须使用解析器来实现模型元素之间的所有其他关系。一方面,解析器允许开发人员独立地实现获取对象数据及其之间任何关系的任何方法。但是,另一方面,与例如通过键访问记录相比,您将不得不尝试实现更复杂的查询选项。一方面,GraphQL数据模式清楚地显示了API层与数据访问层之间的关系。但是,另一方面,数据模式所绑定的主导层是API层。数据模式的内容将对其进行调整,它将不包含处理请求中不涉及的实体。尽管数据描述语言GraphQL的表达能力不如SQL和Prolog这样的成熟的声明性语言,但是此框架的普及表明用于声明性模型描述的工具可以并且应该成为现代编程语言的一部分。



我将总结



PL / SQL是一种方便的语言,它既可以方便地以表和视图的形式描述域模型,又可以方便地使用该逻辑。声明性和过程性组件紧密集成和互补。主要问题是该语言与数据存储位置紧密相关,只能在数据库服务器端执行,而查询执行逻辑仅限于关系数据模型。



在应用程序方面,可以使用LINQ和GraphQL等技术以声明形式描述模型。使用GraphQL数据模式,您可以清晰,非常清晰地描述域模型的结构及其概念的嵌套。而且运行时能够自动收集所需的对象。不幸的是,除了嵌套之外,概念之间的所有其他关系和连接都必须在解析器功能层中实现。 LINQ具有相反的优缺点。灵活的SQL语法使您可以更加灵活地描述概念之间的关系。但是在请求之外,声明性结束,请求对象是OOP世界的元素。需要创建它们,将其分配给变量,并以命令式方式使用它们。



我想结合使用LINQ和GraphQL的优点。这样,就可以像在GraphQL中那样清晰地描述概念的结构,并且可以像在SQL中一样根据逻辑来设置它们之间的关系。这样一来,概念的定义就可以按名称直接作为类使用,而无需显式创建它们的对象,将它们分配给变量,传递对它们的引用等。



我将通过开发一种用于描述域模型的语言来开始设计这种解决方案。但是为此,有必要对现有的知识表示语言进行概述。因此,在下一个出版物中,我想讨论逻辑编程,RDF,OWL和框架逻辑语言,将它们进行比较,并尝试找到对于设计用于描述业务逻辑的语言而言有趣的功能。



对于那些不想等待所有有关Habré的出版物的发布的人,可以通过以下链接获得科学英语的全文:链接:面向混合本体的半结构化数据处理编程



链接到以前的出版物:

设计一种多范例编程语言。第1部分-目的是什么?



All Articles