Help us understand the problem. What is going on with this article?

Common LispでWebアプリ開発入門

はじめに

すごい久しぶりに投稿
Common LispでのWebアプリケーション開発について復習がてらまとめてみる
まずは環境構築、必要なものをインストール

Roswellインストール

roswell (Github)

インストール方法は上記リンク先のReadme参照

Emacsの設定

Emacsを使っている場合は、以下のmコマンドでslimeをインストールします

$ ros install slime

そして、~/.emacs.d/init.elに以下を追記します

init.el
(load (expand-file-name "~/.roswell/helper.el"))

Emacsを起動してM-x slimeでREPL起動。

Utopianインストール

RoswellでUtopianをインストール
fukamachi/utopian (Github)

$ ros install fukamachi/utopian

Qlotインストール

ライブラリバージョン管理ツールQlotをインストール
fukamachi/qlot (Github)

$ ros install qlot

EmacsLemQlotを使うための設定を以下に記載
cxxxr/lem (Github)

この設定を追記し、プロジェクトディレクトリ内でM-x slime-qlot-execを実行

Emacsを使っている場合

~/.emacs.d/init.elに以下を追記

init.el
(defun slime-qlot-exec (directory)
  (interactive (list (read-directory-name "Project directory: ")))
  (slime-start :program "qlot"
               :program-args '("exec" "ros" "-S" "." "run")
               :directory directory
               :name 'qlot
               :env (list (concat "PATH=" (mapconcat 'identity exec-path ":")))))

Lemを使っている場合

~/.lem/init.lispに以下を追記

init.lisp
(define-command slime-qlot-exec (directory)
  ((list (prompt-for-directory "Project directory: " (buffer-directory))))
  (change-directory directory)
  (lem-lisp-mode:run-slime (lem-lisp-mode::get-lisp-command :prefix "qlot exec ")))

最初のアプリケーション

簡単なプロジェクトを作成してみる

プロジェクト作成

データベースはデフォルトではSQLite3を使用する

$ utopian new hello-app

他のデータベースを使用する場合は以下のように--databaseオプションを付けてその後に使用するデータベースを入力する

$ utopian new hello-app-with-postgres --database postgres

生成されたプロジェクトの内訳

hello-app/
├── Lakefile
├── README.markdown
├── app.lisp
├── assets/
│   ├── javascripts/
│   │   └── main.js
│   └── stylesheets/
│       └── main.scss
├── boot.lisp
├── config/
│   ├── application.lisp
│   ├── environments/
│   │   ├── development.lisp
│   │   ├── production.lisp
│   │   └── test.lisp
│   ├── routes.lisp
│   ├── webpack.config.js
│   └── webpack.config.production.js
├── controllers/
│   └── root.lisp
├── db/
│   ├── migrations/
│   └── seeds.lisp
├── hello-app.asd
├── log/
├── models/
├── models.lisp
├── node_modules/
├── package-lock.json
├── package.json
├── public/
├── quicklisp/
├── qlfile
├── qlfile.lock
├── tests/
│   └── main.lisp
├── tests.lisp
└── views/
    ├── errors/
    │   └── 404.html
    ├── index.html.dj
    └── layouts/
        └── default.html.dj

依存関係のインストール

最初にCommon LispNode.jsの依存関係をインストール

$ qlot install
$ npm install

新たにライブラリを追加する場合はqlfileに以下のように追記

qlfile
git utopian https://github.com/fukamachi/utopian
ql lake :latest
git rove https://github.com/fukamachi/rove
git cl-dbi https://github.com/fukamachi/cl-dbi

;; string-caseを追加
ql string-case :latest

その後、qlot installでライブラリをインストール

アプリ起動

EmacsまたはLemを起動し、以下のコマンドを実行します。

M-x slime-qlot-exec

プロジェクトをQuicklispでロードします。

(ql:quickload :hello-app)

以下のコマンドでアプリを起動

(hello-app:start)

この状態でWebブラウザでhttp://localhost:5000/にアクセスするとWelcome to Utopian!と書かれたページが表示されます。

コントローラを生成

コントローラを追加するには以下のコマンドを実行する

$ utopian generate controller welcome index

以下のファイルが自動生成される

controllers/welcome.lisp
(defpackage #:hello-app/controllers/welcome
  (:use #:cl
        #:utopian)
  (:export #:index))
(in-package #:hello-app/controllers/welcome)

(defun index (params)
  (declare (ignore params))
  (render nil))

config/routes.lispにもルーティングルールが追記される

config/routes.lisp
(defpackage #:hello-app/config/routes
  (:use #:cl
        #:utopian
        #:hello-app/config/application)
  (:export #:*app*))
(in-package #:hello-app/config/routes)

(defvar *app* (make-instance 'application))
(clear-routing-rules *app*)

;;
;; Routing rules

(route :GET "/" "root:index")
(route :GET "/welcome/index" "welcome:index")  ; <= 追記されたルーティングルール

ついでにViewも作成される

views/welcome/index.html.dj
{% extends "layouts/default.html.dj" %}
{% block title %}hello-app - welcome{% endblock %}
{% block content %}
<h1>welcome/index</h1>
<p>Find me in views/welcome/index.html.dj</p>
{% endblock %}

cssファイルも生成される

assets/stylesheets/welcome.scss
// This SCSS file is for welcome controller.

追加したページにアクセスするには

controllers/welcome.lispindex関数を以下のように修正し、index関数をコンパイル(C-c C-c)する

controllers/welcome.lisp
(defpackage #:hello-app/controllers/welcome
  (:use #:cl
        #:utopian)
  (:export #:index))
(in-package #:hello-app/controllers/welcome)

(defun index (params)
  (declare (ignore params))
  (render nil :template #P"welcome/index.html.dj"))

次に、views/welcome/index.html.djの先頭のdefault.html.djへのパスを少々修正

views/welcome/index.html.dj
{% extends "../layouts/default.html.dj" %}
{% block title %}hello-app - welcome{% endblock %}
{% block content %}
<h1>welcome/index</h1>
<p>Find me in views/welcome/index.html.dj</p>
{% endblock %}

これでhttp://localhost:5000/welcome/indexにアクセス
または、以下のようリンクを貼る

views/index.html.dj
{% extends "layouts/default.html.dj" %}
{% block title %}Welcome to Utopian{% endblock %}
{% block content %}
<div id="main">
  Welcome to Utopian!
</div>
<a href="/welcome/index">Welcome</a>
{% endblock %}

http://localhost:5000/にアクセスしてWelcomeをクリック

リクエストパラメータを取得するには

assoc-utilsを使うと楽
fukamachi/assoc-utils

qlfileに以下を追加してqlot install

qlfile
git assoc-utils https://github.com/fukamachi/assoc-utils

assoc-utilsagetをインポート
agetを使ってparamsからパラメータを取得

controllers/welcome.lisp
(defpackage #:hello-app/controllers/welcome
  (:use #:cl
        #:utopian)
  (:import-from #:assoc-utils
                #:aget)
  (:export #:index))
(in-package #:hello-app/controllers/welcome)

(defun index (params)
  (let ((foo (aget params "foo"))
        (bar (aget params "bar")))
    (render-json
     `(("foo" . ,foo)
       ("bar" . ,bar))
     :from :alist)))

http://localhost:5000/welcome/index?foo=1&bar=2 にアクセスするとリクエストパラメータが得られる
render-jsonで以下のようにJSON形式で取得できる

{"foo":"1","bar":"2"}

モデルを生成

データベースの元になるモデルを以下のコマンドで作成

$ utopian generate model user name:varchar:20 email:varchar:255

以下のファイルが自動生成されます。

models/user.lisp
(defpackage #:hello-app/models/user
  (:use #:cl
        #:mito)
  (:export #:user
           #:user-name
           #:user-email))
(in-package #:hello-app/models/user)

(defclass user ()
  ((name :col-type (:varchar 20)
         :initarg :name
         :accessor user-name)
   (email :col-type (:varchar 255)
          :initarg :email
          :accessor user-email))
  (:metaclass dao-table-class))

DBマイグレーション

モデルの作成後、実際にデータベースにテーブルを追加する

$ ./quicklisp/bin/lake db:generate-migrations
CREATE TABLE "user" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" VARCHAR(20) NOT NULL,
    "email" VARCHAR(255) NOT NULL,
    "created_at" TIMESTAMP,
    "updated_at" TIMESTAMP
);
Successfully generated: /home/fireflower0/Programming/CommonLisp/WebApp/hello-app/db/migrations/20190805125858.up.sql

以下のファイルが生成される

db/migrations/20190805125858.up.sql
CREATE TABLE "user" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" VARCHAR(20) NOT NULL,
    "email" VARCHAR(255) NOT NULL,
    "created_at" TIMESTAMP,
    "updated_at" TIMESTAMP
);

以下のコマンドでファイル適用

$ ./quicklisp/bin/lake db:migrate
Applying '/home/fireflower0/Programming/CommonLisp/WebApp/hello-app/db/schema.sql'...
-> CREATE TABLE "user" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" VARCHAR(20) NOT NULL,
    "email" VARCHAR(255) NOT NULL,
    "created_at" TIMESTAMP,
    "updated_at" TIMESTAMP
);
-> CREATE TABLE IF NOT EXISTS "schema_migrations" (
    "version" VARCHAR(255) PRIMARY KEY
);
-> INSERT INTO schema_migrations (version) VALUES ('20190805125858');
Successfully updated to the version NIL.

テスト環境にも適用するには以下を実行

$ APP_ENV=test quicklisp/bin/lake db:migrate
Applying '/home/fireflower0/Programming/CommonLisp/WebApp/hello-app/db/schema.sql'...
-> CREATE TABLE "user" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" VARCHAR(20) NOT NULL,
    "email" VARCHAR(255) NOT NULL,
    "created_at" TIMESTAMP,
    "updated_at" TIMESTAMP
);
-> CREATE TABLE IF NOT EXISTS "schema_migrations" (
    "version" VARCHAR(255) PRIMARY KEY
);
-> INSERT INTO schema_migrations (version) VALUES ('20190805125858');
Successfully updated to the version NIL.

データベース操作

データベースにアクセスしてデータの挿入・更新・削除・参照など行う
mitoというO/Rマッパーライブラリを使う

fukamachi/mito

qlfileに以下を追加してqlot install

qlfile
git mito https://github.com/fukamachi/mito

mitoと作成したモデルのuserおよびそのアクセサをインポート

controllers/welcome.lisp
(defpackage #:hello-app/controllers/welcome
  (:use #:cl
        #:mito
        #:utopian)
  (:import-from #:hello-app/models/user
                #:user
                #:user-name
                #:user-email)
  (:import-from #:assoc-utils
                #:aget)
  (:export #:index))
(in-package #:hello-app/controllers/welcome)

(defun index (params)
  (let* ((name (aget params "name"))
         (email (aget params "email"))
         (user (or (find-dao 'user)
                   (create-dao 'user
                               :name name
                               :email email))))
    (render-json
     `(("name" . ,(user-name user))
       ("email" . ,(user-email user)))
     :from :alist)))

http://localhost:5000/welcome/index?name=foo&email=foo@test.comにアクセスするとリクエストパラメータで指定した値がデータベースに登録される
render-jsonで以下のようにJSON形式で取得できる

{"name":"foo","email":"foo@test.com"}

SQLを使う

Common Lisp用のSQLジェネレーターSxQLでS式でSQLを書くことができる

fukamachi/sxql

qlfileに以下を追加してqlot install

qlfile
git sxql https://github.com/fukamachi/sxql

SQL文は以下のように記述し、retrieve-by-sqlで実行する

(select (:*) (from 'user))

以下のように使用する

controllers/welcome.lisp
(defpackage #:hello-app/controllers/welcome
  (:use #:cl
        #:mito
        #:sxql
        #:utopian)
  (:import-from #:hello-app/models/user
                #:user
                #:user-name
                #:user-email)
  (:import-from #:assoc-utils
                #:aget)
  (:export #:index
           #:create))
(in-package #:hello-app/controllers/welcome)

(defun index (params)
  (declare (ignore params))
  (let ((user (car (retrieve-by-sql
                    ;; SELECT * FROM user
                    (select (:*) (from 'user))))))
    (render-json
     `(("id" . ,(getf user :id))
       ("name" . ,(getf user :name))
       ("email" . ,(getf user :email))
       ("created_at" . ,(getf user :created-at))
       ("updated_at" . ,(getf user :updated-at)))
     :from :alist)))

これでhttp://localhost:5000/welcome/indexにアクセス

{"id":1,"name":"foo","email":"foo@test.com","created_at":"2019-08-20 00:16:04.826224000Z","updated_at":"2019-08-20 00:16:04.826224000Z"}

テスト

テストは以下のように実行

$ ./quicklisp/bin/rove hello-app.asd
esting System hello-app/tests

;; testing 'hello-app/tests/main'
  hello-app/tests/main
    test-something
      ✓ Expect (= 1 1) to be true.

✓ 1 test completed

Summary:
  All 1 test passed.

デフォルトのテストファイル

tests/main.lisp
(defpackage #:hello-app/tests/main
  (:use #:cl
        #:rove))
(in-package #:hello-app/tests/main)

(deftest test-something
  (ok (= 1 1)))

以下のようにファイル単体でもテストできる

$ ./quicklisp/bin/rove tests/main.lisp
esting System hello-app/tests

;; testing 'hello-app/tests/main'
  hello-app/tests/main
    test-something
      ✓ Expect (= 1 1) to be true.

✓ 1 test completed

Summary:
  All 1 test passed.

最後に

ざっくりとCommon LispでのWebアプリケーション開発についてまとめてみました
メモ書き程度で説明が足りない気がする・・・、のでその都度書き足し予定

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした