Сегодня в области баз данных преобладают нереляционные, бессхемные модели данных. Нереляционные базы данных более удобны для разработчиков и лучше масштабируются, чем реляционные базы данных прошлого. Однако им сложнее выполнять сложные задачи.
Теперь у нас есть новый игрок в игре для решения этой проблемы: EdgeDB. EdgeDB построена на базе PostgreSQL и представляет новую концептуальную модель для представления данных.
Но прежде чем мы погрузимся в изучение того, что такое EdgeDB, как она сопоставляется с SQL и ORM, и как создать приложение Node.js с EdgeDB, давайте вкратце рассмотрим реляционные базы данных.
- Что такое реляционная база данных?
- Что такое EdgeDB?
- Что такое графо-реляционная модель?
- Что призвана решить EdgeDB?
- Особенности EdgeDB
- EdgeDB против SQL и ORM
- Архитектура EdgeDB
- Практический проект: Создание приложения на Node.js с EdgeDB
- Установка EdgeDB
- Инициализация проекта Node.js
- Добавьте схему в ваше приложение Node.js
- Подведение итогов и следующие шаги
Что такое реляционная база данных?
Реляционная база данных зародилась в 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.