Faunadb:开发人员的现代数据库
#javascript #database #动物

Faunadb是具有内置文档数据模型的多范式关系数据库。 Faunadb为开发人员提供了使用云API中的关系和文档数据库的好处,该数据库几乎可以从任何平台访问。 Faunadb的查询语言称为FQL(Fauna查询语言),一种面向表达的查询语言,可以实现复杂,精确的操作和对文档的检索。所有FQL查询都是交易确保数据完整性的。 Faunadb是多区域的,并使用主动活动群集水平扩展,同时合并大多数现代的应用程序安全功能。

在今天的帖子中,我们将浏览以下内容,而我们发现了什么是faunadb;

  • 它的工作原理
  • 创建一个帐户和数据库
  • 设置本地开发
  • 运行FQL查询

Faunadb如何工作?

文件

faunadb将您的数据存储为类似JSON的对象。这种方法类似于您在MongoDB,Firestore和其他面向文档的数据库中观察到的方法。所有文档都有时间戳和独特的参考。文档ref由集合的名称和字符串中的数字组成。默认情况下,所有文档都是不变的,并且更新创建新文档,每个文档都允许其时间戳进行延时查询。

收藏

收集只是将相关文档分组的一种方式,它们与SQL中的表相似。可以将多个示意性文档存储在集合中,当删除集合时,相关的文档变得无法访问并被异步删除。

参考

参考是一个复合值,它是集合中任何文档的唯一ID,参考由对包含文档的集合的引用和数字ID组成。数据库中的两个文档可以共享相同的参考。

索引

索引允许通过数据属性进行组织和轻松检索文档。索引只是一个查找表,可以加快查找文档的速度,您可以查询一个索引以查找内部的文档,而不是读取集合中的所有文档以选择几个文档。

功能

FQL提供可用于运行查询的内置功能。我们可以组合多个内置功能来创建用户定义的功能(或UDFS)UDFS用于将常用的函数组合到可以重复存储和执行的查询中。

>

创建一个帐户

要创建一个Faunadb帐户,您需要前往Fauna website。免费单击“开始”,然后选择GitHub作为您的身份提供者,然后从Github授权Faunadb。如果一切顺利,应该为您成功创建您的帐户,并且您应该重定向到您的Faunadb仪表板。

设置数据库

现在,您已经成功创建了一个帐户,在仪表板上单击创建数据库按钮,接下来我们需要给数据库一个名称,为您的数据选择一个区域,然后选择使用demo-data来确保创建数据库为我们提供一些演示数据。继续单击创建,如果一切按计划进行。

项目设置

在您喜欢的编辑器中打开一个空文件夹,让我们使用FaunadB设置Nodejs应用程序。运行以下命令以旋转一个新的节点项目

$ npm init --y

接下来,我们需要安装一些基本的扩展名来帮助启动服务器,
首先,让我们安装Express以帮助为我们创建基本服务器。

$ npm i express

运行以下命令安装faunadb和dotenv扩展。

$ npm i faunadb dotenv

现在,我们已经成功安装了应用程序依赖项,返回您的Faunadb仪表板,并确保您单击新创建的数据库。单击“安全性”选项卡,然后单击创建新的数据库键。给密钥一个名称,并确保您设置了键为服务器并继续进行的角色。如果一切都按计划进行,则应看到为您生成的API键,请复制此键并将其存储在项目目录中的DotEnv文件中。

运行FQL查询

创建一个名称app.js的新JavaScript文件。我们将编写和导出该模块中与FaunAdb交互的功能。我们需要做的第一件事是创建一个集合,所以让我们为此编写逻辑。

const { query, Client } = require('faunadb');
require('dotenv').config();

const client = new Client({
  secret: process.env.SECRETE_KEY,
});

//Create a collection
const createCollection = async (name) => {
  try {
    const collection = await client.query(
      query.CreateCollection({ name })
    );
    return [null, collection]
  } catch (error) {
    return [error, collection]
  }
}

module.exports = {
  createCollection,
}

我们与Client一起导入了query对象。 query对象包含我们与FAUNADB数据库进行交互所需的方法。然后,我们创建了Client的新实例,我们需要使用之前创建的秘密密钥对客户端进行身份验证。现在,我们有一个客户,让我们继续进行查询。我们有一个函数createCollection。此功能接受一个单个参数name,该参数将用作我们正在创建的集合的名称。有一个try/catch块来结束创建集合并优雅地处理我们错误的过程。这将是我们将创建其他功能的模式。在try块中,我们称之为client.query,这就是您使用FQL进行查询的方式。

