Celesta 7.x:“打包”的ORM,迁移和测试

也许您已经对开源Celesta库有所了解。 如果没有,没关系,现在我们将告诉您一切。 一年过去了,发布了7.x版,很多事情都发生了变化,是时候总结这些变化了,同时提醒人们Celesta的总体含义。



如果您还没有听说过Celesta,并且在阅读本文时想了解其应用程序对哪些业务任务最有效,我可以推荐旧文章的第一部分或这段半小时的视频 (关于使用Python语言的单词除外)。 但更好的是,请先阅读本文。 我将首先从版本7中发生的更改开始,然后再介绍一个完整的技术示例,该示例使用现代版本的Celesta使用Spring Boot为Java应用程序编写小型后端服务。


7.x版中有哪些更改?


  1. 我们拒绝将Jython用作Celesta内置的语言。 如果早些时候我们以业务逻辑是用Python编写的事实开始谈论Celesta,现在……任何Java语言都可以用作业务逻辑语言:Java,Groovy,JRuby或同一个Jython。 现在Celesta不再调用业务逻辑代码,但是业务逻辑代码使用Celesta及其数据访问类作为最常见的Java库。 是的,因此违反了向后兼容性,但这是我们愿意付出的代价。 不幸的是,我们对Jython的赌注失败了。 几年前,当我们开始使用Jython时,这是一个活跃而有希望的项目,但是多年来,它的开发速度减慢了,语言规范的积压工作积压了,大多数pip库的兼容性问题都没有解决。 最后一根稻草是最新语言版本中的新错误,这些错误在处理生产负荷时表现出来。 我们自己没有资源来支持Jython项目,因此我们决定退出该项目。 Celesta不再依赖Jython。
  2. 现在,数据访问类是使用Maven插件以Java语言(而不是Python)生成的。 因此,由于我们从动态类型转换为静态类型,因此存在更多的重构机会,并且编写主观正确的代码变得更加容易。
  3. 出现了对JUnit5的扩展,因此在JUnit5中编写与数据库一起使用的逻辑测试变得非常方便(稍后将进行讨论)。
  4. 出现了一个单独的项目-spring-boot-starter-celesta ,顾名思义,它是Spring Boot中的Celesta启动器。 将Celesta应用程序打包到易于部署的Spring Boot服务中的能力弥补了通过使用Python脚本简单地更改文件夹来失去在服务器上更新应用程序的能力的损失。
  5. 我们将所有文档从Wiki转移为AsciiDoctor格式,并将其与代码一起置于版本控制中,现在我们拥有每个Celesta版本的最新文档。 对于最新版本,可在此处找到在线文档: https : //courseorchestra.imtqy.com/celesta/
  6. 经常被问到是否有可能通过与Celesta分开的幂等DDL使用数据库迁移。 现在,使用2bass工具就有了这样的机会。

什么是Celesta,她能做什么?


简而言之,Celesta是:


  • 基于数据库优先设计方法的关系数据库和业务逻辑代码之间的中间层,
  • 数据库结构迁移机制,
  • 测试与数据一起使用的代码的框架。

我们支持四种类型的关系数据库:PostgreSQL,MS SQL Server,Oracle和H2。


Celesta的主要特点:


  1. 一个与Java基本原理非常相似的原理:“编写一次,在每个受支持的RDBMS上运行”。 业务逻辑代码不知道它将在哪种数据库上运行。 您可以编写业务逻辑代码并在MS SQL Server中运行它,然后切换到PostgreSQL,这将毫无复杂性地发生(嗯,几乎是:)
  2. 在实时数据库上自动重组。 Celesta项目的整个生命周期都发生在工作数据库已经存在并且充满了需要保存的数据的情况下,但是也有必要不断更改其结构。 Celesta的主要功能之一是能够自动将数据库结构“适合”您的数据模型。
  3. 测试。 人们非常注意确保Celesta的代码是可测试的,以便我们可以自动测试修改数据库中数据的方法,而无需使用诸如DbUnit和容器之类的外部工具,即可轻松,快速,优雅地进行操作。

