从Fauna第1部分(2023 Update)现代化从PostgreSQL到无服务器的现代化
#serverless #postgres #database #动物

考虑到现代开发实践和效率,您可以从传统的关系应用过渡到具有最大规模和无数据一致性妥协的无服务器文档关联数据库。

Moving from PostgreSQL to FaunaDb

介绍

在传统关系数据库之上继续实施许多Web和移动应用程序。尽管这些数据库的许多核心组成部分已有五十年的历史,但这些设计是大多数数据库专业人员最熟悉的。但是,最近,这些选择已转向更多分布式的云原始选项,以支持高度期望。

流行的关系数据库选项之一是PostgreSQL(又称Postgres),其历史可追溯到1986年加州大学伯克利分校的Postgres项目。它自1994年以来自2001年以来一直支持SQL。 ,Postgres是其核心的客户端服务器数据库,并继续由活跃的社区增强,并于2022年底发布了版本15。

在悠久的历史上,Postgres添加了大量扩展名和选项,包括近十二个不同的数据索引选项和许多服务器复制替代方案。多个供应商在核心服务器代码上添加了数据分发选项。相比之下,其他供应商在重新设计的发动机上提供了所谓的电线级兼容性,看起来更像是传统的钥匙值商店。但是,大多数最新的实现仍然一定会在分区复杂性等领域暴露出明显的操作复杂性。

由服务数十亿用户的要求驱动的一些最大的网站导致了新的数据库体系结构替代方案的成熟,这激发了现代化的应用程序,以利用云本地,无服务器,高度分布的数据库选项。 Fauna在这些新替代方案之间增强了关系数据的一致性,同时利用云本地,无服务器,灵活的文档模型,该模型从应用程序开发人员中抽象大多数操作复杂性。

您可能是一个应用程序开发人员,希望从一开始就构建一个旨在扩展良好的应用程序。或者,您是一个数据库专业人员,希望过渡Postgres应用程序,以最大程度地减少您在继续构建功能时可能会定期遇到的缩放障碍量。无论哪种情况,本文系列都适合您。这三篇文章中的第一篇是由Brecht de Rooms构建的existing教程的更新版本。它探讨了将现有的Postgres应用程序转换为动物群的过程,同时确保您从一开始就以可扩展的方式构建数据库。

在第一部分中,我们将在Fauna和Postgres之间进行深入比较,同时我们探索如何在基本数据库中实施和查询一对一的关系。在part two中,我们扩展了这些概念,以涵盖一种多一的关系,以及如何构建一种避免对象相关阻抗不匹配的特定领域的语言。我们在part three中探索具有参考完整性的高级交易,并提出了各种建模,优化和迁移策略。


比较Postgres和Fauna

鉴于Postgres和Fauna都支持关系数据库功能,它们具有许多重要的特征。两者都支持数据关系,可序列化交易,标准化,外键,索引,约束,存储过程以及许多其他典型的关系数据库特征。支持酸性交易和严格的隔离水平,并且在动物区系默认隔离水平上都可以认为更严格,因为即使分布数据,也可以提供这些保证。两者都主要针对,并且非常适合在线交易处理(OLTP)应用程序。动物区系更多地关注此用例,因为它被设计为针对次秒查询优化的云本地数据库。同时,许多Postgres的实现和扩展可以适用于更多的分析用例。

显着差异

