Все, что вам нужно знать о EdgeDB

Сегодня в области баз данных преобладают нереляционные, бессхемные модели данных. Нереляционные базы данных более удобны для разработчиков и лучше масштабируются, чем реляционные базы данных прошлого. Однако им сложнее выполнять сложные задачи.

Теперь у нас есть новый игрок в игре для решения этой проблемы: EdgeDB. EdgeDB построена на базе PostgreSQL и представляет новую концептуальную модель для представления данных.

Но прежде чем мы погрузимся в изучение того, что такое EdgeDB, как она сопоставляется с SQL и ORM, и как создать приложение Node.js с EdgeDB, давайте вкратце рассмотрим реляционные базы данных.

Что такое реляционная база данных?

Реляционная база данных зародилась в 1970-х годах, когда IBM и Oracle сделали первые шаги в направлении концепции уровней баз данных в приложениях. IBM приняла язык структурированных запросов, и позже он стал стандартом де-факто для реляционных баз данных.

Несмотря на то, что реляционные базы данных и SQL были стандартными системами баз данных, они получили много критики. SQL обвиняли в том, что он:

  • Большой язык
  • Трудно сочиняемый
  • Непоследовательность в синтаксисе и семантике
  • Сложно интегрировать с прикладным языком.

EdgeDB устраняет некоторые из этих проблем.

Что такое EdgeDB?

EdgeDB — это первая граф-реляционная база данных с открытым исходным кодом, разработанная как преемник SQL и реляционной парадигмы.

EdgeDB использует графо-реляционную модель, в которой данные описываются и хранятся как сильно типизированные объекты, а отношения связывают объекты.

Она использует PostgreSQL под капотом, наследуя все возможности реляционной базы данных. EdgeDB хранит и запрашивает данные с использованием методов реляционной базы данных и требует строгого проектирования схемы.

Что такое графо-реляционная модель?

Графо-реляционная модель построена поверх реляционной модели с некоторыми дополнительными возможностями. Эта новая модель помогает EdgeDB преодолеть концептуальные и технические трудности, часто возникающие при использовании реляционной базы данных в приложении (несоответствие объектно-реляционного импеданса). EdgeDB также имеет прочную основу и производительность реляционной базы данных.

Давайте рассмотрим некоторую терминологию, чтобы лучше понять граф-реляционную модель.

Реляционная модель Графо-реляционная модель
Таблица Тип объекта
Столбец Свойство/ссылка
Строка Объект

Графо-реляционные базы данных расширяют возможности объектно-реляционной базы данных тремя основными способами:

  • Уникальная идентификация объектов

Все объекты данных являются глобально уникальными, неизменяемыми идентификаторами. Поэтому вам не нужно специально добавлять Ids в свои схемы. EdgeDB имеет ограничение, которое добавляет уникальный идентификатор (UUID) при вставке.

  • Ссылки на объекты

В реляционной модели атрибут имеет определенный набор примитивных типов данных, и связи строятся через эти атрибуты с ограничениями внешнего ключа. Но в граф-реляционной модели объекты имеют примитивные типы данных и прямые ссылки на другие объекты (ссылки). Поэтому вам не нужно возиться с первичными ключами, внешними ключами и объединениями таблиц. Объекты уникальны, а связи представляют их отношения.

  • Кардинальность

Кардинальность определяет количество значений, присваиваемых атрибуту.

В традиционной реляционной модели атрибут имеет только имя и тип данных, но граф-реляционная модель включает в себя третий компонент, называемый кардинальностью. Кардинальность имеет пять различных перечислений: Empty, One, AtMostOne, AtLeastOne, и Many.

Что призвана решить EdgeDB?

Цель EdgeDB — решить сложные проблемы проектирования реляционных моделей. EdgeDB лучше справляется с современными задачами SQL, такими как подзапросы, расширенная агрегация и оконные функции, соблюдая при этом свойства ACID, производительность и надежность.

Особенности EdgeDB

Давайте рассмотрим некоторые особенности EdgeDB, чтобы понять, почему она выделяется:

  • Декларативная схема позволяет выразить наследование, вычисляемые свойства, функции, сложные ограничения и контроль доступа.
  • Система миграции, которая автоматически обнаруживает изменения и сравнивает различия в схемах.
  • Богатая типизированная система с встроенным конструктором запросов на JavaScript/TypeScript.
  • Язык запросов под названием EdgeQL.
  • Поддержка нескольких языков, таких как Python, JavaScript/TypeScript/Deno и Go.
  • Предоставляет инструмент CLI помимо REPL, позволяющий пользователям устанавливать, создавать, обрабатывать миграции и управлять базами данных локально (а вскоре и в облаке).

EdgeDB против SQL и ORM

Как язык структурированных запросов (SQL), так и объектно-реляционное отображение (ORM) имеют свои сильные и слабые стороны. Давайте посмотрим, как EdgeDB противостоит им в некоторых ключевых аспектах:

  • Представление схемы

EdgeDB имеет декларативный язык схем для представления схем. Он использует файлы .esdl для определения схемы, что намного проще в управлении по сравнению с DDL, используемым в SQL.

  • Миграции

В EdgeDB миграции (файлы .edgeql) создаются с помощью CLI. EdgeDB имеет встроенную систему, которая сравнивает изменения схемы с текущей базой данных. Поэтому управлять миграциями намного проще.

  • Синтаксис запросов

EdgeDB создана для решения некоторых из самых неинтуитивных аспектов дизайна SQL, таких как устранение объединений. EdgeQL обладает лучшей композитивностью или способностью писать вложенные утверждения с меньшей кривой обучения.

  • Структура результатов

Структура результатов традиционного SQL-запроса представляет собой список кортежей со скалярными значениями. Чтобы использовать данные в приложении, их нужно преобразовать в объекты, что требует дополнительных шагов в логике приложения. И ORM, и EdgeQL возвращают структурированные объекты в качестве результатов выполнения запросов.

  • Интеграция языков

В EdgeQL вы можете писать запросы, используя обычные строки. Встроенный конструктор запросов позволяет писать запросы EdgeQL с подсветкой синтаксиса, автодополнением и автоформатированием.

  • Производительность

С EdgeDB ваш EdgeQL компилируется с оптимизированными запросами PostgreSQL. Запросы выполняются за один заход.

EdgeQL определяет запросы с большим количеством JOIN и преобразует их в набор подзапросов, а затем агрегирует результаты. Производительность EdgeQL по сравнению с SQL и ORM также намного выше.

  • Мощность

Определения схем EdgeDB и язык EdgeQL связаны между собой, поэтому ваши типы схем могут иметь вычисляемые поля, индексы и ограничения, напоминающие сложные выражения EdgeQL. Это делает EdgeDB мощным решением.

Архитектура EdgeDB

EdgeDB состоит из трехслойной архитектуры: клиент, сервер и сервер PostgreSQL.

Между клиентом и сервером EdgeDB находится уровень двоичного протокола EdgeDB, который наследует некоторые свойства двоичного протокола Postgres.

Он сериализует данные EdgeQL перед передачей на сервер EdgeDB. Затем сериализованные данные EdgeQL будут разобраны, скомпилированы в SQL и выполнены на сервере PostgreSQL.

Сервер EdgeDB имеет кэш в памяти, который кэширует скомпилированные запросы и подготовленные операторы и снижает нагрузку на базу данных при выполнении этих запросов. Он использует родной двоичный протокол Postgres, который позволяет серверу EdgeDB взаимодействовать с сервером PostgreSQL.

Источник оригинального изображения: https://i.imgur.com/5DQjd7U.png

Ядро и сервер EdgeDB написаны на языке Python с некоторыми расширениями Rust для ускорения выполнения.

Практический проект: Создание приложения на Node.js с EdgeDB

Давайте запачкаем руки, создав приложение с EdgeDB. Для этой демонстрации мы создадим небольшой REST API для покемонов.

Сначала установите EdgeDB и инициализируйте проект REST API.

Установка EdgeDB

EdgeDB поддерживает три основные платформы (Windows, Mac и Linux).

В этом примере мы будем использовать Windows. Выполните следующую команду в терминале PowerShell:

$ iwr https://ps1.edgedb.com -useb | iex
Войти в полноэкранный режим Выйти из полноэкранного режима

Для macOS и Linux используйте:

$ curl https://sh.edgedb.com --proto '=https' -sSf1 | sh
Войти в полноэкранный режим Выйти из полноэкранного режима

Инициализация проекта Node.js

Теперь давайте создадим каталог и инициализируем в нем проект Node.

$ mkdir edge-pokemon
$ cd edge-pokemon
$ npm init -y
Войти в полноэкранный режим Выйти из полноэкранного режима

Установите зависимости. Поскольку мы создаем REST API с помощью Node, мы будем использовать фреймворк Express.

$ npm install express edgedb dotenv cors
$ npm install typescript concurrently nodemon @types/cors @types/express @types/node --save-dev
Войти в полноэкранный режим Выход из полноэкранного режима

Поскольку мы используем TypeScript, давайте определим конфигурационный файл TypeScript tsconfig.json. Создайте его с помощью следующей команды:

$ npx tsc --init
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь добавим атрибут "outDir": "./dist" в файл tsconfig.json (где ./dist — это каталог, в котором находится скомпилированный код).

Инициализируйте экземпляр EdgeDB.