为什么需要与DBMS类型无关?


业务逻辑代码与DBMS类型的独立性并不是我们要讲的重点:为Celesta编写的代码根本不知道它运行在哪个DBMS上。 怎么了


首先,由于选择DBMS类型不是一个技术问题,而是一个政治问题。 来到一个新的商业客户时,我们经常发现他已经拥有一种喜欢的DBMS类型的投资资金,并且该客户希望在现有基础架构上查看其他解决方案。 技术格局正在发生变化:尽管MS SQL Server几年前在我们的实践中盛行,但在政府机构和私人公司中越来越发现PostgreSQL。 Celesta支持最常见的DBMS,我们并不担心这些更改。


其次,我想将已经创建的用于解决标准问题的代码从一个项目转移到另一个项目,以创建可重用的库。 诸如分层目录或电子邮件通知分发模块之类的东西本质上是标准的,为什么我们需要为具有不同关系的客户支持多个版本?


第三,最后但并非最不重要的一点是,无需使用内存中的H2数据库即可使用DbUnit和容器来运行单元测试的能力。 在此模式下,H2基础立即启动。 Celesta可以在其中快速创建一个数据方案,之后您可以进行必要的测试并“忘记”数据库。 由于业务逻辑代码实际上不知道它是在什么基础上执行的,因此,如果它在H2上运行时没有错误,那么就可以在PostgreSQL上运行。 当然,Celesta系统本身的开发人员的任务是使用真实的DBMS进行所有测试,以确保我们的平台在不同关系上平等地执行其API。 而我们做到了。 但是不再需要业务逻辑开发人员。


CelestaSQL


跨基础主义如何实现? 当然,仅通过将逻辑与任何数据库细节隔离开的特殊API来处理数据为代价。 Celesta一方面生成用于访问数据的Java类,另一方面生成SQL代码和数据库内部的一些辅助对象。


Celesta不会以最纯粹的形式提供对象关系映射,因为在设计数据模型时,我们不是来自类,而是来自数据库结构。 也就是说,首先我们建立表的ER模型,然后,基于此模型,Celesta本身会生成用于访问数据的游标类。


您只能在每个受支持的DBMS上实现几乎相同功能的功能上才能完成相同的工作。 如果我们以“欧拉圈”的形式有条件地描述我们所支持的每个基地的功能能力,那么我们将得到以下图片:



如果我们提供与数据库类型的完全独立性,那么我们向业务逻辑程序员开放的功能应位于所有基础的交集内。 乍一看,这似乎是一个重大限制。 是:某些特定功能,例如,我们不能使用SQL Server。 但无一例外,关系数据库支持表,外键,视图,序列,JOIN和GROUP BY的SQL查询。 因此,我们可以将这些机会提供给开发人员。 我们为开发人员提供“个性化的SQL”(我们称为“ CelestaSQL”),并在此过程中为相应数据库的方言生成SQL查询。


CelestaSQL语言包括用于定义数据库对象的DDL以及用于视图和过滤器的SELECT查询,但不包含DML命令:游标用于修改数据,尚待讨论。


每个数据库都有其自己的数据类型集。 CelestaSQL也有自己的一组类型。 在撰写本文时,其中有九种,并且此表将它们与各种数据库和Java数据类型中的真实类型进行了比较。


似乎九种类型是不够的(例如,与PostgreSQL 支持的类型相比),但实际上这些类型足以存储财务,贸易和物流信息:字符串,整数,小数,日期,布尔值和Blob始终足以表示此类数据。


文档中使用大量的语法图描述了CelestaSQL语言本身。


修改数据库结构。 幂等DDL


Celesta的另一个主要功能是随着项目的发展迁移工作数据库的结构的方法。 为此,使用内置于幂等DDL的Celesta中的方法。


简而言之,当我们用CelestaSQL编写以下文本时:


CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) ); 

-Celesta不会将此文本解释为“创建表,但是如果已有表,则给出错误”,而是“将表带至所需的结构”。 也就是说:“如果没有表,请创建它,如果有表,请查看其中的字段,类型,索引,外键,默认值等,以及是否需要更改某些内容。这张桌子使它适合正确的种类。”


