Вступление
H2 - это легкий сервер баз данных, написанный на Java. Он может быть встроен в приложения Java или работать как отдельный сервер.
В этом руководстве мы рассмотрим, почему H2 может быть хорошим вариантом для ваших проектов. Мы также узнаем, как интегрировать H2 с Node.js, создав простой Express API.
Особенности H2
H2 был построен с учетом производительности.
« H2 - это сочетание: быстрого, стабильного, простого в использовании и функционального».
Хотя H2 известен в основном потому, что он может быть встроен в приложения Java, он имеет некоторые интересные особенности, которые также применимы к его серверной версии. Давайте посмотрим на некоторые из них дальше.
Размер и производительность
Размер файла .jar, используемого для серверной версии, составляет около 2 МБ. Мы можем скачать его с сайта H2 в комплекте с дополнительными скриптами и документацией. Однако, если мы будем искать в Maven Central, мы сможем загрузить файл .jar самостоятельно.
Производительность H2 проявляется во встроенной версии. Тем не менее, официальный тест показывает, что его версия клиент-сервер также впечатляет.
Базы данных в памяти и шифрование
Базы данных в памяти не являются постоянными. Все данные хранятся в памяти, поэтому скорость значительно увеличивается.
На сайте H2 объясняется, что базы данных In-Memory особенно полезны при создании прототипов или при использовании баз данных только для чтения.
Шифрование - еще одна полезная функция для защиты данных в состоянии покоя. Базы данных можно зашифровать с помощью алгоритма AES-128.
Другие полезные функции
H2 также предоставляет кластерный режим, возможность запускать несколько серверов и соединять их вместе. Запись выполняется на всех серверах одновременно, а чтение выполняется с первого сервера в кластере.
H2 удивляет своей простотой. Он предоставляет несколько полезных функций и прост в настройке.
Давайте запустим сервер H2 в рамках подготовки к следующим разделам:
$ java -cp ./h2-1.4.200.jar org.h2.tools.Server -tcp -tcpAllowOthers -tcpPort 5234 -baseDir ./ -ifNotExists
Аргументы, начинающиеся с tcp
разрешают связь с сервером.
ifNotExists
позволяет создать базу данных при первом доступе к ней.
Описание API и общая схема
Предположим, мы пишем API для регистрации всех найденных на сегодняшний день экзопланет. Экзопланеты - это планеты, находящиеся за пределами нашей Солнечной системы, вращающиеся вокруг других звезд.
Это наше простое определение API , CRUD для одного ресурса:
{.ezlazyload}
Это определение вместе с остальной частью кода, который мы увидим далее, доступно в этом репозитории GitHub .
Вот как наше приложение будет выглядеть в конце этого урока:
{.ezlazyload}
Слева от схемы мы видим API Client. Этим клиентом может быть функция «Попробовать» редактора Swagger или любой другой клиент, например Postman или cURL.
На другом конце мы находим сервер базы данных H2 , работающий на
TCP-порту 5234
как описано выше.
Наконец, наше приложение посередине состоит из двух файлов. У первого будет приложение Express , которое будет отвечать на все запросы REST API. Все конечные точки, которые мы описали в определении выше, будут добавлены в этот файл.
Второй файл будет иметь постоянство, функции для доступа к базе данных для выполнения операций CRUD с использованием пакета JDBC.
Схема базы данных
Чтобы сохранить ресурс Exoplanet в базе данных H2, мы должны сначала написать основные функции CRUD. Начнем с создания базы данных.
Мы используем пакет JDBC для доступа к базам данных через JDBC:
var JDBC = require('jdbc');
var jinst = require('jdbc/lib/jinst');
if (!jinst.isJvmCreated()) {
jinst.addOption("-Xrs");
jinst.setupClasspath(['../h2-1.4.200.jar']);
}
var h2 = new JDBC({
url: 'jdbc:h2:tcp://localhost:5234/exoplanets;database_to_lower=true',
drivername: 'org.h2.Driver',
properties: {
user : 'SA',
password: ''
}
});
var h2Init = false;
function getH2(callback) {
if (!h2Init)
h2.initialize((err) => {
h2Init = true;
callback(err)
});
return callback(null);
};
function queryDB(sql, callback) {
h2.reserve((err, connobj) => {
connobj.conn.createStatement((err, statement) => {
if(callback) {
statement.executeQuery(sql, (err, result) => h2.release(connobj, (err) => callback(result)));
} else {
statement.executeUpdate(sql, (err) => h2.release(connobj, (err) => { if(err) console.log(err) }));
}
});
});
};
module.exports = {
initialize: function(callback) {
getH2((err) => {
queryDB("CREATE TABLE IF NOT EXISTS exoplanets ("
+ " id INT PRIMARY KEY AUTO_INCREMENT,"
+ " name VARCHAR NOT NULL,"
+ " year_discovered SIGNED,"
+ " light_years FLOAT,"
+ " mass FLOAT,"
+ " link VARCHAR)"
);
});
},
Функция initialize()
достаточно проста благодаря заранее написанным
вспомогательным функциям. Он создает таблицу экзопланет, если она еще не
существует. Эта функция должна быть выполнена до того, как наш API
начнет получать запросы. Позже мы увидим, где это можно сделать с
помощью Express.
Объект h2
настраивается со строкой подключения и учетными данными для
доступа к серверу базы данных. Для этого примера это проще, но есть
возможности для улучшения безопасности. Мы могли бы сохранить наши
учетные данные в другом месте, например, в переменных среды.
Также нам нужно было добавить путь к jar-файлу H2
jinst.setupClasspath()
. Это связано с тем, что пакету JDBC
требуется драйвер для подключения к H2 , org.h2.Driver
.
Строка подключения JDBC заканчивается на
/exoplanets;database_to_lower=true
. Это означает, что при первом
подключении будет создана exoplanets
Также имена таблиц и столбцов
будут сохранены в нижнем регистре. Это упростит API, поэтому
преобразование имен свойств не потребуется.
Функция queryDB()
использует JDBC
для доступа к базе данных.
Во-первых, ему необходимо reserve()
соединение с базой данных.
Следующие шаги - createStatement()
и затем executeQuery()
если
ожидается результат, или executeUpdate()
противном случае. Соединение
всегда разрывается.
Все вышеперечисленные функции могут возвращать ошибку. Чтобы упростить этот пример, все ошибки не отмечены, но в реальном проекте мы должны их проверить.
Функция getH2()
возвращает объект, представляющий базу данных. Он
создаст этот объект только один раз, используя тот же механизм, что и
классы Singleton, чтобы всегда возвращать только один экземпляр.
Теперь давайте проверим данные пользователей и позволим им выполнять операции CRUD.
Функции базы данных CRUD
Давайте сделаем необходимые функции, чтобы это приложение могло
выполнять операции CRUD на экзопланетах. Мы добавим их в
module.exports
чтобы мы могли легко ссылаться на них из других файлов,
и создадим persistence.js
который мы сможем использовать:
module.exports = {
getAll: function(callback) {
getH2((err) => queryDB("SELECT * FROM exoplanets", (result) => {
result.toObjArray((err, results) => callback(results))
}));
},
get: function(id, callback) {
getH2((err) => queryDB(`SELECT * FROM exoplanets WHERE id = ${id}`, (result) => {
result.toObjArray((err, results) => {
return (results.length > 0) ? callback(results[0]) : callback(null);
})
}));
},
create: function(exoplanet) {
getH2((err) => {
columns = Object.keys(exoplanet).join();
Object.keys(exoplanet).forEach((key) => exoplanet[key] = `'${exoplanet[key]}'`);
values = Object.values(exoplanet).join();
queryDB(`INSERT INTO exoplanets (${columns}) VALUES(${values})`);
});
},
update: function(id, exoplanet) {
getH2((err) => {
keyValues = []
Object.keys(exoplanet).forEach((key) => keyValues.push(`${key} = '${exoplanet[key]}'`));
queryDB(`UPDATE exoplanets SET ${keyValues.join()} WHERE id = ${id}`);
});
},
delete: function(id) {
getH2((err) => queryDB(`DELETE FROM exoplanets WHERE id = ${id}`));
},
};
Обе функции get()
и getAll()
запрашивают базу данных, чтобы вернуть
одну или несколько экзопланет. API вернет их напрямую клиенту API.
Все функции в основном являются SQL-запросами, но create()
и
update()
заслуживают более подробного объяснения.
Оператор INSERT
SQL может получать разделенные столбцы и значения в
форме INSERT INTO table (column1Name) VALUES ('column1Value')
. Мы
можем использовать метод join()
для создания одной строки столбцов,
разделенных запятыми, и сделать что-то подобное, чтобы объединить все
значения, которые мы хотим, в функции create()
.
Оператор UPDATE
SQL немного сложнее. Его форма -
UPDATE table SET column1Name = 'column1Value'
. Поэтому нам нужно
создать новый массив в функции update()
чтобы сохранить значения в
этом формате и затем join()
.
Давайте сохраним все функции базы данных в отдельном файле
persistence.js
, чтобы мы могли добавить некоторый контекст при вызове
функций в файле API, например:
const persistence = require('./persistence');
persistence.getAll();
Схема дзёи
Как правило, мы всегда должны проверять то, что пользователь отправляет, перед его использованием, например, когда пользователь пытается создать ресурс.
Некоторые пакеты упрощают эту задачу. Мы будем использовать Joi для выполнения проверки.
Во-первых, нам нужно определить схему нашего ресурса, определение
свойств и их типов. Это напоминает нам об операторе SQL CREATE
мы
определили ранее:
const Joi = require('joi');
const exoplanetSchema = Joi.object({
id: Joi.number(),
name: Joi.string().required(),
year_discovered: Joi.number(),
light_years: Joi.number(),
mass: Joi.number(),
link: Joi.string().uri()
})
options({ stripUnknown: true });
Каждый тип требует некоторой проверки. Например, link
должно иметь вид
URI , а name
required()
.
Позже мы можем проверить ресурс с помощью метода
exoplanetSchema.validate(theObject)
. Этот метод вернет объект со
error
с ошибками проверки, если таковые были, и value
с обработанным
объектом. Мы будем использовать эту проверку при создании и обновлении
объекта.
Чтобы повысить надежность нашего API, было бы неплохо игнорировать и
отбрасывать любое дополнительное свойство, не включенное в нашу схему.
Это достигается в приведенном выше определении путем установки для
параметра stripUnknown
true
.
REST API с Express
Мы будем использовать пакет Express для создания нашего REST API. И, как мы только что видели, мы также будем использовать Joi для проверки ресурсов.
Настроим обычный экспресс-сервер:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
Переменная приложения - это наш API, пока он пуст. Express позволяет расширять свою функциональность за счет использования промежуточного программного обеспечения, функций, которые могут изменять запросы и ответы нашего API. В этом случае мы используем два промежуточного программного обеспечения.
Во-первых, cors()
позволит другим приложениям браузера вызывать наш
API. Это включает в себя редактор Swagger Editor,
который
мы можем использовать для тестирования нашего API позже. Если вы хотите
узнать больше об обработке CORS с помощью Node.js и
Express , мы вам поможем.
Во-вторых, мы добавляем express.json()
чтобы включить синтаксический
анализ объектов JSON в теле запросов.
Давайте теперь добавим несколько конечных точек в API. Мы начнем с
post()
и put()
, поскольку они используют проверку Joi, описанную
в последнем разделе:
app.post('/exoplanets', (req, res) => {
delete req.body.id;
const { error, value } = exoplanetSchema.validate(req.body);
if(error)
res.status(405).send(error.details[0].message);
persistence.create(value);
res.status(201);
});
app.put('/exoplanets/:id', (req, res) => {
delete req.body.id;
const { error, value } = exoplanetSchema.validate(req.body);
if(error) {
res.status(405).send(error.details[0].message);
}
persistence.get(req.params.id, (result) => {
if(result) {
persistence.update(req.params.id, value);
res.status(201);
} else {
res.status(404);
}
});
});
Express поддерживает одну функцию для каждого HTTP-глагола, поэтому в
данном случае у нас есть post()
и put()
как две функции.
В обеих функциях ресурс сначала проверяется, а любая error
возвращается клиенту API. Чтобы упростить этот код, в этом случае
возвращается только первая ошибка проверки.
put()
также проверяет, существует ли ресурс, пытаясь получить его из
базы данных. Он обновит ресурс, только если он существует.
С post()
и put()
которые требуют проверки, давайте обработаем
get()
когда пользователи захотят взглянуть на экзопланеты, а также
функцию delete()
используемую для удаления экзопланеты. из базы
данных:
app.get('/exoplanets', (req, res) => persistence.getAll((result) => res.send(result)));
app.get('/exoplanets/:id', (req, res) => {
persistence.get(req.params.id, (result) => {
if(result)
res.send(result);
else
res.status(404);
});
});
app.delete('/exoplanets/:id', (req, res) => {
persistence.get(req.params.id, (result) => {
if(result) {
persistence.delete(req.params.id);
res;
} else {
res.status(404);
}
});
});
Определив все конечные точки, давайте настроим порт, на котором приложение будет прослушивать запросы:
app.listen(5000, () => {
persistence.initialize();
console.log("Exoplanets API listening at http://localhost:5000")
});
Вышеупомянутый обратный вызов будет вызываться только один раз при
запуске сервера, поэтому это идеальное место для initialize()
базы
данных.
Заключение
H2 - полезный, производительный и простой в использовании сервер баз данных. Хотя это пакет Java, он также работает как автономный сервер, поэтому мы можем использовать его в Node.js с пакетом JDBC.
В этом руководстве мы сначала определили простой CRUD, чтобы проиллюстрировать, как получить доступ к базе данных и какие функции доступны. После этого мы определили REST API с Express . Это помогло нам получить более полное представление о том, как получать ресурсы и сохранять их в H2 .
Хотя некоторые концепции были опущены для краткости, такие как аутентификация и разбиение на страницы, это руководство является хорошей справкой для начала использования H2 в наших проектах Express.