Clojureに入門した
Clojureを完全理解した。
前提
入門者の状態
- REPL駆動開発の経験あり
- 関数型プログラミングの経験あり
- 前置記法の経験あり
- Clojureについてはなにもわからない1
やりたいこと
- 最終的には、受け取ったデータをテーブルに投入するサービスを作りたい2
- DOP(データ指向プログラミング)スタイルで実装したいので、DOP本家のClojureで書きたい
- Clojureについて何もわからないので、最低限の要素を学んでおきたい
本記事では、下記について学ぶことにした:
- データを入れた変数(動作確認時に入力の代わりに使う)
- dbに接続する方法
- 入力をテーブルスキーマにマッピングする関数
- 関数を使ったデータ投入
Clojureを使ってサーバーを作る方法もわからないが、それは別の機会にやってみる。
Clojureを触ってみる
まず完全理解する
バイナリをインストールした。 VSCode系エディタに Calva を入れた。
main.cljを作って適当にコードを書く:
(+ 1 2)
Evaluation によると ctrl + enter で評価してみると、REPLを立ち上げてくれそうなので “Connnect” する:
とりあえず “Start your project with a REPL and connect” を選ぶ:
エディタ上で評価結果をプレビュー3できるようになった:
関数定義 & 呼び出しも動いた:
最低限のシンタックスを理解する
変数定義は def らしいので書いてみる
マップは {"key1" value1 "key2" value2}と書くらしい(不安になるシンタックスだ)。
ちなみに、この↓状態からどうがんばってもカーソル左の閉じ括弧を削除できなかったが:
データを定義できた
ClojureからDBを操作してみる
DBのセットアップ
DBにはPostgreSQLを使いたい。 コンテナ版のPostgreSQL(18系)を使う準備をする:
# docker-compose.yml
services:
db:
image: postgres:18-alpine
ports:
- "${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_USER: yoshimoto
POSTGRES_PASSWORD: samonji
POSTGRES_DB: yoshimoto_samonji
volumes:
- postgres_data:/var/lib/postgresql # 18系はマウント位置がここ
networks:
- yoshimoto-samonji
networks:
yoshimoto-samonji:
driver: bridge
volumes:
postgres_data:
Docker Composeで起動する:
$ docker-compose up
Attaching to db-1
db-1 |
db-1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
db-1 |
db-1 | 2026-07-16 08:26:50.738 UTC [1] LOG: starting PostgreSQL 18.4 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
db-1 | 2026-07-16 08:26:50.738 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
db-1 | 2026-07-16 08:26:50.738 UTC [1] LOG: listening on IPv6 address "::", port 5432
db-1 | 2026-07-16 08:26:50.747 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db-1 | 2026-07-16 08:26:50.757 UTC [32] LOG: database system was shut down at 2026-07-16 08:22:51 UTC
db-1 | 2026-07-16 08:26:50.765 UTC [1] LOG: database system is ready to accept connections
テーブル作成などの初期セットアップをしたいので、PostgreSQLコンテナに入ってデータベースに接続する:
$ docker-compose exec db bash
b7127e8132c2:/#
b7127e8132c2:/# psql yoshimoto_samonji -U yoshimoto
psql (18.4)
Type "help" for help.
yoshimoto_samonji=#
接続できたので、テーブルを適当に作成する:
yoshimoto_samonji=# CREATE TABLE IF NOT EXISTS test (
yoshimoto_samonji(# column_1 VARCHAR(64),
yoshimoto_samonji(# column_2 VARCHAR(64)
yoshimoto_samonji(# );
CREATE TABLE
yoshimoto_samonji=# \dt
List of tables
Schema | Name | Type | Owner
--------+------+-------+-----------
public | test | table | yoshimoto
(1 row)
これでデータ投入準備が整った。
DBに接続可能なClojure環境の準備
当初、Postgresが立っているDocker network上にClojureコンテナをデプロイし、ターミナルからREPLに入っていたが、エディタとの連携がないため手打ちやコピペが辛かった。 DBの操作を一通り試すにはそこそこの量のコードを実行する必要がるので、VSCode系エディタからインライン実行ができるように、dev container用のClojure環境をセットアップすることにした。
下記のように、Postgresコンテナと同じネットワーク上にClojureコンテナを構成する:
# docker-compose.yml
services:
db:
# ------------8<----------------------
repl:
image: clojure:tools-deps-trixie-slim
working_dir: /workspace
volumes:
- <THE_CODE_PATH>:/workspace
stdin_open: true
tty: true
depends_on:
db:
condition: service_started
networks:
- yoshimoto-samonji # dbコンテナと同じネットワーク
environment:
DB_HOST: db
DB_PORT: "5432"
DB_NAME: yoshimoto_samonji
DB_USER: yoshimoto
DB_PASSWORD: samonji
command: sleep infinity
また、docker-compose.yml に定義したClojureコンテナを利用するように、dev containerを下記のように定義する:
# .devcontainer/devcontainer.json
{
"name": "clojure-container",
"dockerComposeFile": "<PATH_TO>/docker-compose.yml",
"service": "repl",
"workspaceFolder": "/workspace",
"shutdownAction": "stopCompose",
"customizations": {
"vscode": {
"extensions": [
"betterthantomorrow.calva"
]
}
}
}
Dev container起動時に依存の管理方法を聞かれるので、事前に準備しておく必要がある。
deps.edn を使うことにして、DBにはJDBCクライアントで接続するので、下記のように書いた:
;; deps.edn
{:deps
{org.clojure/clojure
{:mvn/version "1.12.5"}
com.github.seancorfield/next.jdbc
{:mvn/version "1.3.1070"}
org.postgresql/postgresql
{:mvn/version "42.7.7"}}}
以上を設定したうえでエディタのworkspaceをdev containerで開けば、Clojureコードの評価時に立ち上がるCalva REPL環境もdev container内で起動するようになった。
DBへの接続と操作
JDBCクライアントをロードする:
(require '[next.jdbc :as jdbc])
nil ;; <= REPLによる評価結果。以下同
PostgreSQLを利用するためのdata sourceを定義する:
(def ds
(jdbc/get-datasource {:dbtype "postgresql",
:host (System/getenv "DB_HOST"),
:port (parse-long (System/getenv "DB_PORT")),
:dbname (System/getenv "DB_NAME"),
:user (System/getenv "DB_USER"),
:password (System/getenv "DB_PASSWORD")}))
#'user/ds
上で定義したdata sourceを使って実際に接続できるか確認する:
(jdbc/execute! ds ["select current_database(), current_user"])
[{:current_database "yoshimoto_samonji", :current_user "yoshimoto"}]
接続できたので、引き続きテーブル一覧を取得する:
(jdbc/execute!
ds
["select table_name
from information_schema.tables
where table_schema = 'public'
order by table_name"])
[#:tables{:table_name "test"}]
↑先ほど手作業で作ったテーブル test をClojureからも確認できた。
テーブル定義を取得してみる:
(jdbc/execute!
ds
["select column_name, data_type, is_nullable
from information_schema.columns
where table_schema = 'public'
and table_name = ?
order by ordinal_position"
"test"])
[#:columns{:column_name "column_1", :data_type "character varying", :is_nullable "YES"}
#:columns{:column_name "column_2", :data_type "character varying", :is_nullable "YES"}]
↑定義したとおりのスキーマが見えた。
レコードを確認してみる:
(jdbc/execute! ds ["select *
from public.test"])
[]
↑ 当然、まだなにもない。
データを適当に投入してみる:
(jdbc/execute! ds
["insert into public.test (column_1, column_2)
values (?, ?), (?, ?)" "foo-bar" "hello"
"baz" "world"])
[#:next.jdbc{:update-count 2}]
レコードを再確認する:
(jdbc/execute! ds ["select *
from public.test"])
[#:test{:column_1 "foo-bar", :column_2 "hello"}
#:test{:column_1 "baz", :column_2 "world"}]
↑ 登録できていた。
以上から、SQLを直接実行できることを確認できた。
受け取ったデータをDBに投入してみる
次に、より実際のユースケースに近づけた検証をしてみた。 受け取った入力をテーブルのスキーマにマッピングしたうえでDBに投入したい。
まず入力を模したデータを定義する:
(def input {"key1" "abcd", "key2" "123"})
#'user/input
input
{"key1" "abcd", "key2" "123"}
↑ ちゃんと定義できている。
次に、入力をテーブルスキーマにマッピングする関数を定義:
(defn to-test-row [input] {:column_1 (get input "key1"), :column_2 (get input "key2")})
#'user/to-test-row
使ってみる:
(to-test-row input)
{:column_1 "abcd", :column_2 "123"}
↑ 一応できた。
ちょっと関数定義がハードコードっぽかったので、予めテーブルスキーマを定義し、それを参照する形に書き換える:
(def test-schema {:col-1 :column_1, :col-2 :column_2})
#'user/test-schema
(defn to-test-row [input] {(get test-schema :col-1) (get input "key1"), (get test-schema :col-2) (get input "key2")})
#'user/to-test-row
ちなみに、関数の構成要素を部分的に評価した場合にも評価結果をプレビューできるので検証サイクルを回しやすい。
部分評価した場合にも評価結果が見える
テーブルスキーマを参照する形で定義し直した関数を試す:
(to-test-row input)
{:column_1 "abcd", :column_2 "123"}
↑ 動いた。
次は、DBにデータを投入する関数を書きたい。
副作用のある関数には ! をつけるのが慣習らしい:
(require '[next.jdbc.sql :as sql])
nil
(defn save-test! [ds input] (sql/insert! ds :test (to-test-row input)))
#'user/save-test!
input を保存してみる:
(save-test! ds input)
#:test{:column_1 "abcd", :column_2 "123"}
↑保存できた。
その後もいろいろ叩きまくった結果:
(jdbc/execute! ds ["select *
from public.test"])
[#:test{:column_1 "foo-bar", :column_2 "hello"} #:test{:column_1 "baz", :column_2 "world"}
#:test{:column_1 "abcd", :column_2 "123"} #:test{:column_1 "foo-bar", :column_2 "hello"}
#:test{:column_1 "baz", :column_2 "world"} #:test{:column_1 "abcd", :column_2 "123"}
#:test{:column_1 "abcd", :column_2 "123"} #:test{:column_1 "abcd", :column_2 "123"}
#:test{:column_1 "abcd", :column_2 "123"} #:test{:column_1 "foo-bar", :column_2 "hello"}
#:test{:column_1 "baz", :column_2 "world"} #:test{:column_1 "abcd", :column_2 "123"}]
↑保存できている。
学んだこと
- 面倒でも、エディタ連携のREPL環境を作ると後が楽
- 書きにくさというより、評価結果が読みにくい。慣れていかねば
- Lisp系のシンタックスなのに、
[]と{}があるだけでなんかモダンに感じる
次はサーバー部分の書き方について学んでみたい。