Вступление
H2 - это легкий сервер баз данных, написанный на Java. Он может быть встроен в приложения Java или работать как отдельный сервер.
В этом руководстве мы рассмотрим, почему H2 может быть хорошим вариантом для ваших проектов. Мы также узнаем, как интегрировать H2 с Python, создав простой API Flask.
Особенности 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
как описано выше.
Наконец, наше приложение посередине состоит из трех файлов Python. У первого будет приложение Flask , которое будет отвечать на все запросы REST API. Все конечные точки, которые мы описали в определении выше, будут добавлены в этот файл.
Второй файл будет иметь постоянство, функции, которые обращаются к базе
данных для выполнения операций CRUD с использованием пакета JayDeBeApi
Наконец, третий файл будет содержать схему, представляющую ресурс,
которым управляет API, Exoplanet
. Мы будем использовать Marshmallow
для представления этой схемы. Первые два файла Python будут использовать
эту схему для представления ресурсов и передачи их друг другу.
Начнем с файла постоянства.
Схема базы данных
Чтобы сохранить ресурс Exoplanet в базе данных H2, мы должны сначала написать основные функции CRUD. Начнем с написания создания базы данных. Мы используем пакет JayDeBeApi для доступа к базам данных через JDBC:
import jaydebeapi
def initialize():
_execute(
("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)"))
def _execute(query, returnResult=None):
connection = jaydebeapi.connect(
"org.h2.Driver",
"jdbc:h2:tcp://localhost:5234/exoplanets",
["SA", ""],
"../h2-1.4.200.jar")
cursor = connection.cursor()
cursor.execute(query)
if returnResult:
returnResult = _convert_to_schema(cursor)
cursor.close()
connection.close()
return returnResult
Функция initialize()
достаточно проста из-за вспомогательных функций
после. Он создает таблицу экзопланет, если она еще не существует. Эта
функция должна быть выполнена до того, как наш API начнет получать
запросы. Позже мы увидим, как это сделать с помощью Flask .
Функция _execute()
содержит строку подключения и учетные данные для
доступа к серверу базы данных. Для этого примера это проще, но есть
возможности для улучшения безопасности. Мы могли бы сохранить наши
учетные данные в другом месте, например, в переменных среды.
Кроме того, мы добавили путь к файлу jar H2 connect()
, поскольку в
нем есть драйвер, который нам нужен для подключения к H2 -
org.h2.Driver
.
Строка подключения JDBC заканчивается на /exoplanets
. Это означает,
что при первом подключении будет создана exoplanets
Вы могли заметить, что _execute()
может возвращать результат
SQL-запроса с помощью функции _convert_to_schema()
. Давайте теперь
посмотрим, как работает эта функция.
Схемы Marshmallow и функции базы данных CRUD
Некоторые запросы SQL возвращают табличные результаты, особенно SELECT
. JayDeBeApi отформатирует эти результаты как список кортежей.
Например, для схемы, определенной в последнем разделе, мы могли бы
получить результат, подобный этому:
>>> connection = jaydebeapi.connect(...
>>> cursor = connection.cursor()
>>> cursor.execute("SELECT * FROM exoplanets")
>>> cursor.fetchall()
[(1, 'Sample1', 2019, 4.5, 1.2, 'http://sample1.com')]
Ничто не мешает нам управлять результатами в этом формате и в конечном итоге возвращать их клиенту API. Но забегая вперед, мы знаем, что будем использовать Flask , поэтому было бы неплохо уже возвращать результаты в формате, рекомендованном Flask.
В частности, мы будем использовать Flask-RESTful для облегчения использования маршрутов API. Этот пакет рекомендует использовать Marshmallow для анализа запросов. Этот шаг позволяет нормализовать объекты. Таким образом, мы можем отбросить неизвестные свойства и, например, выделить ошибки проверки.
Давайте посмотрим, как будет выглядеть класс Exoplanet, чтобы мы могли продолжить обсуждение:
from marshmallow import Schema, fields, EXCLUDE
class ExoplanetSchema(Schema):
id = fields.Integer(allow_none=True)
name = fields.Str(required=True, error_messages={"required": "An exoplanet needs at least a name"})
year_discovered = fields.Integer(allow_none=True)
light_years = fields.Float(allow_none=True)
mass = fields.Float(allow_none=True)
link = fields.Url(allow_none=True)
class Meta:
unknown = EXCLUDE
Определение свойств кажется знакомым. Это то же самое, что и схема базы
данных, включая определение обязательных полей. Все поля имеют тип,
определяющий некоторую проверку по умолчанию. Например, link
определяется как URL-адрес, поэтому строка, не похожая на URL-адрес, не
будет действительной.
Сюда также могут быть включены конкретные сообщения об ошибках, например
проверка name
.
В этом примере проекта мы хотим отбросить или исключить все неизвестные
поля, и клиент API может отправлять ошибочные данные. Это достигается во
вложенном классе Meta
Теперь мы можем использовать load()
и loads()
Методы Зефир
преобразовать и проверить наши ресурсы.
Теперь, когда мы знакомы с Marshmallow , мы можем объяснить, что
_convert_to_schema()
:
def _convert_to_schema(cursor):
column_names = [record[0].lower() for record in cursor.description]
column_and_values = [dict(zip(column_names, record)) for record in cursor.fetchall()]
return ExoplanetSchema().load(column_and_values, many=True)
В JayDeBeApi имена столбцов сохраняются в description
курсора, а
данные можно получить с помощью метода fetchall()
Мы использовали
составные части списков в первых двух строках, чтобы получить имена и
значения столбцов, и zip()
чтобы объединить их.
Последняя строка принимает объединенный результат и преобразует их в
объекты ExoplanetSchema
которые Flask может обрабатывать в
дальнейшем.
Теперь, когда мы объяснили _execute()
и ExoplanetSchema
, давайте
посмотрим все функции базы данных CRUD:
def get_all():
return _execute("SELECT * FROM exoplanets", returnResult=True)
def get(Id):
return _execute("SELECT * FROM exoplanets WHERE id = {}".format(Id), returnResult=True)
def create(exoplanet):
count = _execute("SELECT count(*) AS count FROM exoplanets WHERE name LIKE '{}'".format(exoplanet.get("name")), returnResult=True)
if count[0]["count"] > 0:
return
columns = ", ".join(exoplanet.keys())
values = ", ".join("'{}'".format(value) for value in exoplanet.values())
_execute("INSERT INTO exoplanets ({}) VALUES({})".format(columns, values))
return {}
def update(exoplanet, Id):
count = _execute("SELECT count(*) AS count FROM exoplanets WHERE id = {}".format(Id), returnResult=True)
if count[0]["count"] == 0:
return
values = ["'{}'".format(value) for value in exoplanet.values()]
update_values = ", ".join("{} = {}".format(key, value) for key, value in zip(exoplanet.keys(), values))
_execute("UPDATE exoplanets SET {} WHERE id = {}".format(update_values, Id))
return {}
def delete(Id):
count = _execute("SELECT count(*) AS count FROM exoplanets WHERE id = {}".format(Id), returnResult=True)
if count[0]["count"] == 0:
return
_execute("DELETE FROM exoplanets WHERE id = {}".format(Id))
return {}
Все функции в основном являются SQL-запросами, но create()
и
update()
заслуживают дополнительных пояснений.
Оператор INSERT
SQL может получать разделенные столбцы и значения в
форме INSERT INTO table (column1Name) VALUES ('column1Value')
. Мы
можем использовать join()
чтобы объединить все столбцы и разделить их
запятыми, и сделать что-то подобное, чтобы объединить все значения,
которые мы хотим вставить.
Оператор UPDATE
SQL немного сложнее. Его форма -
UPDATE table SET column1Name = 'column1Value'
. Итак, нам нужно
чередовать ключи и значения, и мы сделали это с помощью функции zip()
.
Все эти функции возвращают None
при возникновении проблемы. Позже,
когда мы их вызовем, нам нужно будет проверить это значение.
Давайте сохраним все функции базы данных в отдельном файле
persistence.py
, чтобы мы могли добавить некоторый контекст при вызове
функций, например:
import persistence
persistence.get_all()
REST API с Flask
Теперь, когда мы написали слой для абстрагирования доступа к базе данных, мы готовы написать REST API. Мы будем использовать пакеты Flask и Flask-RESTful, чтобы сделать наше определение максимально простым. Как мы узнали ранее, мы также будем использовать Marshmallow для проверки ресурсов.
Flask-RESTful требует определения одного класса для каждого ресурса
API, в нашем случае только ресурса Exoplanet
Затем мы можем связать
этот ресурс с таким маршрутом:
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
api = Api(app)
class Exoplanet(Resource):
# ...
api.add_resource(Exoplanet, "/exoplanets", "/exoplanets/<int:Id>")
Таким образом, все наши маршруты, /exoplanets
и /exoplanets/<int:Id>
будут направлены в определенный нами класс.
Например, на конечную точку GET /exoplanets
get()
внутри класса
Exoplanet
Поскольку у нас также есть конечная точка
GET /exoplanet/<Id>
get()
должен иметь необязательный параметр с
именем Id
.
Давайте посмотрим на весь класс, чтобы лучше понять это:
from flask import request
from flask_restful import Resource, abort
from marshmallow import ValidationError
import persistence
class Exoplanet(Resource):
def get(self, Id=None):
if Id is None:
return persistence.get_all()
exoplanet = persistence.get(Id)
if not exoplanet:
abort(404, errors={"errors": {"message": "Exoplanet with Id {} does not exist".format(Id)}})
return exoplanet
def post(self):
try:
exoplanet = ExoplanetSchema(exclude=["id"]).loads(request.json)
if not persistence.create(exoplanet):
abort(404, errors={"errors": {"message": "Exoplanet with name {} already exists".format(request.json["name"])}})
except ValidationError as e:
abort(405, errors=e.messages)
def put(self, Id):
try:
exoplanet = ExoplanetSchema(exclude=["id"]).loads(request.json)
if not persistence.update(exoplanet, Id):
abort(404, errors={"errors": {"message": "Exoplanet with Id {} does not exist".format(Id)}})
except ValidationError as e:
abort(405, errors=e.messages)
def delete(self, Id):
if not persistence.delete(Id):
abort(404, errors={"errors": {"message": "Exoplanet with Id {} does not exist".format(Id)}})
Остальные HTTP-команды обрабатываются так же, как GET
, методами с
именем post()
, put()
и delete()
.
Как мы уже говорили ранее, логические ошибки при доступе к базе данных
заставят функции возвращать None
. Эти ошибки фиксируются здесь, когда
это необходимо.
Кроме того, исключения, которые представляют ошибки проверки, запускаются Marshmallow , поэтому эти ошибки также фиксируются и возвращаются пользователю вместе с соответствующей ошибкой возврата.
Заключение
H2 - полезный, производительный и простой в использовании сервер баз
данных. Хотя это пакет Java, он также может работать как автономный
сервер, поэтому мы можем использовать его в Python с пакетом
JayDeBeApi
В этом руководстве мы определили простое приложение CRUD, чтобы проиллюстрировать, как получить доступ к базе данных и какие функции доступны. После этого мы определили REST API с Flask и Flask-RESTful .
Хотя некоторые концепции были опущены для краткости, такие как аутентификация и разбиение на страницы, это руководство является хорошей справкой для начала использования H2 в наших проектах Flask.