Clojure 向けの、SQL ファーストかつ安全性をデフォルトとするデータアクセス方式。
このライブラリは以下を提供する:
.sql ファイルを唯一の信頼できる情報源とする)next.jdbcすべてのクエリは SQL ファイルで定義する。
DSL やクエリビルダで SQL を置き換えない。
理由:
サポートするのは、少数のコメントベース構文だけとする。
理由:
理由:
生成されるクエリは、データベースのインデックス構造に従う。
理由:
このライブラリは JDBC アクセス層そのものを置き換えることを目的としない。
初期実装では、実行バックエンドとして next.jdbc を前提とする。
理由:
SQL ファイルは classpath 上の論理パス sql/ 配下に置く。
通常は src/sql/ または resources/sql/ に置く。
推奨レイアウト:
sql/<database>/<schema>/<table>/<function-name>.sql
例:
src/sql/postgresql/public/users/get-by-id.sql
postgresql をデフォルトの database セグメントとするpublic理由:
/*$name*/Prepared statement のパラメータ。
? に置き換えられるuser.profile.status のような多段 dot-path を使えるkeyword → string → symbolSELECT * FROM users WHERE id = /*$id*/1
→
SELECT * FROM users WHERE id = ?
bind 値:
[123]
DEFAULTscalar の bind 変数に bisql/DEFAULT を渡した場合、? ではなく SQL の DEFAULT として出力される。
{:status bisql/DEFAULT}
INSERT INTO users (status) VALUES (/*$status*/'active')
→
INSERT INTO users (status) VALUES (DEFAULT)
ALLscalar の bind 変数に bisql/ALL を渡した場合、? ではなく SQL の ALL
として出力される。
{:limit bisql/ALL}
例:
SELECT * FROM users LIMIT ALL
初期実装での注意:
$ バインディングでのみサポートするIN /*$ids*/(...) のような collection binding では使えないIN)WHERE id IN /*$ids*/(1,2,3)
{:ids [10 20 30]}
→
WHERE id IN (?, ?, ?)
bind 値:
[10 20 30]
IN (...) の中でのみ有効理由:
/*%if*/WHERE 1 = 1
/*%if name */
AND name = /*$name*/'foo'
/*%else */
AND status = 'inactive'
/*%end */
xif / elseif / else / endelseif 断片: /*%elseif condition => <fragment> */else 断片: /*%else => <fragment> */nil と false だけを偽として扱うnil として扱うelseif は、最初の if に続く truthy な branch のうち最初に一致したものを使うelseif が => <fragment> を持つ場合、その inline fragment を elseif body として使うelse が => <fragment> を持つ場合、その inline fragment を else body として使うinline elseif => <fragment> または inline else => <fragment> と comment 外 body を同時に持つ場合、そのテンプレートは不正として拒否するelse の後ろに elseif は置けないAND または OR がある場合、その後続の演算子も取り除くAND または OR がなく、直前に WHERE または HAVING がある場合、その節キーワードも取り除くif と elseif でサポートするのは単一の変数名だけelse は式を取らない/*%for*/UPDATE users
SET
/*%for item in items separating , */
/*!item.name*/column_name = /*$item.value*/'sample'
/*%end */
WHERE id = /*$id*/1
/*%for item in items */ ... /*%end *//*%for item in items separating , */ ... /*%end */ の構文も使えるitem はループ内でのみ有効なローカル変数名item.name, item.value, user.profile.name のような dot-path 参照をサポートするkeyword → string → symbol の順で行うfor ブロックの直後に AND または OR がある場合、その後続の演算子も取り除くfor ブロックの直後に AND または OR がなく、直前に WHERE または HAVING がある場合、その節キーワードも取り除くseparating を使った場合、区切り文字は 2 件目以降の反復の前に出力される,, AND, OR を置いて最後だけ削る仕様は使わない。区切りが必要なら separating を使うfor をサポートしないif と for は、SQL ファイル全体を汎用プログラミング言語化するためではなく、SQL の一部を局所的に組み立てるために使う。
if の主な想定ユースケースWHERE / HAVING の条件出し分けSELECT *
FROM users
WHERE
/*%if active */
active = true
/*%end */
SET 項目の出し分けUPDATE users
SET
/*%if display-name */
display_name = /*$display-name*/'Alice'
/*%else */
display_name = display_name
/*%end */
WHERE id = /*$id*/1
else 断片SELECT *
FROM users
WHERE
/*%if active */
active = true
/*%else => status = 'inactive' */
/*%end */
elseif 分岐SELECT *
FROM users
WHERE
/*%if active */
active = true
/*%elseif pending */
status = 'pending'
/*%else */
status = 'inactive'
/*%end */
elseif 断片SELECT *
FROM users
WHERE
/*%if active */
active = true
/*%elseif pending => status = 'pending' */
/*%else => status = 'inactive' */
/*%end */
ORDER BY / LIMIT の有無の切り替えSELECT *
FROM users
/*%if sort-by-created-at */
ORDER BY created_at DESC
/*%end */
/*%if limit */
LIMIT /*$limit*/100
/*%end */
for の主な想定ユースケースWHERE 句の条件列挙SELECT *
FROM users
WHERE
/*%for item in filters separating AND */
/*!item.column*/column_name = /*$item.value*/'sample'
/*%end */
UPDATE ... SET の項目列挙UPDATE users
SET
/*%for item in items separating , */
/*!item.name*/column_name = /*$item.value*/'sample'
/*%end */
WHERE id = /*$id*/1
INSERT の列と値の列挙INSERT INTO users (
/*%for column in columns separating , */
/*!column.name*/column_name
/*%end */
) VALUES (
/*%for column in columns separating , */
/*$column.value*/'sample'
/*%end */
)
VALUESINSERT INTO users (email, status)
VALUES
/*%for row in rows separating , */
(/*$row.email*/'user@example.com', /*$row.status*/'active')
/*%end */
LIKE は型付きの値で扱う。
WHERE name LIKE /*$name*/'foo%'
{:name (sql/like-prefix "smith")}
→
WHERE name LIKE ?
bind 値:
["smith%"]
/*^name*/ (任意)SQL リテラルを直接埋め込む。
WHERE type = /*^type*/'A'
→
WHERE type = 'BOOK'
String は単一引用符付きの SQL 文字列リテラルとして埋め込むString に ' を含めてはならない理由:
/*!name*/ (上級者向け)明示的なエスケープハッチとして、生の SQL 断片を注入する。
ORDER BY /*!order-by*/column_name DESC
/*!name*/ も /*$name*/ や /*^name*/ と同様に、直後にサンプルトークンが必要/*!name*/ はデフォルトで安全な機能ではない/*$name*/ または /*^name*/ を優先する理由:
宣言コメントは template metadata を提供する。
/*:<name>
<edn>
*/
/*:doc */ だけは、EDN として読めない場合に trim した plain string として扱う:meta 配下に返す例:
/*:doc
Find orders by customer ID.
*/
/*:tags
[:orders :list]
*/
→
{:meta {:doc "Find orders by customer ID."
:tags [:orders :list]}}
/*:name */template 内ローカルな query 名を定義する。
/*:name find-user-by-email */
SELECT * FROM users WHERE email = /*$email*/'user@example.com'
/*:name */ を省略できる/*:name */ が必須load-query は単一 template ファイルのみを扱うload-queries は query-name をキーにして template を返す:name は query-name の解決に使い、返却される :meta にも残す.sql を除く)や /*:name */ に含まれる . は namespace 区切りとして扱うcore を使う初期実装では、公開 API は小さく保つ。
(load-query "postgresql/public/users/get-by-id.sql")
classpath 上の sql/... から SQL ファイルを読み込み、パース済み表現を返す。
(render-query query {:id 1})
テンプレート SQL を次の形に展開する:
{:sql "SELECT * FROM users WHERE id = ?"
:params [1]}
コンパイラ実装の土台として、次も公開する:
(def parsed-template
(parse-template "SELECT * FROM users WHERE id = /*$id*/1"))
(renderer-plan parsed-template)
(emit-renderer-form parsed-template)
(compile-renderer parsed-template)
(evaluate-renderer parsed-template {:id 1})
parse-template は declaration を除いた SQL template 文字列を parsed-template
へ変換する。renderer-plan はその parsed-template を実行寄りの plan へ変換する。
emit-renderer-form はその plan から再利用可能な renderer 関数 form を生成する。
compile-renderer は parsed-template を実行時に再利用可能な renderer 関数へ
コンパイルする。evaluate-renderer は parsed-template を直接評価し、内部
renderer 段階と同じ形を返す:
{:sql "SELECT * FROM users WHERE id = ?"
:bind-params [1]}
parsed-template 層は parser の出力であり、statement kind (:select,
:insert, :update, :delete) と、:where, :having, :set, :values,
:limit, :offset のような clause 単位の文脈も node に注釈する。
renderer-plan 層は、その後段の code generation と interpreter ベースの
renderer 経路のための実行寄り中間表現である。
現在の renderer-plan は、次の形を安定契約として持つ:
{:op :renderer-plan
:statement-kind :select
:steps [...]}
top-level の :steps ベクタは、実行寄りの step map を並べたものになる。
現在の step 種別は次の4つである:
:append-text:append-variable:branch:for-each:append-text は :sql, :context, :statement-kind を持つ。
:append-variable は :sigil, :parameter-name, :collection?, :context,
:statement-kind を持つ。
:branch は :branches を持ち、各 branch は次の形になる:
{:expr "active" ;; else は nil
:steps [...]}
:for-each は loop 契約として次の形を持つ:
{:op :for-each
:item-name "item"
:collection-name "items"
:separator ","
:context :set
:statement-kind :update
:steps [...]}
emitted Clojure form の正確な形自体は implementation detail だが、
parsed-template -> renderer-plan -> renderer-form という層分離は、
今後の compiler 境界として固定していく。
CLJ では、compile-renderer は引き続き emitted renderer form を eval して
関数化する。CLJS では、同じ renderer-plan を小さな interpreter へ入力し、
その interpreter を背後に持つ再利用可能 renderer 関数を返す。
現在の主経路は emit-renderer-form である。defrender と defquery は、
マクロ展開時に emitted renderer form をそのまま埋め込み、
compile-renderer は eval を使う実行時向けの薄い convenience wrapper
として残している。
(defrender)
(defrender "admin")
(defrender "/sql/postgresql/public/users/get-by-id.sql")
(defrender "/sql/postgresql/public/users")
defrender は、SQL ファイルに含まれる query ごとにレンダリング関数を定義する。
引数なしの場合は、現在の namespace を classpath 上のディレクトリへ変換し、
その配下の .sql ファイルを再帰的に走査して定義する。相対パスを渡した場合は、
その namespace 由来ディレクトリ配下として解決する。/ から始まるパスを渡した
場合は classpath root からの絶対パスとして解決する。ファイルではなくディレクトリを
渡した場合は、その配下の .sql ファイルを再帰的に走査し、見つかった query を
すべて定義する。現在の namespace は探索起点に使うだけで、実際の関数は見つかった
SQL ファイルのパスから導出される namespace に定義される。
defrender が query-name を解決する優先順位は次のとおり:
/*:name *//*:name */ に namespace suffix が含まれない場合でも、ファイル名側で suffix を
与えられる。生成される var 名は、最終的に解決された query-name の最後の
セグメントになる。それ以前のセグメントは、SQL ファイルの親ディレクトリ由来
namespace の下へ付け足す suffix として使う。
例:
sql/postgresql/public/users/get-by-id.sql
-> sql.postgresql.public.users.core/get-by-idsql/postgresql/public/users/hoge.list-order-by-created-at.sql
-> sql.postgresql.public.users.hoge/list-order-by-created-atsql/postgresql/public/users/crud.sql と /*:name crud.get-by-id */
-> sql.postgresql.public.users.crud/get-by-idディレクトリを読む場合、ファイルはパス順に再帰処理する。var 名衝突は error とする。
defquery は、実行可能な query 関数を定義する高レイヤのファサードである。
デフォルトでは :next-jdbc アダプタへ委譲する。
実行は adapter namespace 配下で提供する。
例:
(ns sql.postgresql.public.users.core
(:require [bisql.core :as bisql]
[bisql.adapter.next-jdbc :as bisql.jdbc]))
(bisql/defquery "/sql/postgresql/public/users/get-by-id.sql")
(bisql.jdbc/exec! datasource get-by-id {:id 42})
bisql.adapter.next-jdbc/exec! は、query function metadata の :cardinality を見て
next.jdbc/execute-one! または next.jdbc/execute! を選ぶ。:cardinality が
未指定の場合は :many をデフォルトとする。
これにより bisql.core は、読み込み・解析・レンダリング・関数生成に集中できる。
(generate-crud datasource {:schema "public"})
PostgreSQL のスキーマメタデータから、クエリ定義、SQL template ファイル、 テーブルごとの query namespace ファイルを生成する。
例:
(-> (generate-crud datasource {:schema "public"})
(write-crud-files! {:output-root "src/sql"}))
(-> (generate-crud datasource {:schema "public"})
(write-declaration-files! {:output-root "src/sql"}))
生成される namespace ファイルは、SQL テンプレートから導出した
docstring 付きの query var を declare する:
(ns sql.postgresql.public.users.crud
(declare ^{:arglists '([datasource] [datasource template-params])
:doc "..."}
get-by-id)
同じ生成フローは CLI としても提供できる:
clojure -M -m bisql.cli gen-config
clojure -M -m bisql.cli gen-crud --config bisql.edn
clojure -M -m bisql.cli gen-declarations --config bisql.edn
設定ファイルは :db と :generate を持つ EDN map とし、生成される雛形では既定値をコメントで例示する。設定ファイルがなくても、優先順位が CLI オプション > 環境変数 > 設定ファイル > デフォルトなので各コマンドは動作する。
gen-declarations は補助機能として残す。浅い階層で (defquery) を呼んだときに、
未宣言の namespace に関数が定義されるのを避けたいプロジェクトや、
IDE / REPL で使うナビゲーション用の declare と docstring がほしい
プロジェクトでは、明示的な namespace ファイルを生成する用途で使える。
デフォルトでは docstring にはプロジェクトルートからの相対 SQL パスと行番号だけを含め、
SQL テンプレート本文も含めたい場合は --include-sql-template を使う。
理由:
CRUD 関数はデータベーススキーマから生成する:
public(insert! db row)
以下の場合に生成する:
PostgreSQL の INSERT ... ON CONFLICT ON CONSTRAINT ... DO UPDATE RETURNING * を使う。
(upsert-by-id! db row)
複合キーの例:
(upsert-by-user-id-and-device-identifier! db row)
生成される upsert query は、挿入時の値を :inserting に入れて受け取る。
また、衝突時に既存行の値を維持したい列は :non-updating-cols で指定できる:
(users.crud/upsert-by-id
datasource
{:inserting {:email "alice@example.com"
:display-name "Alice"
:status "active"
:created-at #inst "2026-04-12T00:00:00Z"}
:non-updating-cols {:created-at true}})
このとき生成 SQL は INSERT INTO ... AS t を使い、
:non-updating-cols.<column> が truthy な列では
EXCLUDED.<column> の代わりに t.<column> を使う。
その結果、その列の値は変更されず既存値が維持される。
以下の場合にのみ生成する:
(update-by-id! db {:id ...
:set {...}})
複合キーの例:
(update-by-account-id-and-user-id! db {:account-id ...
:user-id ...
:set {...}})
以下の場合にのみ生成する:
(delete-by-id! db {:id ...})
複合キーの例:
(delete-by-account-id-and-user-id! db {:account-id ...
:user-id ...})
get-by-*以下の場合に生成する:
(get-by-id db {:id ...})
複合キーの例:
(get-by-account-id-and-user-id db {:account-id ...
:user-id ...})
list-by-*以下に対して生成する:
インデックス (a, b, c) に対して:
生成されるプレフィックス:
(a)(a, b)(a, b, c)生成しないもの:
(b)(b, c)(a, c)理由:
残りのインデックス列を ORDER BY に利用する。
例:
(customer_id, created_at, id)
customer_idWHERE customer_id = ?
ORDER BY created_at, id
customer_id, created_atWHERE customer_id = ?
AND created_at = ?
ORDER BY id
WHERE customer_id = ?
AND created_at = ?
AND id = ?
(ORDER BY なし)
理由:
すべての list-by-* 関数は limit と offset を必須とする。
(list-by-customer-id db {:customer-id 10
:limit 100
:offset 0})
offset は 0 以上の整数でなければならない理由:
生成される名前:
list-by-customer-id-and-created-at
ルール:
and で連結する理由:
以下は意図的に除外する:
>=, <=)理由:
テストはレイヤごとに分離する:
理由:
このシステムは以下を提供する:
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |