common-lisp
CommonLisp
lisp
0

Common LispでWebアプリを作るときのパターン

Common LispでWebアプリを作るときのパターン

個人的なメモです。
忘れるかもしれないし、あとで見返すと役にたつかもしれない。

サーバーサイド

サーバーサイドでのCommon Lisp実装について。

永続化

Webアプリケーションといえば永続化 (ほんまか)。
何を永続化するのかわかってなくても永続化したくなってしまう。

日本語ではシンプルに ブログをDBに保存する と言うと思う。これをそのままCLに直すと

(save db blog)

になるはず。ブログ (blog) を DB (db) に 保存する (save)という対応になっていて、助詞を除けば必要十分な情報量になってそう。

dbの実体がMySQLの場合、saveの定義は下のような感じ。

(defun save (db blog)
  (let ((conn (mysql-db-connection db)))
    (send-mysql-query conn "INSERT INTO ...")
    (send-mysql-query conn "INSERT INTO ...")
  db)

send-mysql-querymysql-db-connectionはMySQLを扱うためのAPIのつもり。

戻り値としてdbを返しておくと、状態変化がなくなったように見えるとか、threading macroが使えたりして、いろいろ便利な気がする。

threading macroを使っても読みやすそう。

(defun save (db blog)
  (-> (mysql-db-connection db)
      (send-mysql-query "INSERT INTO ...")
      (send-mysql-query "INSERT INTO ..."))
  db)

ただ、こんな感じに生クエリを書いてもいいけど、ORマッパー (mitoとか) を使ったほうがお手軽。

dbの実態はMySQLかもしれないし別の何かかもしれない。
永続化がどのように実装されているかは、永続化を使う側からは気にしなくてすむような、抽象度の高い関数をインターフェースにしておくと、可読性が上がりそう。
一方、永続化の仕組みの詳細を露出させたほうが、より細かいチューニングが可能になって、パフォーマンスを向上しやすそう。とりあえず今は、ブログをDBに保存するを素直に表現することを第一に、抽象度の高い関数を公開しておく。

実行時において、dbの実体が複数ありうるときは、defgenericで実装するのが自然に思われる。

(defgeneric save (db blog)

(defmethod save ((db mysql-db) blog)
  (let ((conn (mysql-db-connection db)))
    (send-mysql-query conn "INSERT INTO ...")
    (send-mysql-query conn "INSERT INTO ..."))
  db)

(defmethod save ((db file-db) blog)
  (with-open-file (stream (file-db-path db) :direction :output)
    ...)
  db)

上の例では、dbの実態としてMySQLとファイルの例を書いた。
今の段階ではこれで書いたようにみえても、これからこの先も、これら二つの違いを覆い隠せるくらいの抽象度で実装できるかはわからない。DBに保存じゃなくて、MySQLに保存、になるかもしれない。実行時はどっちか1つしか選ばないのに最初からdefgenericで実装するのはムダかも。

永続化はrepositoryにやらせるのがよくあるパターン、だと思う。
だけど、今回ブログをDBに保存するから始まったせいか、repositoryの出番がなかった。
せっかくなので、パッケージにもってきてみる。

;; path: b/entities/blog/repository.lisp
(defpackage :b.entities.blog.repository
  (:use :cl)
  (:export :save))
(in-package :b.entities.blog.repository)

(defun save (db blog)
  (let ((conn (mysql-db-connection db)))
    (send-mysql-query conn "INSERT INTO ...")
    (send-mysql-query conn "INSERT INTO ..."))
  db)

これで永続化は終わり。

ルートのパッケージはb。(いい名前が思いつかなかった…)
entitiesは、他のアプリケーションから共通に使われる大事そうな概念を置くところというイメージ。

データ

ブログについて。ブログとは何かを言わずにブログについて扱っていた。

ブログは、中身、作成日時、ブログを一意に特定するidを持つことにする。
これはdefstructで定義できる。

(defstruct blog id created-at content)

ブログの作成日時が後で変更することは基本無く、idに至っては絶対変更しちゃいけないんだけど、defstructで定義しちゃうと変更可能になってしまう。
ここ、変更不可能にするやり方はある気がする。無いなら紳士協定で…。

;; path: b/entities/blog/blog.lisp
(defpackage :b.entities.blog
  (:use :cl)
  (:export :make-blog
           :blog-id
           :blog-created-at
           :blog-content))
(in-package :b.entities.blog)

(defstruct blog id created-at content)

一意なid

uuidでもいいし、MySQLでauto incrementする専用のテーブルを用意してもよさそう。

;; path: b/entities/id.lisp
(defpackage :b.entities.id
  (:use :cl)
  (:export :uuid-generator
           :gen))
(in-package :b.entities.blog)

(defgeneric gen (generator) ())

(defclass uuid-generator () ())

(defmethod gen ((generator uuid-generator))
  ...)

MySQLを使ってblogを永続化する場合、おそらくblogテーブルを作るので、blogテーブル自体にauto incrementなidカラムをつけてidを生成させることも可能になる。
この場合blogテーブルはblogの永続化だけでなく、blogの一意なidを生成する責務を追うことになる。
idの生成をDBにやらせるのはロジックがこんがらがりそうな気がするので、今回はblogの永続化とは独立してidを生成することにする。blogテーブルが単一責務の原則に違反しそうだし。

アプリケーション

ユーザーがやりたい処理を、entities等を使って処理するレイヤーというイメージ。

空ブログを作成する

idを振って、作成日時と一緒にブログを作って、dbに保存すると、空ブログを作成できる。
これをそのまま表現すると、下のように書ける。

;; path: b.lisp
(defpackage :b
  (:use :cl))
(in-package :b)

(defun blog-response (blog)
  (list :id (b.entities.blog:blog-id blog)
        :created-at (b.entities.blog:blog-created-at blog)
        :content (b.entities.blog:blog-content)))

(defun create-empty-blog (db &key id-generator)
  (let ((blog (b.entities.blog:make-blog
               :id (b.entities.id:gen id-generator)
               :created-at (get-universal-time)
               :content "")))
    (b.entities.blog.repository:save db blog)
    (blog-response blog))                

entities以下のオブジェクトは生で返さずリストで返すか、ラップして返すようにする。
こうすることで、アプリケーションに依存するレイヤーと、アプリケーションが依存するレイヤー (entitiesレイヤー) とか直に結合することが防ぐことができる。たとえば、b.entities.blogのリファクタリングがcreate-empty-blogを使う人にまで影響を及ぼさないようにできる。

structを定義するのが面倒だったので、返り値はプロパティリストに。

変更されるオブジェクト (ここではdb) を第一引数に持ってきている。
これはなんとなくだけど、オブジェクトの状態を変更するような関数を定義するとき、

(defun オブジェクトを変更する (変更されるオブジェクト 変更するためのパラメータ)
  ...)

という並びになっていると、読みやすい気がしている。

Web API

APIレイヤーは、httpやフレームワークのお作法に則りデータをやり取りするだけにしたい。

;; path: b/ningle/route.lisp
(setf (ningle:route *app* "/api/blogs/" :method :put)
      (lambda (params)
        (declare (ignore params))
        (blog->json
         (b:create-empty-blog
           (make-instance 'b.db.mysql:mysql-db :connection *connection*)
           :id-generator
           (make-instance 'b.entities.id:uuid-generator)))))

コンフィグファイルみたいなものから自動でmake-instanceされたりしたら便利そう。

ブログの中身を書き換える

空のブログを作っただけだと、多分あまりうれしくない。
一度作成したブログを編集できると、うれしい人がいるかもしれない。
これを実現するために、新しい関数をアプリケーションレイヤーにつくる。

実現方法としては、

  • blog-idに対応するblogをdbから再構成
  • 再構成したblogのcontentnew-contentに置き換え
  • 置き換えたblogを保存

になるだろう。これを実装すると、おそらく下のようになる。

;; path: b.lisp
(defun update-blog-content (db blog-id new-content)
  (let ((blog (b.entities.blog.repository:load-by-id db blog-id)))
    (if (not blog)
        (error 'no-such-blog-error :blob-id blog-id)
        (progn
          (setf (b.entities.blog:blog-content blog) new-content)
          (b.entities.blog.repository:update db blog)
          (blog-response blog)))))

blogをDBから再構成する関数と、blogを更新する関数を、新たにrepositoryに追加。

;; path: b/entities/blog/repository.lisp
(defun update (db blog)
  (let ((conn (mysql-db-connection db)))
    (send-mysql-query conn "UPDATE ...")
    (send-mysql-query conn "UPDATE ..."))
  db)

(defun load-by-id (db blog-id)
  ...)

ただ、このような機能を作って本当に喜ばれるかはわからないので、
実際に使ってみて、やっぱり使えないってなったら、潔く削除して、別の解決方法を考える。