尽管两个数据库都可以实施大多数数据库要求,但它们的实现在某些领域有很大差异:

  1. 关系模型与多模型: Postgres通常被描述为具有对象关系数据库,其其他数据类型支持JSON和JSONB等文档,但主要是构建和用作传统的关系数据库。 Fauna通常被描述为具有JSON文档核心的文档相关数据库。与其他通用文档数据库相反,它提供了关系选项和灵活的索引以保持一致性。此外,它增加了时间性和类似图形的特征​​,例如图形遍历。
  2. 模式: Postgres和Fauna都有一个逻辑数据库的概念,其中包括描述包括记录的集合,索引和安全属性的模式记录。但是,Postgres强制执行模式,而动物群则被认为是无模式的,因为它支持添加其他字段或属性,同时仍提供了实施完整性和唯一性的机制。
  3. 数据库连接: Postgres期望像许多类似的关系数据库一样持久连接。大多数实现必须配置连接池,并且必须关注连接开销和限制。动物群使用无状态,安全的HTTP连接,不需要连接管理。
  4. 数据库作为API: Postgres是为自主或由云提供商托管或管理的,需要管理服务器实例,读取副本,分区等。有些供应商声称他们提供了无服务器的邮政局,但是在许多情况下,仍有一些准备决定要考虑。另一方面,动物群作为具有单个全局端点的API传递,并且没有簇,分区和复制要配置。此外,单个全局端点可以智能地将请求路由到最接近数据的副本。
  5. 分布:在传统数据库中分布时,异步复制是最流行的分布形式,它引入了最终的一致性和潜在的数据丢失。如果需要一致性,则提供synchronous replication作为一种选择,但通常在性能方面以高昂的价格出现,尤其是在需要跨区域分配的情况下。尽管诸如Postgres之类的传统数据库并未考虑到分销,但最近的许多改进和补充提供了多种选择,以补充其核心引擎的非分布性质。相比之下,动物群是从头开始构建的,是一个可扩展的多区域分布数据库。它的灵感来自Calvin算法,该算法通过依赖确定性计算来加快数据共识。此外,数据的分布对应用程序开发人员是透明的,并且可以容纳数据局部性限制。
  6. 多租户:,尽管Postgres提供了各种选择来创建SaaS应用程序,您可以在其中拥有多个租户(无论是跨服务器还是在单个数据库中),但Fauna具有一个独特的儿童数据库概念。儿童数据库完全彼此隔离,并能够为子数据库创建单独的权限,并且儿童数据库无法确定是否存在父母数据库。
  7. 查询语言: Postgres使用的主要语言是SQL或更准确的SQL方言。尽管SQL已经存在了数十年,并且已众所周知,记录和标准化,但每个SQL数据库都显示出该语言的显着差异。例如,为了支持JSON操作和事件流,您将遇到Postgres中使用的SQL命令与MySQL(例如MySQL)中使用的SQL命令之间的显着差异。应用程序开发人员可能会使用对象关联映射器(ORM),该映射器(ORM)添加了一些应用程序复杂性,而无需删除需要了解Postgres特定的SQL约定。 GraphQl可以通过添加扩展名与Postgrs一起使用,其中有许多选择要考虑。动物群提供了自己的本地查询语言FQL,旨在与现代编码范式保持一致。由于FQL和其他设计考虑的性质,动物群不需要ORM,也不容易注射。自定义语言的选择植根于动物区系的可扩展和分布式设计。它旨在防止长期运行的交易,在分布式方案中最大化性能,高度组合以减少往返,并具有透明且可预测的查询计划。当我们在即将到来的部分中广泛介绍FQL时,这些特征中的许多特征将在本文及其后续部分中变得明显。除了FQL之外,Fauna还支持开箱即用的GraphQl。

Postgres和Fauna:术语映射

下表总结了Fauna概念与Postgres对应物的关系,并且在大多数情况下也适用于与其他关系数据库使用的类似术语。该表来自此blog,Fauna文档提供了有关这些概念的更多详细信息以及许多其他常见的SQL命令和概念here

postgres fauna 详细信息
行或记录 文档 在Postgres中,记录(或行)表示一个不同的数据库条目,并且必须符合包含表的列定义。在动物区系中,文档代表字段及其值的嵌套结构,没有指定的结构或类型。每个文档,即使在同一集合中,也可以具有自己的独立结构,使文档示意性。此外,每个文档都是版本的,存储了文档突变从创建到删除的历史。
收集 表存储记录,集合存储文档。在Postgres中,表列定义指定了表中所有记录的结构。列定义指定可以存储在列中的值的名称和类型。在动物区系中,收藏是文档的容器,在这些文档上不施加特定的结构。
区域 动物区系没有主要或次要概念,所有区域均可提供阅读和写入。
次要,待机,副本 区域 动物区系没有主要或次要概念,所有区域均可提供阅读和写入。
复制 复制 Fauna的复制是半同步的,不需要任何操作员管理。
碎片,分区 不适用 动物群不需要操作员以任何方式管理碎片或分区。
主键 参考(Ref) 文档的唯一标识符。
外键 参考(Ref) 一个从一个文档到另一个文档的指针。
索引,物质视图 索引 动物群融合了索引和观点的概念。必须明确引用索引。
事务 事务 Postgres和Fauna都支持酸交易。
模式,数据库 数据库 Postgres和Fauna都有一个逻辑数据库的概念,其中包括描述所包括记录的集合,索引和安全属性的模式记录。在Postgres和其他关系数据库中,架构是指该数据库中定义的一组表定义和约束。该模式被强制执行,因此没有行违反其表定义或约束。在动物区系中,数据库定义还包括具有任意深嵌套的儿童数据库。动物群数据库包含示意文档,没有文档级别可用的模式执行;但是,必要时可以使用验证功能。 FAUNA GRAPHQL API确实应用了模式执行以符合GraphQl架构。
存储过程,用户定义的功能 用户定义的功能(UDF)或功能 动物群支持用fql编写的用户定义功能。

表和行是动物群中的集合和文档,就像其他文档数据库中一样。我们在传统数据库中知道的索引中的索引结合了索引和视图。由于索引包含数据,我们将直接查询索引中的索引,类似于一致的有序视图。用户定义的功能(UDF)与存储过程相似,除了与Postgres相比,查询和UDF在Fauna中以相同的语言编写,而在Postgres中,您将分为PL/pgSQL语言以存储过程。由于从查询到UDF的轻松过渡,Fauna用户通常更频繁地使用UDF。


从Postgres到Fauna:建立基础知识

在本节中,我们将学习如何创建数据库,集合,插入一些文档并查询它们。我们将重点介绍本文中的一对一关系;我们将在下一篇文章中建模一个多一的关系,并在第三部分也是最后一期中介绍其他建模选项。我们将使用著名的Postgres tutorial,该Postgres tutorial提供了DVD rental business的数据库模型。 DVD租赁应用程序的模型如下:

ERD for Sample DVD Rental Business
图。 1。 DVD租赁业务的实体关系图。资料来源:PostgreSQL Tutorial

数据库模式在其中心具有胶卷,并允许客户从全国各地的不同商店租用DVD,支持购物篮以及电影,付款,商店和客户管理。多亏了标准化模型,它支持许多访问模式:客户可以按类别,评级,发行年,标题,描述或演员找到电影。工作人员可以在当前居住或检索逾期租金列表的地方检索工作人员。

创建一个新数据库

在本节中,我们将创建数据库。即使您不遵循,此2键式过程(加上键入名称)显示了将数据库创建过程与设置传统数据库进行比较的不同。

报名

动物群是一个云数据库。无需设置硬件或配置。您需要做的就是去dashboard.fauna.com并注册一个帐户。

Fauna Sign Up Screen

创建数据库

Create a new database screen

单击新数据库创建将立即创建的新数据库。

填写数据库的名称,然后单击创建

现在您已经创建了一个数据库,可以通过在仪表板壳中粘贴提供的代码(或使用UI播放以创建集合/索引等)来跟进。当然,您还可以使用driversshell端子之一将查询发送到动物区系。外壳是在幕后的JavaScript驱动程序周围建造的。在编写FQL时,我们不是在编写字符串,而是功能,尽管当我们使用仪表板外壳时,这可能并不明显。

Fauna Dashboard Screen

一对一的关系

让我们从模型的小且简单的子集开始,然后逐渐扩展。电影和语言关系是一个多对多的关系,应该很容易建模。由于该模型仍然非常简单,因此我们会不时添加一些东西,然后绿色以使其更有趣。我们将同时添加口语和字幕语言,而不是一种语言。

Updating the ERD to add a one-to-many relationship.
图。 2。为口语和字幕语言添加一对多关系。

创建语言集合

在Postgres中,我们有包含行的表;在动物区,我们有包含文件的收藏。要创建一个新的胶片集合,我们可以使用仪表板接口,但是就像在SQL一样,可以完全在查询语言中完成集合,索引,安全角色甚至数据库的操作和创建。为了创建电影集,我们还可以在仪表板外壳中粘贴以下代码段:

CreateCollection({name: language})
创建语言文档

我们可以为语言创建等效文档,如下所示:

Create(
   Collection("language"), 
   {
       data: {
          name: "English"
       }
   }
 )

Create()函数用于创建文档,类似于Postgres中的插入语句。它将要将文档作为第一个参数和JSON对象存储在其中的集合中。应用程序数据始终嵌套在数据键下,该密钥将其与特殊动物区域分开。一旦我们执行了create()语句,我们就可以通过转到仪表板中的 collections 选项卡来查看文档。

Created language document

Fauna自动生成了两个字段:参考和TS,这是ID和last_update的动物群。

  • 参考:对文档的独特参考。参考的存在并不意味着我们不能再使用动物区系中的其他ID。我们将在开始查询时很快就会看到本地动物群引用和ID之间的区别。
  • ts:该文档的时间戳,该时间戳将自动更新文档。 Fauna的时间戳是temporality功能的一部分,可提供时间旅行并支持streaming功能。

注意:您的参考ID将不同。如果您正在关注,则可以从仪表板的集合视图中获取任何文档的参考ID。

创建电影文件

要存储电影文档,我们需要一个新的集合:

CreateCollection({name: "film"})

让我们创建几个简化的电影文档。实际上,由于动物群既是文档又是关系数据库,因此我们对电影和语言之间的关系有许多潜在的选择(至少五个)。但是,任何未归一化的东西通常都是优化,因此让我们从归一化模型开始。

我们将使它更有趣,并添加口语和字幕语言。我们将通过在文档中存储本机引用来参考先前创建的语言,如下所示(同样,您的参考ID将不同):

Create(
    Collection("film"), 
    {
       data: {
        title: "Academy Dinosaur",
        language: {
            spoken: Ref(Collection("language"), "288878259769180673"),
            subtitles: Ref(Collection("language"), "288878259769180673")
        }
      }
    }
  )

第二个文档可能是:

Create(
    Collection("film"), 
    {
       data: {
        title: Back to the Future",
        language: {
            spoken: Ref(Collection("language"), "288878259769180673"),
            subtitles: Ref(Collection("language"), "288878259769180673")
        }
      }
    }
  )

另一种方法是将语言添加为嵌入式对象。如果我们不打算直接查询语言,我们可以将其添加为嵌入式对象。

Create(
    Collection("film"),
    {
        data: {
            title: "Academy Dinosaur",
            language: {
                spoken: { name: "English" },
                subtitles: { name: "English" }
            }
        }
    }
)
嵌入与归一化数据

Fauna是一个文档数据库,它可能会让我们思考:不加入数据,因为流行文档数据库的局限性,它将其重复为解决方法。由于动物区系是关系的,因此不是必需的,而是工具带中的另一种选择。当使用动物群体与其他文档数据库合作时,这是一个重要的考虑因素:典型化是一种选择,而不是解决方法。

postgres不是本地文档数据库,而是其选项,是可用于模拟文档的JSONB类型。但是,此类列不从optimizations(例如列统计)中受益,仅限于原始操作,缺乏modify values的内置功能,并且在SQL中可能很尴尬。缺乏列统计使查询计划者视而不见,从而导致潜在的production issues。鉴于这些限制,您可能会诉诸于常规列中的经常查询属性和JSONB列中的其余部分。