$ edgedb project init
Войдите в полноэкранный режим Выйти из полноэкранного режима

Приведенная выше команда создаст файл edgedb.toml и каталог dbschema, в котором хранится схема, миграции и конфигурации для ваших экземпляров EdgeDB.

Добавьте схему в ваше приложение Node.js

Теперь давайте создадим нашу схему. Перейдите к файлу схемы по умолчанию dbschema/default.esdl.

module default {

    scalar type Result extending enum<Won, Lost, Tie>;

    type Pokemon {
        required property name -> str;
        required property description -> str;
        property height -> int64;
        property weight -> int64;
    }

    type Battle {
        property result -> Result;
        required link contender -> Pokemon;
        required link opponent -> Pokemon;
    }
}
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Обратите внимание, что мы не добавляем сюда поле id, первичные или внешние ключи. Вместо этого мы построили отношения между Pokémon и Battle через ссылку. Каждый объект Battle будет иметь связь или отношение к покемону через свойства contender и opponent.

Теперь мы создадим файл миграции на основе нашей схемы.

$ edgedb migration create
Вход в полноэкранный режим Выйдите из полноэкранного режима

В результате будет создан файл миграции dbschema/migrations/<migration_number>.esdl, состоящий из запроса EdgeQL с некоторыми командами DDL, такими как CREATE TYPE, CREATE PROPERTY, CREATE LINK. Запустите миграцию с помощью следующей команды.

$ edgedb migrate
Войти в полноэкранный режим Выйдите из полноэкранного режима

Будут сгенерированы два объекта — Pokémon и Battle. Вы можете выполнить команду edgedb list types, чтобы подтвердить это.

Теперь мы можем начать кодировать сервер нашего приложения. Но сначала давайте воспользуемся конструктором запросов в нашем проекте, чтобы с помощью TypeScript написать полностью типизированные запросы EdgeQL.

$ npx edgeql-js
Вход в полноэкранный режим Выход из полноэкранного режима

На основе нашей схемы будут сгенерированы некоторые типы и привязки JavaScript/TypeScript для нашего экземпляра EdgeDB в каталоге dbschema/edgeql-js/.

Создайте сервер Express, создав файл index.ts в корне проекта.

import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";
dotenv.config();
import cors from "cors";

const app: Express = express();
const port = process.env.APP_PORT || 3000;

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.listen(port, () => {
  console.log(`[server]: Server is running at https://localhost:${port}`);
});
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Определите конечные точки и напишите запросы с edgeql-js внутри них. Начнем с конечных точек /pokemon и /pokemons.

import * as edgedb from "edgedb";
import e from "./dbschema/edgeql-js";
const client = edgedb.createClient(); // initialize the EdgeDB connection

app.post("/pokemon", async (req: Request, res: Response) => {
  try {
    const query = e.insert(e.Pokemon, {
      name: req.body.name,
      description: req.body.description,
      height: req.body.height,
      weight: req.body.weight,
    });
    const result = await query.run(client);
    res.status(200).send(result);
  } catch (error) {
    console.error(error);
    res.status(500).send(error);
  }
});
Вход в полноэкранный режим Выход из полноэкранного режима

В приведенной выше конечной точке вы заметите, что мы создали объект запроса через edgeql-js, передав некоторые параметры из объекта запроса.

Когда вы выполните приведенный выше запрос, данные сохранятся под типом объекта Pokémon.

app.get("/pokemons", async (_req: Request, res: Response) => {
  try {
    const query = e.select(e.Pokemon, (pokemon: any) => ({
      id: true,
      name: true,
      description: true,
      height: true,
      weight: true,
    }));
    const result = await query.run(client);
    res.status(200).send(result);
  } catch (error) {
    console.error(error);
    res.status(500).send(error);
  }
});
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы написали запрос и выбрали некоторые атрибуты или свойства. Вы можете передавать атрибуты или свойства вместе с булевыми значениями, чтобы заполнить их.

Теперь перейдем к специальным конечным точкам /battle и /battles, которые работают со связями (отношениями с объектами покемонов).

app.post("/battle", async (req: Request, res: Response) => {
  try {
    const query = e.insert(e.Battle, {
      contender: e.select(e.Pokemon, (pokemon) => ({
        filter: e.op(pokemon.id, "=", e.uuid(req.body.contender_id)),
      })),
      opponent: e.select(e.Pokemon, (pokemon) => ({
        filter: e.op(pokemon.id, "=", e.uuid(req.body.opponent_id)),
      })),
      result: req.body.result,
    });
    const result = await query.run(client);
    res.status(200).send(result);
  } catch (error) {
    console.error(error);
    res.status(500).send(error);
  }
});
Вход в полноэкранный режим Выход из полноэкранного режима