通过这种方法,我们实现了重构和版本控制脚本以确定数据库结构的功能:


  • 我们在脚本中看到了结构的当前“所需图像”,
  • 随着时间的推移,结构中发生了什么,由谁以及为什么发生了变化,我们可以查看版本控制系统,
  • 对于ALTER命令,Celesta会根据需要在“幕后”自动生成并执行它们。

当然,这种方法有其局限性。 Celesta尽一切努力确保自动迁移是无痛且无缝的,但这并非在所有情况下都可行。 这篇文章概述了这种方法的动机,可能性和局限性(也提供英语版本 )。


为了加快检查/更新数据库结构的过程,Celesta在数据库中应用了DDL脚本校验和的存储(直到校验和被更改,才开始检查和更新数据库结构的过程)。 为了使更新过程继续进行而不会出现与彼此依赖的对象的更改顺序有关的问题,使用外键对方案之间的依赖关系进行拓扑排序。 文档中将详细介绍自动迁移过程。


创建Celesta项目和数据模型


我们将考虑的演示项目位于github上 。 让我们看看在编写Spring Boot应用程序时如何使用Celesta。 这是您需要的Maven依赖项:


  • org.springframework.boot:spring-boot-starter-webru.curs:spring-boot-starter-celesta (有关更多详细信息, ru.curs:spring-boot-starter-celesta文档)。
  • 如果您没有使用Spring Boot,则可以直接连接ru.curs:celesta-system-services依赖项。
  • 为了基于ru.curs:celesta-maven-plugin -SQL脚本生成数据访问类的代码,需要使用ru.curs:celesta-maven-plugin演示示例或文档的源代码描述了如何连接它。
  • 要利用为修改数据的方法编写JUnit5单元测试的功能,必须在测试范围内连接ru.curs:celesta-unit

现在创建一个数据模型并编译数据访问类。


假设我们正在为一家电子商务公司做一个项目,该项目最近与另一家公司合并。 每个都有自己的数据库。 他们收集订单,但是在合并数据库之前,他们需要一个入口点才能从外部收集订单。


这个“入口点”的实现应该非常传统:带有CRUD操作的HTTP服务,该操作将数据存储在关系数据库中。


由于Celesta实施数据库优先设计方法,因此首先我们需要创建一个存储订单的表结构。 如您所知,订单是一个复合实体:它由一个标题组成,该标题存储了有关客户,订单日期和该订单其他属性的信息,以及许多行(商品)。


因此,为工作:创建


  • src/main/celestasql -默认情况下,这是CelestaSQL项目脚本的路径
  • 它包含重复Java包的文件夹结构的子文件夹(在本例中为ru/curs/demo )。
  • 在包文件夹中,创建一个具有以下内容的.sql文件:

 CREATE SCHEMA demo VERSION '1.0'; /** */ CREATE TABLE OrderHeader( id VARCHAR(30) NOT NULL, date DATETIME, customer_id VARCHAR(30), /**  */ customer_name VARCHAR(50), manager_id VARCHAR(30), CONSTRAINT Pk_OrderHeader PRIMARY KEY (id) ); /** */ CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) ); ALTER TABLE OrderLine ADD CONSTRAINT fk_OrderLine FOREIGN KEY (order_id) REFERENCES OrderHeader(id); CREATE VIEW OrderedQty AS SELECT item_id, sum(qty) AS qty FROM OrderLine GROUP BY item_id; 

在这里,我们描述了通过外键连接的两个表,以及一个视图,该视图将返回所有订单中存在的商品的汇总数量。 如您所见,这与常规SQL没什么不同,除了CREATE SCHEMA命令,我们在其中声明了demo模式版本(有关版本号如何影响自动迁移的信息,请参阅文档 )。 但是也有功能。 例如,我们使用的所有表和字段名称都只能使它们可以用Java语言转换为有效的类和变量名称。 因此,排除空格,特殊字符。 您还可以注意到,我们放在表名和某些字段上的注释不是像往常一样以/ *开头,而是以/ **开头,JavaDoc注释是如何开始的-这绝非偶然! 以/ **开头的实体定义的注释将在运行时在该实体的.getCelestaDoc()属性中提供。 当我们希望为数据库元素提供其他元信息时,这很有用:例如,人类可读的字段名称,有关如何在用户界面中表示字段的信息等。


CelestaSQL脚本执行两个同样重要的任务:首先,用于部署/修改关系数据库的结构,其次,用于数据访问类的代码生成。


我们现在可以生成数据访问类,只需运行mvn generate-sources命令,或者,如果您使用的是IDEA,请单击Maven控制面板中的“生成源和更新文件夹”按钮。 在第二种情况下,IDEA会“ target/generated-sources/celestatarget/generated-sources/celesta创建target/generated-sources/celesta文件夹,并将其内容可导入项目源代码中。 代码生成的结果如下所示-数据库中每个对象的一个​​类:



在应用程序设置中(在我们的情况下)在src/main/resources/application.yml文件中指定了到数据库的连接。 使用spring-boot-starter-celesta时,IDEA会告诉您代码完成中可用的代码选项。


如果我们不想为演示目的而使用“真实的” RDBMS,可以使用以下配置让Celesta在内存模式下使用内置H2数据库:


 celesta: h2: inMemory: true 

要连接“真实”数据库,请将配置更改为类似


 celesta: jdbc: url: jdbc:postgresql://127.0.0.1:5432/celesta username: <your_username> password: <your_password> 

(在这种情况下,您还需要通过Maven依赖项将PostgreSQL JDBC驱动程序添加到应用程序中)。


当启动与数据库服务器连接的Celesta应用程序时,可以观察到为空数据库创建了必要的表,视图,索引等,而对于非空数据库,则将它们更新为DDL中指定的结构。


创建数据处理方法


一旦确定了如何创建数据库结构,就可以开始编写业务逻辑。


为了能够实现分配访问权限和日志记录操作的要求,对Celesta中数据的任何操作均代表用户执行,没有“匿名”操作。 因此,任何Celesta代码都在CallContext类中描述的调用上下文中执行。


  • 在开始可以修改数据库中数据的操作之前,请激活CallContext
  • 激活时,将从连接池中获取与数据库的连接,然后事务开始。
  • 操作CallContext如果操作成功,则CallContext执行commit()如果执行过程中发生未处理的异常,则执行rollback()CallContext关闭并且数据库连接将返回到池中。

如果我们使用spring-boot-starter-celesta,那么@CelestaTransaction注释的所有方法自动执行这些操作。