相比之下,动物群的查询和索引功能不会根据文档是否包含嵌套数据或归一化而改变。这并不意味着构成规范化是推荐的实践,但它确实变得更具吸引力,作为优化阅读性能的一种技术。我们将在本系列结束时深入研究一些高级优化技术,并显示FQL如何帮助您达到灵活性,优化和数据校正之间的最佳位置。

在某些数据库中,嵌套对象嵌套时可能会受到索引灵活性。但是,不管数据是作为嵌套对象还是归一化的,都可以灵活地构建动物的索引。此外,动物群索引在嵌套值或数组上同样良好。例如,我们可以通过在索引中添加以下路径数据>语言>语言>语言来索引语言。如果语言是一个数组:

{
        data: {
            title: "Academy Dinosaur",
            language: {
                spoken: [{ name: "English" }, { name: "French" }],
                subtitles: { name: "English" }
            }
        }
    }

我们可以在索引中添加相同的路径,而动物群会认识到它是一个数组并展开它。通过使用诸如index bindings之类的高级功能,我们甚至可以将口语和字幕语言与一个索引相结合,以使用特定语言找到任何电影,无论是口语还是字幕语言。

在我们谈论权衡方面以及更替代的策略之前,让我们看看我们如何检索电影,然后通过包括语言来进行简单的加入。

查询收藏

最简单的Postgres查询很容易

SELECT id FROM film

相反,FQL是一种程序语言,例如PL/PGSQL。尽管最初是冗长的,但我们会发现我们可以将语言扩展到驯服的冗长。

本文着重于复制动物区系传统的关系模型,提供了向您介绍FQL的机会。如果您想深入研究FQL的基础知识,here是从头开始的绝佳指南,这是将基本SQL查询转换为FQL的guide

检索所有文档首先要在集合上执行Documents()功能。 Collection()函数返回该集合的引用,并从该系列中检索膜参考。

Documents(Collection("film")) 

执行第一个查询片段时,您可能会惊讶地发现,它只是返回类似的东西 - 这是对一组电影的引用,但尚未对实际的电影文档进行引用。

{
    "@set": {
        documents: Collection("film")
    }
}

就像SQL一样,FQL受到关系代数的启发。在FQL中,我们构建集合。例如,我们可以使用诸如Union()Difference()Distinct()的熟悉功能将上述语句与其他集合结合在一起。一组只是我们要检索的数据的定义,但尚未是具体数据集。

,尽管如果我们使用仪表板外壳,似乎就像字符串,但实际上我们正在使用基础JavaScript驱动程序的功能。尝试在仪表板外壳中拼写函数,您会收到熟悉的<function> undefined错误。或写:

var a = Documents(Collection("film"))
a

确保同时复制整个语句,因为仪表板外壳无法保持运行之间的变量。

现在,JavaScript变量包含我们的查询定义,我们可以使用该变量继续以比字符串串联允许的更优雅的方式编写我们的查询。许多用户将其用于extend语言或构建自己的DSL,我们将在本文的其余部分中广泛显示。

分页

实现集合并检索数据,称为Paginate()函数是强制性的;这是确保可扩展性并始终保持交易相对较小的重要措施。在食用大多数交互式API时,这是一种普遍的最佳实践。由于我们添加了两部胶片,因此我们可以添加一个大小参数以查看分页的作用。 Fauna将返回光标返回下一页。

Paginate(Documents(Collection("film")), {size: 1})

现在返回电影参考的页面:

{
  {
    after: [Ref(Collection("film"), "288877928042725889")],
    data: [Ref(Collection("film"), "288801457307648519")]
  }
}

我们称分页为一旦,我们的集合将转换为页面。 data types documentation列表哪些函数可以在集合,页面,数组等上调用。

然后,我们可以复制光标以进入下一页。

Paginate(Documents(Collection("film")), {
size: 1, 
after: [Ref(Collection("film"), "288877928042725889")]
})

在Postgres中尝试multiple ways of pagination的数据架构师可能想知道这是什么样的分页。 Fauna的方法接近Keyset方法,并巧妙地利用快照来确保在适应数据时不会更改页面。这是可能的,因为我们在动物群中查询的所有内容都得到了分类索引的支持。就像台阶一样,索引是必须避免发出表现不佳的查询的强制性的。

