Martenとは
MartenはCrystal言語用のWebアプリケーションフレームワークです。ORMからテンプレートエンジンまで詰め込まれたいわゆるフルスタックフレームワークになります。
本記事執筆時点ではまだ公開から2ヶ月しか経っておらず、少し機能不足な面は否めませんが、他のCrystal用Webアプリケーションフレームワークには無い特徴を幾つか持っており、今後の発展に期待が持てます。
- 作者: Morgan Aubert氏(本記事執筆時点ではShopify所属)
- 公式サイト: https://martenframework.com
- GitHub: https://github.com/martenframework
Crystalユーザーには当然ながらRubyの愛好家が多いため、これまでのCrystal用WebアプリケーションフレームワークはRubyのRailsやSinatra、Rubyからの影響が色濃いElixirのPhoenixを参考にしたものが多いです。一方、Martenは作者が元々Pythonisitaだったこともあり、Djangoから強い影響を受けています。Djangoからの影響をざっと並べてみましょう。
- モデル定義からマイグレーションを自動生成
- Djangoの汎用ビューに似た汎用ハンドラ
- Django同様、意識的に機能を抑えたテンプレート
- 「アプリ」の概念
- プロジェクト構成
かなりDjangoに似ていますね。しかし単なるDjangoの模倣ではなく、モデルのコールバックなどRailsからの影響もいくらか受けているようです。Ruby界隈以外にも目を向ける作者の視野の広さが伺えます。このままうまく発展していけば PythonよりRuby派だけどフレームワークとしてはDjangoの方が好きだし静的型検査したいし動作も高速であってほしいワガママな人にはかなり刺さるフレームワーク になり得ると思います。
次節ではMartenの書き味をなんとなく理解して頂くために、モデル、マイグレーション、スキーマ、ハンドラ、ルーティング、テンプレートについてざっくりと紹介していきます。各機能の詳細な説明やインストール方法、アプリの概念とプロジェクト構成については公式ドキュメントを参照してください。
各機能の概要
モデルとマイグレーション
Martenのモデルとマイグレーションの仕組みはDjangoそっくりです。モデル定義から自動的にマイグレーションファイルが生成されるので、モデルへのカラム追加等の変更を行う際にRailsと違ってモデルとマイグレーションを二重管理せずに済みます。もちろんこのDjango方式にも色々と欠点はありますが、モデルとデータベーステーブルが密結合でOKかつ単純なCRUDしかしない小規模なアプリケーションを作る分には大変便利だと思います。
それでは実例を見てみましょう。まずはモデルを定義します。
# 論文モデル
# 既にTagモデル、Publisherモデル、Doiモデルが定義されているものとする
class Paper < Marten::Model
# 主キーをUUIDにする場合
# 整数連番にする場合は型をbig_intにし、auto: trueを指定する
# 整数連番の場合はinitialized_idメソッドの定義は不要
field :id, :uuid, primary_key: true
# :カラム名, :型名, オプション: 値
field :title, :string, max_size: 255
field :summary, :string, max_size: 255
field :content, :text
# 多対多
field :tags, :many_to_many, to: Tag
# 多対一
field :publisher, :many_to_one, to: Publisher
# 一対一
field :doi, :one_to_one, to: Doi
def initialized_id
@id ||= UUID.random
end
end
モデルを定義したらmarten genmigrations
コマンドでマイグレーションファイルを自動生成し、marten migrate
でマイグレーションを実行します。後からモデルにカラムを追加するなどの変更を行う場合でも変更処理を自動生成してくれます。Djangoと一緒ですね。
スキーマ
Martenにおけるスキーマとは、DBのスキーマではなく入力データに対するシリアライザのことを指します。フォームデータやJSONペイロードに対して使えます。
class TagSchema < Marten::Schema
field :name, :string, max_size: 50
field :description, :string, max_size: 255
end
ハンドラ
ハンドラはリクエストとレスポンスを処理するクラスで、DjangoのView、Railsのコントローラ(の一部)に相当します。単純なCRUDについてはDjangoのクラスベース汎用ビューに相当する汎用ハンドラが用意されており、記述量を大幅に減らすことができます。
まずは汎用ハンドラを使わないcreateの例です。フォームデータに対する処理では先程定義したスキーマが使われていますね。
class TagCreateHandler < Marten::Handler
@schema: TagSchema?
def get
render("tag_create.html", context: { schema: schema })
end
def post
if schema.valid?
tag = Tag.new(schema.validated_data)
tag.save!
redirect(reverse("home"))
else
render("tag_create.html", context: { schema: schema })
end
end
private def schema
@schema ||= TagSchema.new(request.data)
end
end
同じ処理を汎用ハンドラで書くとこうなります。記述量が激減していますね。
class TagCreateHandler < Marten::Handlers::RecordCreate
model Tag
schema TagSchema
template_name "tag_create.html"
success_route_name "home"
end
ルーティング
DjangoのURLディスパッチャに相当します。
各URLに対応するハンドラを紐付けていきます。
Marten.routes.draw do
path "/", TagListHandler, name: "home"
path "/tag/create", TagCreateHandler, name: "tag_create"
path "/tag/<pk:uuid>", TagDetailHandler, name: "tag_detail"
path "/tag/<pk:uuid>/update", TagUpdateHandler, name: "tag_update"
path "/tag/<pk:uuid>/delete", TagDeleteHandler, name: "tag_delete"
# 以下はプロジェクト生成時点で自動生成されているので、手書きする必要は無いです
if Marten.env.development?
path "#{Marten.settings.assets.url}<path:path>", Marten::Handlers::Defaults::Development::ServeAsset, name: "asset"
path "#{Marten.settings.media_files.url}<path:path>", Marten::Handlers::Defaults::Development::ServeMediaFile, name: "media_file"
end
end
テンプレート
Djangoのテンプレート、Railsのビューに相当します。
MartenのテンプレートはJinjaに似ていて、Crystalの知識を必要としません。
他の多くのテンプレートエンジンと同様に、Martenのテンプレートも再利用可能な部品をパーシャルとしてくくり出すことができます。
以下の例では、作成と更新のフォームをパーシャルとして作成し、それぞれのテンプレートで利用しています。
フォームのパーシャル
<form method="post" action="" novalidate>
<input type="hidden" name="csrftoken" value="{% csrf_token %}" />
<fieldset>
<div><label>タグ名</label></div>
<input type="text" name="{{ schema.name.id }}" value="{{ schema.name.value }}"/>
{% for error in schema.name.errors %}<p><small>{{ error.message }}</small></p>{% endfor %}
</fieldset>
<fieldset>
<div><label>タグの説明</label></div>
<textarea name="{{ schema.description.id }}" value="{{ schema.description.value }}">{{ schema.description.value }}</textarea>
{% for error in schema.description.errors %}<p><small>{{ error.message }}</small></p>{% endfor %}
</fieldset>
<fieldset>
<button>送信</button>
</fieldset>
</form>
作成のテンプレート
<h1>タグの作成</h1>
{% include "partials/tag_form.html" %}
更新のテンプレート
<h1>"{{ tag.name }}"タグの編集</h1>
{% include "partials/tag_form.html" %}
現時点でMartenに足りないもの
本記事執筆時点ではMartenは公開から僅か2ヶ月しか経っておらず、バージョンはまだ0.1.3です。非常に若いフレームワークなので、やはり足りない機能がいくつかあります。
- メール(2022年12月23日現在、既に実装のほとんどが完了しドキュメントの用意も進んでいるようで、間もなく使えるようになると思われます)
- 認証(作者が取り組む意思を表明しているので、遠からず使えるようになるはずです)
- JSON API作成の支援
このうちメールと認証については近く登場するようですが、JSON API作成支援についての見通しは立っていません。
現状でもハンドラからJSONを返すことができる上にスキーマはJSONペイロードも処理できるので、工夫すればAPI用途での使用も可能ではあります。筆者も簡易なREST APIを作ることには成功しました。※現状、筆者のAPI作成例はセキュリティへの配慮が不十分で他者がそのまま真似たら危険なので、まだ公開はしませんが
ただ、やはり定型的な処理と典型的な構造を予め提供する仕組みが欲しいところです。
Martenの構成はDjangoにそっくりなので、Django REST Frameworkに相当するものをプラグイン的に作ることは可能なはずです。
実は本当はこのアドベントカレンダー公開までに筆者が原型を作って協力者を募るつもりだったのですが、間に合いませんでした(笑
Marten REST Frameworkを一緒に作ってくださる方、大募集中です!