У нас есть несколько вложенных запросов, написанных для атрибутов contender и opponent, которые получают объект Pokémon. Эти объекты покемонов используются для создания отношений или связей между покемонами и типами объектов Battle.

app.get("/battles", async (_req: Request, res: Response) => {
  try {
    const query = e.select(e.Battle, (battle: any) => ({
      id: true,
      contender: { name: true },
      opponent: { name: true },
      result: true,
    }));
    const result = await query.run(client);
    res.status(200).send(result);
  } catch (error) {
    console.error(error);
    res.status(500).send(error);
  }
});
Вход в полноэкранный режим Выход из полноэкранного режима

Мы используем запрос select в приведенной выше конечной точке для получения и заполнения данных связей (отношений). Обратите внимание, что мы передаем значения name: true для атрибутов contender и opponent, что позволяет получить название покемона, связанного с объектами battle. Таким образом, с помощью edgeql-js можно писать безопасные для типов запросы.

Теперь мы можем выполнять эти запросы через наше приложение Express. Но сначала давайте добавим несколько скриптов в раздел scripts нашего файла package.json.

"scripts": {
    "build": "npx tsc",
    "start": "node dist/index.js",
    "dev": "concurrently "npx tsc --watch" "nodemon -q dist/index.js""
},
Вход в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что в скрипте dev есть некоторые специальные ключевые слова (инструменты), такие как concurrently и nodemon. Эти инструменты могут пригодиться на этапе разработки. Они позволяют нам выполнять несколько команд одновременно и автоматически перезапускать наше приложение при обнаружении изменения файла в нашем проекте.

Сценарий build скомпилирует наш TypeScript-код в ES6 (на основе атрибута target в compilerOptions в файле tsconfig.json). Команда start запускает скомпилированную версию приложения Express.

Давайте запустим сервер разработки, выполнив следующий скрипт на терминале из корневого каталога проекта.

$ npm run dev
Вход в полноэкранный режим Выйти из полноэкранного режима

Это запустит проект Express на http://localhost:3000. Протестируйте это приложение с помощью Postman, инструмента, позволяющего тестировать конечные точки API.

Примечание: При первом запуске проекта вы можете столкнуться с ошибкой MODULE_NOT_FOUND (Cannot find module '/path/to/project/edge-pokemon/index.js'). Это происходит потому, что папка сборки или ./dist еще не создана. Вы можете избежать этого, запустив build перед start, или запустив start еще раз.

Сначала мы протестируем /pokemon, который создаст или сохранит покемона. Это конечная точка POST, поэтому нам нужно отправить данные тела в x-www-form-urlencoded форме. Теперь добавьте параметры name, description, height и weight.

При тестировании этой конечной точки вы заметите, что в качестве ответа возвращается уникальный id объекта покемона. Это стандартное поведение API EdgeDB insert.

Далее, давайте протестируем /pokemons, который вернет всех созданных покемонов. Это конечная точка GET, поэтому для получения данных необходимо отправить запрос GET. Для этой конечной точки не нужно передавать никаких параметров.

В качестве ответа эта конечная точка отправит массив данных о покемонах.

Протестируйте конечную точку /battle, где вам нужно будет сделать POST-запрос для создания битвы. Для этого передайте параметры contender_id (id покемона), opponent_id (id покемона) и result (только одно из строковых значений Won, Lost, Tie).

Эта конечная точка также возвращает id — уникальный идентификатор объекта битвы.

Наконец, получите некоторые битвы, сделав GET-запрос к конечной точке /battles.

Эта конечная точка отправит в ответ массив данных о битвах покемонов.

Вы можете найти полный код для этого в моей репозитории на GitHub. Не стесняйтесь клонировать репозиторий, поиграть с демонстрационным проектом и посмотреть, как работает EdgeDB.

Подведение итогов и следующие шаги

В этом посте мы создали приложение для Node.js с использованием EdgeDB. Мы изучили крутые возможности EdgeDB — ее богатую типовую систему, многофункциональный CLI и хороший инструмент миграции. Мы увидели, как EdgeDB поддерживает основные языки программирования и обеспечивает высокую производительность.

Недавно была выпущена версия 1.0 EdgeDB, и дорожная карта на пути к версии 2.0 выглядит многообещающе. Вы можете узнать больше из замечательной документации по EdgeDB. Существует также активное и заинтересованное сообщество EdgeDB на Discord.

Счастливого кодинга!

P.S. Если вам понравился этот пост, подпишитесь на наш список JavaScript Sorcery для ежемесячного глубокого погружения в более волшебные советы и трюки JavaScript.

P.P.S. Если вам нужен APM для вашего Node.js приложения, обратите внимание на AppSignal APM для Node.js.

Оцените статью
devanswers.ru
Добавить комментарий