此功能接受查询作为其参数。要创建一个集合,我们将query.CreateCollection调用并传递给具有名称属性的对象作为参数。我们存储查询的结果,该查询是变量collection中的新创建的集合。我们返回以NULL为第一个值的数组,将collection作为第二个值返回。这也将是此代码库中的模式,所有辅助功能都必须返回一个数组对象,其中一个值代表错误对象,第二个值是从函数中的返回值。

catch块内部,我们也返回一个数组,匹配从辅助功能返回数组的模式,这一次是在第二个参数为null时抛出的错误对象。然后,我们使用module.export导出createCollection函数。现在,让我们在新文件index.js中创建基本的Express服务器

// index.js

const express = require('express');
const { createCollection } = require('./app');

const app = express();
app.use(express.json());

app.post('/collection', async (req, res) => {
  const {name} = req.body;
  try {
    const [error, collection] = await createCollection(name);
    if (error) throw error
    if (collection) res.status(200).json(collection)
  } catch(error) {
    return res.status(400).json(error)
  }
})

app.listen(3000, () => console.log('App running on PORT 3000'));

在我们导入的index.js文件中,然后我们从app.js导入createCollection函数。我们创建一个新的Express应用程序,并将Express上的JSON解析器用作应用中间件。接下来,我们注册一条路线和一个邮政请求的处理程序。该路线是/collection,在处理程序功能内部,我们从请求正文中破坏了名称,在try块中,我们称之为等待createCollection并以名称传递。我们破坏errorcollection。如果有error,那么如果成功创建了集合,我们只会丢弃错误,我们将其发送回响应。

catch块中,我们刚刚设置了对400的响应状态,然后我们将错误发送回响应,然后在端口3000上侦听连接。如果您使用Nodemon运行此应用;

$ nodemon i index.js

如果一切顺利,您应该在控制台中看到App running on PORT 3000的消息。让我们对端点进行查询以测试它。

POST http://localhost:3000/collection
Content-Type: application/json

{
    "name": "todos"
}

如果您提出此请求,则应该得到看起来像这样的响应;

{
  "ref": {
    "@ref": {
      "id": "todo",
      "collection": {
        "@ref": {
          "id": "collections"
        }
      }
    }
  },
  "ts": 1691589603965000,
  "history_days": 0,
  "name": "todo"
}

现在,我们有一个集合,让我们继续添加一些数据。

// app.js continued

const createTodo = async (note) => {
  try {
    const todo = await client.query(
      query.Create(
        query.Collection('Todos'),
        { data: { status: 'PENDING', note } }
      )
    );
    return [null, todo];
  } catch (error) {
    return [error, null];
  }
}

module.exports = {
  createCollection,
  createTodo
}

上面的摘要处理创建新的todo对象的过程。我们有一个函数createTodo,它接受字符串note作为其参数。在try块中,我们称为client.query并传递查询。要创建一个新文档,您需要调用query.Create,然后将REF传递给您希望该文档所属的集合。 Create方法接受的第二个参数是具有data属性的数据对象。此data属性的值是我们要存储在文档中的数据。我们的数据将具有status属性,该属性将为PENDING和Note属性,该属性将作为Todo的标题。让我们为其设置处理程序。

//index.js continued

const {
  createCollection,
  createTodo,
} = require("/.app.js");


