'(you lisp me)

Lispって何だ

Caveman2でイベント管理システムを構築した話

気づけば1月末です。
あ! あけましておめでとうございます(遅い)

昨年末の話なのですが、Caveman2でイベント管理システムなるものを作っておりました。
現在はAmazon EC2Ubuntuインスタンスで運用していて、ドメインはお名前.comで一番安いやつを選びました。
現物はこれです。

経緯としましては、大学内で非公式にLT大会を運営していて、それの統合的な管理が出来るものが欲しくて作りました。
connpassのグループ機能を切り出してきたみたいな感じです。
もともとは調整さんで出欠管理してたんですが、情報学部生としてそれは雑魚過ぎるなと思ったので自分で作った流れになります。

Caveman2の情報が少ないので、実装の参考になりそうなことをメモ程度に書いておきます。

目次

  • データベース連携
    • スキーマ
    • リモート データベースへの接続
    • モデルの書き方
  • <form>のデータ受け渡し
    • ファイルのアップロード
  • ログイン
    • 外部サービス連携

データベース連携

スキーマ

(caveman2:make-project)で作成されるdb/schema.sqlの使い道が不明だったのですが、quickdocs-databaseを見たところ、SQLのテーブルの定義を書いておくものみたいです。自動で生成されるからCaveman2に関係するファイルかと思いましたが、そうではないらしい(データベース周りの知識不足がバレてしまう)
使い方は例の通り、mysqlコマンドでデータベースを指定してリダイレクトしてやります。

$ mysql -uroot -e 'CREATE DATABASE quickdocs DEFAULT CHARACTER SET utf8'
$ mysql -uroot quickdocs-database < db/schema.sql

ここはCaveman2関係無い話。

リモート データベースへの接続

AWSのRDSでMySQLサーバーを利用しているので、リモートのデータベースに接続する必要がありました。
以下をsrc/config.lispに書いておきます。

(defconfig |production|
  `(:databases ((:maindb :mysql
                         :host "RDSインスタンスのエンドポイント"
                         :port 1234
                         :database-name "hoge"
                         :username "foo"
                         :password "bar"))))

この設定を利用する時は以下のように環境変数を設定してアプリケーションを起動します。

$ APP_ENV=production clackup --server :woo --port 8080 app.lisp

モデルの書き方

テーブルごとの処理はそれぞれ1つのファイルにまとめてモデルとして扱ったほうが楽ですし、拡張もしやすいです。
quickdocsの例だとsrc/model/以下にモジュールとして作成しているのでそれに倣います。
mitoが使えるならそっちを使った方が楽かもしれません。

  • src/model/event.lisp
(in-package :cl-user)
(defpackage ipu-lt-server.model.event
  (:use :cl
        :cl-annot.class
        :sxql)
  (:import-from :datafly
                :model
                :retrieve-all
                :retrieve-one
                :execute)
  (:import-from :datafly.inflate
                :octet-vector-to-string)
  (:import-from :cl-dbi
                :<dbi-database-error>)

;; ここから
  (:import-from :ipu-lt-server.model.himg
                :create-himg
                :himg-id)
  (:import-from :ipu-lt-server.model.user
                :user
                :user-id))
;; ここまでは他のモデルのインポート
(in-package :ipu-lt-server.model.event)

(syntax:use-syntax :annot)


@export-accessors
@export
@model
(defstruct (event
            (:has-many (users user)
             (select :*
                     (from :user)
                     (where (:= :id (select :user_id
                                            (from :participant)
                                            (where (:= :event_id id)))))
                     (order-by (:desc :id)))))
  id
  name
  description
  dates
  himg-id)
@export 'event-users


@export
(defun create-event (name dates upfile desc)
  (if (null (dates-validate dates))
      nil
      (let* ((himg (if (null upfile)
                       nil
                       (create-himg (get-universal-time) upfile)))
             (himgid (if (null himg)
                         nil
                         (himg-id himg))))
        (handler-case
            (progn
              (execute
               (insert-into :event
                            (set= :name name
                                  :dates (encode-date-str dates)
                                  :himg_id himgid
                                  :description desc)))
              (let ((event
                     (retrieve-one
                      (select :*
                              (from :event)
                              (where (:= :name name))
                              (limit 1))
                      :as 'event)))
                event))
          (<dbi-database-error> (c) nil)))))


@export
(defun get-event (id)
  (retrieve-one
   (select :*
           (from :event)
           (where (:= :id id))
           (limit 1))
   :as 'event))

こんな感じで書いていきます。
これをsrc/web.lispから利用するには、src/config.lispの中でuse宣言を追加して、cl-reexportでエクスポートします。
階層的なパッケージを作成したい時に利用するライブラリだそうです。

(in-package :cl-user)
(defpackage ipu-lt-server.db
  (:use :cl
        :ipu-lt-server.model.user
        :ipu-lt-server.model.himg
        :ipu-lt-server.model.event
        :ipu-lt-server.model.participant
        :ipu-lt-server.model.link)
;; 〜中略〜
(in-package :ipu-lt-server.db)

(cl-reexport:reexport-from :ipu-lt-server.model.user)
(cl-reexport:reexport-from :ipu-lt-server.model.himg)
(cl-reexport:reexport-from :ipu-lt-server.model.event)
(cl-reexport:reexport-from :ipu-lt-server.model.participant)
(cl-reexport:reexport-from :ipu-lt-server.model.link)

;; 〜以下略〜

続いてこれらのファイルをモジュールとして登録します。
asdファイルのcomponentsのところにmodelを書き加えてください。

  :components ((:module "src"
                :components
                ((:file "main" :depends-on ("config" "view" "db"))
                 (:file "web" :depends-on ("view" "openid"))
                 (:file "openid" :depends-on ("config" "view" "db"))
                 (:file "view" :depends-on ("config"))
                 (:file "db" :depends-on ("config" "model"))
                 (:module "model"
                  :components
                  ((:file "event" :depends-on ("himg" "user"))
                   (:file "user")
                   (:file "himg")
                   (:file "participant")
                   (:file "link")))
                 (:file "config"))))

<form>のデータ受け渡し

基本的には、そのまま引数に内容が入る形になります。

(defroute ("/post" :method :POST) (&key |message|)
  (format nil "~A" |message|))

ファイルのアップロード

ファイルも同時に送信する場合はformタグにenctype="multipart/form-data"属性を指定することになります。
この場合は(slot-value |file| 'flexi-streams::vector)としてバイト列を取り出してやる必要があります。
※この辺りの挙動が謎で、localhostでやると上手く行かなかった。

例えば画像ファイルであれば、そのまま(write-sequence)でファイルに書き出せばHTMLの方で表示出来る形になります。

(defroute ("/post" :method :POST) (&key |message| |file|)
  (let* ((time (write-to-string (get-universal-time)))
         (fname (merge-pathnames
                 (format nil "~A.jpg" time)
                 *static-directory*)))
    (with-open-file (out fname
                         :direction :output
                         :element-type '(unsigned-byte 8)
                         :if-does-not-exist :create)
      (write-sequence (slot-value |file| 'flexi-streams::vector) out))
    (format nil "~A" message)))

ログイン

外部サービス連携

今回作ったシステムのメインユーザーは学生なので、Googleアカウント認証を利用しました(学生メール=Gmail)
以前Qiitaに書いたコードをそのまま使っているのでそちらを参考にしてください。

qiita.com

以上、雑なまとめでした。