Рубрики
Без рубрики

Интеграция H2 с Python и Flask

В этом уроке мы будем использовать пакет JayDeBeApi для подключения Python к драйверу базы данных H2. Мы создали приложение RESTful Flask с возможностями CRUD.

Автор оригинала: Leandro Cofre.

Вступление

H2 – это легкий сервер баз данных, написанный на Java. Он может быть встроен в Java-приложения или работать как автономный сервер.

В этом уроке мы рассмотрим, почему H2 может быть хорошим вариантом для ваших проектов. Мы также узнаем, как интегрировать H2 с Python, создав простой Flask API.

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

H2 был построен с учетом производительности.

H2 – это сочетание: быстрый, стабильный, простой в использовании и функции”.

Хотя H2 выделяется главным образом тем, что может быть встроен в Java-приложения, он имеет некоторые интересные функции, которые также применимы к его серверной версии. Давайте посмотрим на некоторые из них.

Размер и производительность

Файл .jar , используемый для серверной версии, составляет около 2 МБ. Мы можем скачать его с сайта H2 , в комплекте с дополнительными скриптами и документацией. Однако если мы будем искать в Maven Central, то сможем загрузить файл .jar сам по себе .

H2 performance сияет в своей встроенной версии. Тем не менее, официальный бенчмарк показывает, что его клиент-серверная версия также впечатляет.

Базы данных в памяти и шифрование

Базы данных в памяти не являются постоянными. Все данные хранятся в памяти, поэтому скорость значительно увеличивается.

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

Шифрование-еще одна полезная функция для защиты данных в состоянии покоя. Базы данных могут быть зашифрованы с помощью алгоритма AES-128 .

Другие Полезные Функции

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

H2 удивляет своей простотой. Он предоставляет несколько полезных функций и прост в настройке.

Давайте запустим сервер H2 в рамках подготовки к следующим разделам:

$ java -cp ./h2-1.4.200.jar org.h2.tools.Server -tcp -tcpAllowOthers -tcpPort 5234 -baseDir ./ -ifNotExists

Аргументы, начинающиеся с tcp , позволяют осуществлять связь с сервером. Аргумент if Not Exists позволяет создать базу данных при первом обращении к ней.

Описание API и Общая схема

Предположим, мы пишем API для регистрации всех экзопланет, найденных на сегодняшний день. Экзопланеты-это планеты, находящиеся за пределами нашей Солнечной системы и вращающиеся вокруг других звезд.

Это наше простое определение API , CRUD для одного ресурса:

Определение REST API

Это определение вместе с остальной частью кода, который мы увидим далее, доступно в этом репо GitHub .

Вот как будет выглядеть наше приложение в конце этого урока:

Общая схема

Слева от диаграммы мы видим API-клиент. Этим клиентом может быть функция “Try it out” редактора Swagger или любой другой клиент, например Postman или cURL.

На другом конце мы находим сервер базы данных H2 , работающий на TCP-порту 5234 как было объяснено выше.

Наконец, наше приложение в середине состоит из трех файлов Python. У первого будет приложение Flask , которое будет отвечать на все запросы REST API. Все конечные точки, которые мы описали в приведенном выше определении, будут добавлены в этот файл.

Второй файл будет иметь персистентность, функции, которые обращаются к базе данных для выполнения операций CRUD, используя пакет JayDeBeApi .

Наконец, третий файл будет содержать схему, представляющую ресурс, которым управляет API, – Exoplanet . Мы будем использовать пакет Marshmallow для представления этой схемы. Первые два файла python будут использовать эту схему для представления ресурсов и передачи их друг другу.

Давайте начнем с файла персистентности.

Схема базы данных

Чтобы сохранить ресурс экзопланеты в базе данных 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() содержит строку подключения и учетные данные для доступа к серверу базы данных. Это проще для этого примера, но есть место для улучшения в отношении безопасности. Мы могли бы сохранить наши учетные данные в другом месте, например в переменных среды

Кроме того, мы добавили путь к файлу H2 jar в метод connect () , так как в нем есть нужный нам драйвер.

Строка соединения JDBC заканчивается на /экзопланетах . Это означает, что при первом подключении будет создана база данных под названием exoplanets .

Строка соединения JDBC заканчивается на /экзопланетах . Это означает, что при первом подключении будет создана база данных под названием exoplanets .

Схемы зефира и функции базы данных 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.

Ничто не мешает нам управлять результатами в этом формате и в конечном итоге возвращать их клиенту API. Но забегая вперед , мы знаем, что будем использовать Flask , поэтому было бы хорошо уже возвращать результаты в формате, который рекомендует Flask.

Ничто не мешает нам управлять результатами в этом формате и в конечном итоге возвращать их клиенту API. Но забегая вперед , мы знаем, что будем использовать || Flask||, поэтому было бы хорошо уже возвращать результаты в формате, который рекомендует Flask.

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 для преобразования и проверки наших ресурсов.

Теперь , когда мы познакомились с 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() для их объединения.

Последняя строка принимает объединенный результат и преобразует его в Exoplanet Schema объекты, которые Flask может далее обрабатывать.

Теперь, когда мы объяснили функцию _execute() и класс Exoplanet Schema , давайте посмотрим все функции базы данных 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() заслуживают более подробного объяснения.

Оператор SQL INSERT может принимать разделенные столбцы и значения в виде INSERT INTO table (column 1 Name) VALUES ('column1Value') . Мы можем использовать функцию join() для объединения всех столбцов и разделения их запятыми, а также сделать что-то подобное для объединения всех значений, которые мы хотим вставить.

Оператор UPDATE SQL немного сложнее. Его форма – UPDATE table SET . Поэтому нам нужно чередовать ключи и значения, и мы сделали это с помощью функции zip () .

Все эти функции возвращают None при возникновении проблемы. Позже, когда мы вызовем их, нам придется проверить это значение.

Давайте сохраним все функции базы данных в ее собственном файле, persistence.py , поэтому мы можем добавить некоторый контекст при вызове функций, например:

import persistence

persistence.get_all()

REST API С Колбой

Теперь, когда мы написали слой для абстрагирования доступа к базе данных, мы готовы написать 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/")

Таким образом, все наши маршруты, /экзопланеты и /экзопланеты/ будут направлены к определенному нами классу.

Например, конечная точка GET/exoplanets будет отвечать методом get() внутри класса Exoplanet . Поскольку у нас также есть конечная точка GET/exoplanet/ , этот метод 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 .