假设我们要编写一个将文档保存到数据库的处理程序。 其控制器级别的代码可能如下所示:


 @RestController @RequestMapping("/api") public class DocumentController { private final DocumentService srv; public DocumentController(DocumentService srv) { this.srv = srv; } @PutMapping("/save") public void saveOrder(@RequestBody OrderDto order) { CallContext ctx = new CallContext("user1"); //new SystemCallContext(); srv.postOrder(ctx, order); } 

通常,在控制器方法级别(即,已经通过身份验证时),我们知道用户ID,并且可以在创建CallContext时使用它。 将用户绑定到上下文可以确定访问表的权限,还可以记录代表他所做的更改。 的确,在这种情况下,为了使代码与数据库进行交互的可操作性,必须在系统表中指示对用户“ user1”的权限。 如果您不想使用Celesta访问分配系统,并赋予会话上下文对任何表的所有权限,则可以创建SystemCallContext对象。


在服务级别保存发票的方法可能如下所示:


 @Service public class DocumentService { @CelestaTransaction public void postOrder(CallContext context, OrderDto doc) { try (OrderHeaderCursor header = new OrderHeaderCursor(context); OrderLineCursor line = new OrderLineCursor(context)) { header.setId(doc.getId()); header.setDate(Date.from(doc.getDate().atStartOfDay(ZoneId.systemDefault()).toInstant())); header.setCustomer_id(doc.getCustomerId()); header.setCustomer_name(doc.getCustomerName()); header.insert(); int lineNo = 0; for (OrderLineDto docLine : doc.getLines()) { lineNo++; line.setLine_no(lineNo); line.setOrder_id(doc.getId()); line.setItem_id(docLine.getItemId()); line.setQty(docLine.getQty()); line.insert(); } } } 

请注意@CelestaTransaction批注。 多亏了它,代理对象DocumentService将使用上述CallContext ctx参数执行所有这些服务操作。 也就是说,在方法执行的开始,它将已经绑定到数据库连接,并且事务将准备开始。 我们可以专注于编写业务逻辑。 在我们的例子中,读取OrderDto对象并将其保存到数据库。


为此,我们使用所谓的游标-使用celesta-maven-plugin生成的类。 我们已经看到了它们是什么。 为每个架构对象创建一个类-两个表和一个视图。 现在,我们可以使用这些类在业务逻辑中访问数据库对象。


要在订单表上创建游标并选择第一条记录,您需要编写以下代码:


 OrderHeaderCursor header = new OrderHeaderCursor(context); header.tryFirst(); 

创建标题对象后,我们可以通过getter和setter访问表条目的字段:



创建游标时,我们必须使用活动调用上下文-这是创建游标的唯一方法。 呼叫上下文携带有关当前用户及其访问权限的信息。


使用游标对象,我们可以做不同的事情:过滤,浏览记录,以及自然地插入,删除和更新记录。 整个游标API在文档中进行了详细描述。


例如,我们的示例代码可以按如下方式开发:


 OrderHeaderCursor header = new OrderHeaderCursor(context); header.setRange("manager_id", "manager1"); header.tryFirst(); header.setCounter(header.getCounter() + 1); header.update(); 

在此示例中,我们通过manager_id字段设置过滤器,然后使用tryFirst方法找到第一条记录。


(为什么“尝试”)

getfirstinsertupdate方法有两个选项:不带try前缀(如get(...)等)和带try前缀( tryGet(...)tryFirst()等)。 。 如果数据库没有适当的数据来执行操作,则没有try前缀的方法将引发异常。 例如,如果没有记录进入游标上的过滤器集,则first()将引发异常。 同时,带有try前缀的方法不会引发异常,而是返回一个布尔值,该值指示相应操作的成功或失败。 建议的做法是尽可能使用不带try前缀的方法。 通过这种方式,创建了“自检”代码,及时在逻辑和/或数据库数据中发出错误信号。


触发tryFirsttryFirst变量将填充一条记录的数据,我们可以读取它们并为其分配值。 当游标中的数据完全准备好后,我们执行update() ,它将游标的内容存储在数据库中。


此代码可能会受到什么问题影响? 当然,比赛条件的出现/丢失的更新! 因为在我们收到带有“ tryFirst”行的数据的那一刻到我们试图在“ update”点更新该数据的那一刻之间,其他人已经可以在数据库中接收,修改和更新该数据。 读取数据后,游标绝不会阻止其他用户使用它们! 为了防止丢失更新,Celesta使用了乐观锁定原则。 默认情况下,在每个表中, recversion都会创建一个recversion字段,并在UPDATE触发器的ON级别上递增版本号并验证更新的数据是否具有与该表相同的版本。 如果发生问题,则引发异常。 您可以在文章“ 防止丢失的更新 ”中阅读有关此内容的更多信息。


再次回顾事务与CallContext对象相关联。 如果Celesta过程成功,则会发生提交。 如果Celesta方法以未处理的异常结束,则会发生回滚。 因此,如果在某个复杂的过程中发生错误,则会回滚与调用上下文有关的整个事务,就像我们尚未开始对数据做任何事情一样,数据也不会损坏。 如果由于某种原因,您需要在某种大型过程中间进行提交,则可以通过调用context.commit()来执行显式提交。


测试数据方法


让我们创建一个单元测试,以检查将OrderDto存储在数据库中的服务方法的正确性。


当使用JUnit5和celesta-unit模块中提供的JUnit5扩展时,这非常容易。 测试的结构如下:


 @CelestaTest public class DocumentServiceTest { DocumentService srv = new DocumentService(); @Test void documentIsPutToDb(CallContext context) { OrderDto doc =... srv.postOrder(context, doc); //Check the fact that records are in the database OrderHeaderCursor header = new OrderHeaderCursor(context); header.tryFirst(); assertEquals(doc.getId(), header.getId()); OrderLineCursor line = new OrderLineCursor(context); line.setRange("order_id", doc.getId()); assertEquals(2, line.count()); } } 

感谢注解@CelestaTest (它是@CelestaTest的扩展),我们能够在测试方法中声明CallContext context参数。 该上下文已被激活并绑定到数据库(内存中的H2),因此我们不需要将服务类包装在代理中-我们使用new而不是使用Spring创建它。 但是,如有必要,请使用Spring工具将服务注入测试中,这没有任何障碍。


我们创建单元测试的前提是,在执行之前,数据库将完全为空,但是具有所需的结构,并且在执行之后,我们不必担心我们在数据库中留下了“垃圾”。 这些测试以很高的速度执行。


让我们创建第二个过程,该过程返回带有聚合值的JSON,以显示我们订购了多少产品。


该测试将两个订单写入数据库,然后检查新的getAggregateReport方法返回的getAggregateReport


 @Test void reportReturnsAggregatedQuantities(CallContext context) { srv.postOrder(context, . . .); srv.postOrder(context, . . .); Map<String, Integer> result = srv.getAggregateReport(context); assertEquals(5, result.get("A").intValue()); assertEquals(7, result.get("B").intValue()); } 

为了实现getAggregateReport方法getAggregateReport我们将使用OrderedQty视图,我记得,在CelestaSQL文件中,该视图如下所示:


 create view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id; 

要求是标准的:我们按数量汇总订单行,并按产品代码分组。 已经为该视图创建了OrderedQtyCursor游标,我们可以使用它。 我们声明此游标,对其进行迭代并收集所需的Map<String, Integer>


 @CelestaTransaction public Map<String, Integer> getAggregateReport(CallContext context) { Map<String, Integer> result = new HashMap<>(); try (OrderedQtyCursor ordered_qty = new OrderedQtyCursor(context)) { for (OrderedQtyCursor line : ordered_qty) { result.put(ordered_qty.getItem_id(), ordered_qty.getQty()); } } return result; } 

物化Celesta视图


为什么使用视图不利于获取聚合数据? 这种方法是可行的,但实际上,它在整个系统中都放置了定时炸弹:毕竟,作为SQL查询的视图在系统中累积数据时运行的速度越来越慢。 他将不得不总结和分组越来越多的行。 如何避免这种情况?


Celesta尝试实现在平台级别上业务逻辑程序员经常面临的所有标准任务。


MS SQL Server具有实例化(索引)视图的概念,这些视图存储为表并随着源表中数据的更改而快速更新。 如果我们在“干净​​的” MS SQL Server中工作,那么对于我们而言,用索引视图替换视图将正是我们所需要的:检索汇总的报告不会因为累积的数据而减慢速度,并且此时将执行更新汇总的报告的工作。将数据插入订单行表中,并且随着行数的增加也不会增加太多。


但是,如果我们通过Celesta使用PostgreSQL,该怎么办? 通过添加物化这个词来重新定义视图:


 create materialized view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id; 

让我们启动系统,看看数据库发生了什么。


我们将注意到OrderedQty消失了,而出现了OrderedQty表。 同时,在OrderLine表中填充数据后,OrderedQty表中的信息将被“神奇地”更新,就像OrderedQty将是一个视图一样。


如果我们看一下基于OrderLine表构建的触发器,那么这里没有魔术。 Celesta收到了创建“物化视图”的任务,分析了查询并在OrderLine表上创建了用于更新OrderedQty 。 通过在CelestaSQL文件中插入一个具体化的关键字,我们解决了性能下降的问题,甚至不需要更改业务逻辑代码!


, , , . «» Celesta , , JOIN-, GROUP BY. , , , , . . .


结论


Celesta. — .

Source: https://habr.com/ru/post/zh-CN455746/


All Articles