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}と書くらしい(不安になるシンタックスだ)。

ちなみに、この↓状態からどうがんばってもカーソル左の閉じ括弧を削除できなかったが:

コマンドパレットから “slurp” で飲み込めた:

データを定義できた

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系のシンタックスなのに、[]{} があるだけでなんかモダンに感じる

次はサーバー部分の書き方について学んでみたい。


  1. JVMで動くLisp系言語らしいね、くらいの理解) ↩︎

  2. この用途で使いたいため ↩︎

  3. Emacsと違って、実際にコードに埋め込まれているわけではない(確かにコメントアウトされてないし) ↩︎