H2 は、Java で書かれた軽量なデータベースサーバーです。
Javaアプリケーションに組み込んだり、スタンドアロンサーバーとして動作させることができます。
このチュートリアルでは、なぜH2があなたのプロジェクトにとって良い選択肢となり得るのか、その理由を確認します。
また、シンプルなFlask APIを構築することで、H2とPythonを統合する方法を学びます。
H2の特徴
H2は、パフォーマンスを重視して作られました。
「H2は、高速、安定、使いやすさ、そして機能の組み合わせです。
H2は、主にJavaアプリケーションに組み込むことができるため著名ですが、サーバー版にも適用できる興味深い機能がいくつかあります。
次にそのいくつかを見てみましょう。
サイズと性能
サーバー版に使用されている.jarファイルは2MB程度です。
H2のサイトから、スクリプトやドキュメントと一緒にダウンロードできます。
Maven Centralで検索すれば、.jarファイルを単体でダウンロードすることもできます。
H2の性能は、組み込み版で発揮される。
それでも、公式ベンチマークでは、クライアント・サーバー版も印象的であることが示されている。
インメモリデータベースと暗号化
インメモリデータベースは永続的ではありません。
すべてのデータはメモリ上に保存されるため、速度が大幅に向上します。
H2のサイトでは、インメモリデータベースは、プロトタイピングや読み取り専用のデータベースを使用する場合に特に有効であると説明しています。
暗号化は、静止しているデータを保護するためのもう一つの便利な機能です。
データベースは、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を書くとしよう。
太陽系外惑星とは、太陽系外で他の星を周回する惑星を発見したものです。
REST APIの作成にまだ慣れていない方は、Spring BootでREST APIを構築する完全ガイドをお読みください!
これはシンプルなAPI定義で、1つのリソースに対するCRUDです。
この定義と、次に説明する残りのコードは、この GitHub リポジトリにあります。
このチュートリアルの最後には、私たちのアプリケーションはこのようになります。
この図の左側には、APIクライアントがあります。
このクライアントは、Swagger Editorの「Try it out」機能でも、PostmanやcURLのような他のクライアントでもかまいません。
もう一方の端には、H2データベースサーバーがあり、上で説明したようにTCPポート5234
で動作しています。
最後に、真ん中のアプリケーションは3つのPythonファイルから構成されています。
最初のファイルには、すべてのREST APIリクエストに応答するFlaskアプリが含まれています。
上記の定義で説明したすべてのエンドポイントは、このファイルに追加されます。
2つ目のファイルには、永続化、つまりデータベースにアクセスしてCRUD操作を実行する関数があり、 JayDeBeApi
パッケージを使用しています。
最後に、3番目のファイルには、APIが管理するリソースである Exoplanet
を表すスキーマが格納されます。
このスキーマを表現するために、Marshmallow
パッケージを使用します。
最初の二つの Python ファイルはこのスキーマを利用してリソースを表現し、互いに受け渡しすることになります。
まず、persistence ファイルから始めましょう。
データベーススキーマ
ExoplanetリソースをH2データベースに格納するために、まず基本的なCRUD関数を記述します。
まず、データベースの作成を書きましょう。
JDBCでデータベースにアクセスするために、JayDeBeApiパッケージを使用します。
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
初期化()`関数は、この後にヘルパー関数があるので、とてもシンプルです。
この関数は太陽系外惑星のテーブルが存在しなければ作成します。
この関数はAPIがリクエストを受信し始める前に実行されなければなりません。
Flaskのどこでそれを行うかは後述します。
_execute()
関数には、データベースサーバーにアクセスするための接続文字列と認証情報が含まれています。
この例ではよりシンプルですが、セキュリティに関しては改善の余地があります。
例えば環境変数など、別の場所に認証情報を保存することができます。
また、connect()
メソッドに H2 の jar ファイルのパスを追加しました。
これは、H2 に接続するために必要なドライバ org.h2.Driver
を含んでいるからです。
JDBC 接続文字列は /exoplanets
で終わります。
これは、初めて接続したときに exoplanets
という名前のデータベースが作成されることを意味します。
execute()が
_convert_to_schema()` 関数を使用して SQL クエリの結果を返すことができることにお気づきでしょうか。
では、その関数がどのように動作するのか見てみましょう。
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が推奨するフォーマットで結果を返せるようにしておくと良いだろう。
特に、APIルートの使用を容易にするためにFlask-RESTfulを使用する予定です。
そのパッケージは、リクエストを解析するために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
ネストされたクラスで実現されています。
これで、Marshmallow の 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()
メソッドで取得できます。
最初の2行でリスト内包を使ってカラム名と値を取得し、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()
についてはもう少し説明が必要でしょう。
INSERTSQL 文は、
INSERT INTO table (column1Name) VALUES (‘column1Value’)という形式で、カラムと値を分離して受け取ることができます。
すべてのカラムを結合してカンマで区切るにはjoin()` 関数を使用し、挿入したいすべての値を結合するには同様の操作を行います。
UPDATESQL文はもう少し複雑です。
その形式はUPDATE table SET column1Name = ‘column1Value’です。
そこで、キーと値を交互に並べる必要があるのですが、これはzip()` 関数を使って行いました。
これらの関数はすべて、問題が発生した場合には None
を返します。
後でこれらを呼び出すときには、その値をチェックする必要があります。
全てのデータベース関数を専用のファイル persistence.py
に保存して、関数を呼び出すときにコンテキストを追加できるようにしましょう。
import persistence
persistence.get_all()
FlaskによるREST API
データベースへのアクセスを抽象化するレイヤーを書いたので、REST APIを書く準備ができました。
できるだけ簡単に定義できるように、FlaskとFlask-RESTfulパッケージを使用します。
以前学習したように、リソースの検証にはMarshmallowも使います。
Flask-RESTful では、API リソースごとに 1 つのクラス、この場合は 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
というエンドポイントには、 Exoplanet
クラスの中の get()
というメソッドが応答します。
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アプリケーションを定義しました。
その後、FlaskとFlask-RESTfulを使用してREST APIを定義しました。
認証やページングなど、簡潔にするためにいくつかのコンセプトは省略しましたが、このチュートリアルはFlaskプロジェクトでH2を使い始めるための良いリファレンスになります。
です。