尽管我们似乎在上面的查询中不使用索引,但文档()实际上使用了用参考排序的内置索引。通过包括分页,上面的查询实际上与以下查询更相似,但是以卓越的分页形式,因为Fauna中的默认page size为64:

SELECT * FROM film
LIMIT 64
功能组成

在上一个查询中,我们仅返回文档引用。为了将这些引用转换为完整的文档数据,使其更加等同于SELECT *,我们将使用Map() FQL函数循环这些引用,并在每个引用上调用Get()

Map(
    Paginate(Documents(Collection("film"))),
    Lambda(["ref"], Get(Var("ref")))
) 

除了map()和get()外,我们在上面的代码段中介绍了Lambda()Var()。 lambda()是一个简单的匿名函数的FQL名称,我们可以将其传递给每个元素上的映射。 VAR()用于检索FQL变量(在这种情况下,将参数传递给lambda)。在这一点上,您可能想知道:我们可以直接检索完整的文档而不是使用MAP()吗?我们可以通过向索引添加更多值来做到这一点,当我们在第三篇文章中谈论优化时,我们将广泛解决权衡。

为什么在这里使用var()和lambda()语法?如前所述,我们通过调用JavaScript函数来组合查询。使用诸如Refâ€的字符串作为变量,并使用VAR()检索它们,有助于使JS变量与FQL变量分开。编写JavaScript函数编写查询的优点在于可扩展性。例如,我们可以通过编写等同于SQL中的简单SELECT *的简单函数来扩展FQL。

function SelectAll(name) {
    return Map(
        Paginate(Documents(Collection(name))),
        Lambda(["ref"], Get(Var("ref")))
    ) 
}

使用此功能定义,我们将其称为:

SelectAll(film")

这种合成性使您可以在其他数据库中以难以置信或难以置信的方式使用动物群,正如我们稍后涵盖优化策略时会看到的。一个典型的示例是Fauna GraphQl查询,该查询将FQL一对一地汇编,并维护相同的数据库保证。这在动物区系相对容易,但需要传统数据库中的高级技术。

查询一对多关系:多个选项

是我们决定将附加数据嵌入集合文档中或使用显式引用,我们有多个选项可以检索一对一关系中相关的数据。

查询嵌入式文档

如果我们通过直接将语言存储在胶片文档中嵌入了语言文档,则我们不必更改查询,只需返回胶片文档即可包括语言。

Map(
    Paginate(Documents(Collection("film"))),
    Lambda(["ref"], Get(Var("ref")))
) 

用get()检索电影文档将立即返回完整的文档,包括语言。

{
    ref: Ref(Collection("film"), "289321621957640705"),
    ts: 1612177450050000,
    data: {
      title: "Academy Dinosaur",
      language: {
        spoken: {
          name: "English"
        },
        subtitles: {
          name: "English"
        }
      }
    }
  }
用本机引用查询归一化数据

早些时候,我们选择存储本地参考而不是嵌入文档,如下所示:

{
    data: {
        title: "Academy Dinosaur",
        language: {
            spoken: Ref(Collection("language"), "288878259769180673"),
            subtitles: Ref(Collection("language"), "288878259769180673")
        }
    }
}

这意味着我们需要编写相当于Postgres的同等学词。在Postgres中,这将如下:

SELECT * FROM film 
JOIN "language" as spol ON spol.language_id = film.spoken_language_id 
JOIN "language" as subl ON subl.language_id = film.subtitles_language_id 
LIMIT 64

在Postgres中,我们定义了我们希望看到并依靠查询优化器选择正确的算法的加入。如果查询优化器做出了错误的判断,query performance can suffer significantly。根据数据和加入的方式,JOIN算法可能会有所不同,甚至在数据大小更改时也会发生变化。

由于动物区系的可扩展性,我们希望从价格和绩效方面进行可预测性。我们已经讨论了诸如Union(),差异()和独特()之类的设定功能,并且我们可以在不使用Fauna中使用Join()语句来实现这一目标。尽管加入一般可以扩展,但它们的性能将取决于许多因素。为了确保可预测性,我们将加入分页后的实现文件。我们将在lambda中检索电影文件,并在每个级别上铺平了薄片,就像我们何时应对多到多的加入时。

让我们继续逐步构建先前的查询,该查询将我们的电影文档返回语言参考。我们将像以前一样从这里开始:

Map(
    Paginate(Documents(Collection("film"))),
    Lambda(["ref"], Get(Var("ref")))
) 

首先,我们将稍微重构以用Let()绑定变量,从而为我们的查询带来更多的结构,并使它们更可读。在let()中,我们可以检索与我们想要的电影有关的任何内容。这里是我们的下一个增量迭代:

Map(
    Paginate(Documents(Collection("film"))),
    Lambda(["filmRef"], 
        Let({
           film: Get(Var("filmRef"))
        }, 
        // for now, we’ll return the film variable.
        Var("film")
    ))
)

接下来,我们使用Select()从电影文档中检索两种语言参考,并通过 derecrencing 获得实际语言用get()的口语和字幕语言参考。

Map(
    Paginate(Documents(Collection("film"))),
    Lambda(["filmRef"],
        Let({
            film: Get(Var("filmRef")),
            spokenLang: Get(Select(['data', 'language', 'spoken'], Var("film"))),
            subLang: Get(Select(['data', 'language', 'subtitles'], Var("film")))
        },
        // todo return
        )
    )
)

最后,在我们指定的结构中返回这些变量。现在,我们可以在外壳中尝试此完整功能:

Map(
    Paginate(Documents(Collection("film"))),
    Lambda(["filmRef"],
        Let({
            film: Get(Var("filmRef")),
            spokenLang: Get(Select(['data', 'language', 'spoken'], Var("film"))),
            subLang: Get(Select(['data', 'language', 'subtitles'], Var("film")))
        },      
        // return a JSON object
        {
            film: Var("film"),
            language: {
                spoken: Var("spokenLang"),
                subtitles: Var("subLang")
            }
        })
    )
)

现在的完整结果是完美的结构,两种语言都很好。

{
  data: [
    {
      film: {
        ref: Ref(Collection("film"), "352445336554307667"),
        ts: 1672376915430000,
        data: {
          title: "Academy Dinosaur",
          language: {
            spoken: Ref(Collection("language"), "352444818516869204"),
            subtitles: Ref(Collection("language"), "352444818516869204")
          }
        }
      },
      language: {
        spoken: {
          ref: Ref(Collection("language"), "352444818516869204"),
          ts: 1672376421400000,
          data: {
            name: "English"
          }
        },
        subtitles: {
          ref: Ref(Collection("language"), "352444818516869204"),
          ts: 1672376421400000,
          data: {
            name: "English"
          }
        }
      }
    },
    {
      film: {
        ref: Ref(Collection("film"), "352447504810246229"),
        ts: 1672378983250000,
        data: {
          title: "Back to the Future",
          language: {
            spoken: Ref(Collection("language"), "352444818516869204"),
            subtitles: Ref(Collection("language"), "352444818516869204")
          }
        }
      },
      language: {
        spoken: {
          ref: Ref(Collection("language"), "352444818516869204"),
          ts: 1672376421400000,
          data: {
            name: "English"
          }
        },
        subtitles: {
          ref: Ref(Collection("language"), "352444818516869204"),
          ts: 1672376421400000,
          data: {
            name: "English"
          }
        }
      }
    }
  ]
}
使用用户定义的主键查询归一化数据

在上面的示例中,我们可以访问电影文档中的语言参考。因此,我们可以直接使用get()来解释这些参考。我们本可以选择用户定义的主键,而不是本机的Fauna参考:

Create(
    Collection("film"),
    {
        data: {
            title: "Academy Dinosaur",
            language: {
                spoken: 6,
                subtitles: 6
            }
        }
    }
)


Create(
    Collection("language"), 
    {
       data: {
          id: 6,
          name: "English"
       }
    }
  )

Fauna仍然能够检索这些语言,但是它需要额外的索引步骤。为了进行比较,让我们再次使用此用例实现相同的查询。首先,我们需要一个索引来通过ID检索语言:

CreateIndex({
    name: "language_by_id",
    source: Collection("language"),
    terms: [
      {
        field: ["data", "id"]
      }
    ],
    values: [
      {
        field: ["ref"]
      }
    ]
})

indexes in Fauna的主要成分是术语。术语确定索引匹配的内容,而值确定其返回的内容(以及按什么顺序)。由于索引在动物群中返回值,因此它们是我们在Postgres中知道的视图和索引的混合。实际上,一旦知道应用程序的数据访问模式,我们就可以通过索引来显着优化我们的查询,正如我们稍后解释的那样。在这种情况下,我们将以通用的方式开始,仅从索引返回语言参考。

我们将使用Match()函数调用索引。只要我们只需要Match()的第一个结果,我们就可以在比赛中调用get()。我们将在以后看到如何处理多个结果。

Map(
    Paginate(Documents(Collection("film"))),
    Lambda(["filmRef"],
        Let({
            film: Get(Var("filmRef")),
            spokenLangId: Select(['data', 'language', 'spoken'], Var("film")),
            subLangId: Select(['data', 'language', 'subtitles'], Var("film")),
            spokenLang: Get(Match(Index("language_by_id"), Var("spokenLangId"))),
            subLang: Get(Match(Index("language_by_id"), Var("subLangId"))),
        },
            // and return the values however you want.
        )
    )
)

在这一点上,您可能会想知道,我应该使用本机动物参考文献还是用户定义的密钥?您可以选择。如果您在迁移的Postgres实现中使用了自定义密钥或ID,则可能还有其他业务逻辑来继续使用它们。如果您使用Fauna的本机引用,则可以简化代码并更有效地检索数据(类似于Graph Database Call index-free-adjacency)。

您可以在文档创建期间在Fauna的本机引用中设置自己的自定义ID:

Create(
    Ref(Collection('language'), '6'),
    { name: { title: 'English' } },
)

概括

我们在这里覆盖了很多地面。首先,我们概述了应用程序开发人员正在考虑远离传统关系数据库(如Postgres)的关键动机。即使具有Postgres特征成熟度,水平扩展传统数据库应用程序仍然具有挑战性,尤其是如果应用程序要处理非常重要的流量负载。我们介绍了Postgres和Fauna之间的关键差异,并提供了术语映射,该术语应该证明许多共同​​的概念的等效性。

我们完成了创建数据库,构建几个集合并在这些集合中插入文档的过程,以说明从一个集合到另一个集合的典型一对一数据库关系。我们使用本地动物群引用以及嵌入和分配数据的替代方案以及使用用户定义的ID以及这些决策如何改变我们构建查询以检索所有这些情况下的数据的方式。我们使用FQL来逐步构成看起​​来很像JavaScript的函数,说明了在您的应用程序代码中可以实现的深刻集成,而无需ORM或避免使用嵌入式SQL字符串,可以容易注射。

这使我们为第二部分设置了我们,在这里我们使用多对多的关系示例来涵盖更多的建模策略,并扩展使用FQL编写特定领域的语言的想法。最后,第三部分着重于参考完整性以及其他建模,优化和迁移思想。当您完成这一旅程时,您应该采取坚实的基础策略来过渡您的邮政驱动的应用程序。该策略将为您提供足够的指导,从一开始就拥有可扩展和性能的系统。它还将为您设置一个可组合且可测试的代码库,该代码库将帮助您维护现代应用程序最佳实践的代码。

关于作者

Luis Colon是一位数据科学家,专注于现代数据库应用程序和最佳实践以及其他无服务器,云和相关技术。他目前在Fauna,Inc。担任高级技术传教士。您可以在TwitterReddit@luiscolon1与他联系。