app.post('/todo', async (req, res) => {
  const {note} = req.body;
  try {
    const [error, todo] = await createTodo(note);
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})

我们在应用程序上注册了一个备用后处理程序。该路线是/todo,在处理程序内部,我们会破坏请求主体的注释。在try块中,我们创建一个新的todo,并用响应将其发送回响应,否则如果有错误,我们以400的状态将该错误发送回该错误。现在让我们测试此端点。

POST http://localhost:3000/todo
Content-Type: application/json

{
    "note": "Cleaning"
}

服务器的响应应该看起来像这样;

{
  "ref": {
    "@ref": {
      "id": "372695699207225424",
      "collection": {
        "@ref": {
          "id": "todos",
          "collection": {
            "@ref": {
              "id": "collections"
            }
          }
        }
      }
    }
  },
  "ts": 1691689166285000,
  "data": {
    "status": "PENDING",
    "note": "Cycling"
  }
}

继续创建更多的娱乐活动,现在我们需要设置一个索引。索引是FQL的核心,它们将是我们将如何从FQL检索数据的方式,让我们设置一个索引来检索所有已待处理的Todos。

// app.js continued

// CREATE INDEX
const createIndex = async (
  name,
  collection,
  field
) => {
  try {
    let index;
    if (field) {
      index = await client.query(
        query.CreateIndex({
          name,
          source: query.Collection(collection),
          terms: [{ field }],
        })
      );
    } else {
      index = await client.query(
        query.CreateIndex({
          name,
          source: query.Collection(collection),
        })
      );
    }
    return [null, index];
  } catch (error) {
    console.log(error)
    return [error, null];
  }
}



module.exports = {
  createCollection,
  createTodo,
  createIndex,
}

首先,我们定义createIndex函数。该功能需要三个参数,并返回一个带有两个元素的数组。在尝试块内部,我们声明了一个称为index的变量。该变量将存储创建的索引。然后,我们检查字段参数是否未定义。如果未定义,该函数将在指定字段上创建索引。否则,该功能会在整个集合上创建索引。我们使用client.query()方法来创建索引。 query.CreateIndex()方法创建了一个新索引。名称参数指定索引的名称。源参数指定将创建索引的集合。术语参数指定将创建索引的字段。然后,我们将功能从此模块中导出。让我们为此设置一个处理程序。

// index.js continued

const {
  createIndex
} = require('./app');

app.post('/index', async (req, res) => {
  const {name,collection,field} = req.body;
  try {
    const [error, index] = await createIndex(
      name,
      collection,
      field ?? null 
    );
    if (error) throw error
    if (index) res.status(200).json(index)
  } catch(error) {
    return res.status(400).json(error)
  }
});

我已将处理程序函数附加到/index路由上,该路由将在向该路线提出后请求时执行。路由处理程序功能首先解析请求主体并提取namecollectionfield参数。名称参数指定要创建的索引的名称。集合参数指定将创建索引的集合的名称。字段参数指定将创建索引的字段。如果省略此参数,则将在整个集合上创建索引。

然后该功能调用createIndex函数以创建索引。 createIndex函数返回一个带有两个元素的数组:第一个元素是错误消息,第二个元素是索引。如果CreateIndex函数返回错误消息,则处理程序功能会引发错误。该错误将作为400不良请求响应发送回客户端。如果CreateIndex函数返回索引,则处理程序函数将索引作为200 OK响应发送给客户端。让我们测试这个端点。

POST http://localhost:3000/index
Content-Type: application/json

{
  "name": "todos_by_status",
  "collection": "todos",
  "field": ["data", "status"]
}

响应应该看起来像这样;

{
  "ref": {
    "@ref": {
      "id": "todos_by_status",
      "collection": {
        "@ref": {
          "id": "indexes"
        }
      }
    }
  },
  "ts": 1691691134550000,
  "active": true,
  "serialized": true,
  "name": "todos_by_status",
  "source": {
    "@ref": {
      "id": "todos",
      "collection": {
        "@ref": {
          "id": "collections"
        }
      }
    }
  },
  "partitions": 8
}

现在,让我们设置一个函数以检索所有待处理的戒酒;

// app.js continued

const getTodoByIndex = async (
  index,
  value
) => {
  try {
    const todos = await client.query(
      query.Map(
        query.Paginate(
          query.Match(
            query.Index(index),
            value
          )
        ),
        query.Lambda(x => query.Get(x))
      )
    );
    return [null, todos]
  } catch (error) {
    return [error, null]
  }
}

module.exports = {
  createCollection,
  createTodo,
  createIndex,
  getTodoByIndex,
}

上方的摘要首先定义了一个称为getTodoByIndex的函数。该函数采用两个参数:index参数指定索引的名称,而value参数指定了将索引与匹配的值。然后,该函数使用client.query()方法执行查询。客户端变量是对Faunadb客户端的引用。 query.Map()方法创建了一个新地图。 query.Paginate()方法在查询结果。

query.Match()方法与匹配指定索引和值匹配的集合中的文档匹配。 query.Lambda()方法创建了lambda函数。 lambda函数将针对查询返回的每个文档执行。 query.Get()方法从数据库获取文档。该函数返回一个带有两个元素的数组:第一个元素是错误消息,第二个元素是查询返回的文档数组。让我们设置一个处理程序功能以检索待处理的待办事项。

// index.js continued
const {
  getTodoByIndex
} = require("./app");

app.get('/todos/index',  async (req, res) => {
  const {index, value} = req.body
  try {
    const [error, todo] = await getTodoByIndex(
      index,
      value
    );
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})

我们已将路由处理程序功能附加到/todos/index路由上,该路由将在向该路线提出Get请求时执行。功能首先解析请求主体并提取indexvalue参数。 index参数指定索引的名称,而value参数指定索引与匹配的值。然后,该函数调用getTodoByIndex函数以检索戒酒。
如果GetTodobyIndex函数返回错误消息,则处理程序功能会引发错误。该错误将作为400不良请求响应发送回客户端。如果getTodobyIndex函数返回文档数组,则处理程序功能将文档的数组发送回客户端,作为200 OK响应。让我们测试这个端点。

GET http://localhost:3000/todos/index
Content-Type: application/json

{
  "index": "todos_by_status",
  "value": "PENDING"
}

我们应该恢复一系列的招待,让我们创建一个处理程序功能以检索单个todo;

// app.js continued

const getTodo = async (id) => {
  try {
    const todo = await client.query(query.Get(
      query.Ref(
        query.Collection("todos"),
        id
      )
    ));
    return [null, todo];
  } catch (error) {
    return [error, null];
  }
}


module.exports = {
  createCollection,
  createTodo,
  createIndex,
  getTodoByIndex,
  getTodo,
}

我们定义了一个称为getTodo的函数,该函数从Todos Collection中获取todo文档。该函数采用一个参数:指定要获取文档的ID的id参数。该函数首先使用client.query()方法执行查询。 query.Get()方法从数据库获取文档。 query.Ref()方法创建了对文档的引用。

query.Collection()方法指定文档所在的集合。id参数指定文档的ID。该函数返回一个带有两个元素的数组:第一个元素是错误消息,第二个元素是查询返回的文档。让我们设置一个处理程序功能以检索todo

// index.js continued
const {getTodo} = require('./app');

app.get('/todo/:id', async (req, res) => {
  const {id} = req.params;
  try {
    const [error, todo] = await getTodo(id);
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})

我们有一个路由处理程序功能,该功能从ID的Todos Collection中获取todo文档。该函数已连接到/todo/:id路由,因此在向该路线提出GET请求时将执行它。功能首先解析请求参数并提取ID参数。 id参数指定要获取的文档的ID。然后,该功能调用getTodo函数以获取todo。如果GetToDo函数返回错误消息,则处理程序功能会引发错误。该错误将作为400不良请求响应发送回客户端。如果GetTodo函数返回文档,则处理程序功能将文档发送给客户端以200个OK响应。最后,让我们设置一个辅助功能以更新todo。

// * UPDATE TODO
const updateTodo = async (id, status) => {
  try {
    const todo = await client.query(
      query.Update(
        query.Ref(query.Collection("Todos"), id),
        { data: { status: status } },
      )
    )
    return [null, todo];
  } catch (error) {
    return [error, null]
  }
}

module.exports = {
  createCollection,
  createTodo,
  createIndex,
  getTodo,
  getTodoByIndex,
  updateTodo
}

我创建了一个名为updateTodo的函数,该函数在Todos Collection中更新了TODO文档。该函数采用两个参数:id参数指定要更新文档的ID,并且状态参数指定文档的新状态。 query.Update()方法更新文档。 query.Ref()方法创建了对文档的引用。 query.Collection()方法指定了该文档所在的集合。id参数指定文档的ID。数据对象指定将在文档中更新的数据。在这种情况下,正在更新状态属性。该函数返回一个带有两个元素的数组:第一个元素是错误消息,第二个元素是已更新的文档。让我们为此设置一个路由处理程序函数;

// index.js continued

const {updateTodos} = require('./app');

app.patch('/todo/:id',  async (req, res) => {
  const {id} = req.params;
  const {status} = req.body;
  try {
    const [error, todo] = await updateTodo(id, status);
    if (error) throw error
    if (todo) res.status(200).json(todo)
  } catch(error) {
    return res.status(400).json(error)
  }
})

我们有一个创建的路由处理程序功能,可更新Todos集合中的TODO文档的状态。该功能连接到/todo/:id路由,因此在对该路由的补丁请求时将执行。功能首先解析请求参数并提取ID和状态参数。 id参数指定要更新文档的ID,并且状态参数指定文档的新状态。然后,该功能调用updateTodo函数以更新todo。

如果updateTodo函数返回错误消息,则处理程序功能会引发错误。该错误将作为400不良请求响应发送回客户端。如果updateTodo函数返回文档,则处理程序函数将文档发送回客户端以200 ok响应。

我将在这里停下来,让您弄清楚如何检索集合中的所有文档以及如何从集合中删除文档。由于时间因素,FaunadB具有GraphQL API,我们跳过了很多东西,并且可以自动创建一个GraphQl端点以与我们的数据进行交互。我们不仅限于在后端使用FaunAdb,还可以在React或Svelte Project等前端应用中使用它。我们只是在FQL中几乎没有触摸Lambda功能,我们只是刷了FQL的表面,这些概念中的每一个都需要单独的帖子。在下面的评论部分中留下您的想法,有关Faunadb,FQL和我们应该介绍的下一个概念。