clails の Model は Ruby on Rails の ActiveRecord を参考にしたデータベースアクセスレイヤーです。 ただし、Rails とは異なり、すべてのカラム情報をハッシュテーブルで管理し、カラムへのアクセスには専用の関数を使用します。
- Model はデータベーステーブルと対応します
- カラム情報は直接クラス定義に記述せず、データベースから自動的に取得されます
- カラムへのアクセスには
ref関数、値の設定には(setf ref)を使用します - 変更されたカラムのみが更新されます(dirty flag による追跡)
Migration ファイルはデータベースのスキーマ変更を管理するファイルです。
db/migrate/ ディレクトリ配下に配置します。
YYYYmmdd-HHMMSS-description.lisp
例: 20240101-120000-create-users-table.lisp
(in-package #:your-app/db/migrate)
(defmigration "20240101-120000-create-users-table"
(:up #'(lambda (conn)
(create-table conn :table "users"
:columns '(("name" :type :string
:not-null T)
("email" :type :string
:not-null T)
("age" :type :integer
:not-null NIL)
("is-active" :type :boolean
:default-value T))))
:down #'(lambda (conn)
(drop-table conn :table "users"))))(defmigration "20240102-130000-add-phone-to-users"
(:up #'(lambda (conn)
(add-column conn :table "users"
:columns '(("phone" :type :string
:not-null NIL))))
:down #'(lambda (conn)
(drop-column conn :table "users"
:column "phone"))))(defmigration "20240103-140000-add-index-to-users"
(:up #'(lambda (conn)
(add-index conn :table "users"
:index "idx-users-email"
:columns '("email"))))
:down #'(lambda (conn)
(drop-index conn :table "users"
:index "idx-users-email")))):string- 文字列(VARCHAR):text- テキスト(TEXT):integer- 整数:float- 浮動小数点数:decimal- 固定小数点数:datetime- 日時:date- 日付:time- 時刻:boolean- 真偽値
カラムの型によって、データベースから取得される値の形式が異なります。
:datetime- Universal Time(整数)として返されます:date- Universal Time(整数)として返されます。時刻部分は 00:00:00 です:time- 00:00:00 からの経過秒数(整数)として返されます
:not-null- NULL を許可しない場合はT、許可する場合はNIL:default-value- デフォルト値を指定
;; データベース作成
(clails/model/migration:db-create)
;; Migration 実行
(clails/model/migration:db-migrate)
;; Migration ステータス確認
(clails/model/migration:db-status)
;; 最後の Migration をロールバック
(clails/model/migration:db-rollback)
;; 最後の N 個の Migration をロールバック
(clails/model/migration:db-rollback :step 3)
;; データベースに初期データを投入
(clails/model/migration:db-seed)最後の N 個の Migration をロールバックします。最近のデータベース変更を元に戻す必要がある場合に便利です。
;; 最後の Migration をロールバック
(clails/model/migration:db-rollback)
;; 最後の 3 つの Migration をロールバック
(clails/model/migration:db-rollback :step 3)db/seeds.lisp から初期データをデータベースに投入します。通常、テストデータや初期アプリケーションデータをデータベースに投入するために使用されます。
;; db/seeds.lisp を作成してシードデータを記述
;; 例: db/seeds.lisp の内容
;; (let ((user (make-record '<user>
;; :name "管理者"
;; :email "admin@example.com")))
;; (save user))
;; シードコマンドを実行
(clails/model/migration:db-seed)(in-package #:your-app/model)
(defmodel <user> (<base-model>)
(:table "users"))これだけで、users テーブルに対応する Model が定義されます。
カラム情報はデータベースから自動的に取得されます。
アプリケーション起動時に一度だけ実行します。
(clails/model/base-model:initialize-table-information):belongs-to- 親への参照(外部キーを持つ側):has-many- 子への参照(外部キーを持たない側)
;; 親 Model
(defmodel <company> (<base-model>)
(:table "company"
:relations ((:has-many "your-app/model::<department>"
:as :departments
:foreign-key :company-id))))
;; 子 Model
(defmodel <department> (<base-model>)
(:table "department"
:relations ((:belongs-to "your-app/model::<company>"
:column :company
:key :company-id)
(:has-many "your-app/model::<employee>"
:as :employees
:foreign-key :department-id))))
;; 孫 Model
(defmodel <employee> (<base-model>)
(:table "employee"
:relations ((:belongs-to "your-app/model::<department>"
:column :department
:key :department-id)))):as- 関連にアクセスするためのエイリアス(キーワード):foreign-key- 子テーブルが持つ外部キー(キーワード)
:column- 親にアクセスするためのエイリアス(キーワード):key- 自テーブルが持つ外部キー(キーワード)
新しいレコードのインスタンスを作成します。
;; 新規インスタンス作成
(defvar *user* (make-record '<user>
:name "Taro Yamada"
:email "taro@example.com"
:age 30
:is-active T))
;; カラムの値を取得
(ref *user* :name) ; => "Taro Yamada"
(ref *user* :email) ; => "taro@example.com"
(ref *user* :age) ; => 30
(ref *user* :is-active) ; => T
;; カラムの値を設定
(setf (ref *user* :age) 31);; 全件取得
(defvar *users* (execute-query
(query <user>
:as :user)
'()))
;; WHERE 句を使用
(defvar *active-users* (execute-query
(query <user>
:as :user
:where (:= (:user :is-active) T))
'()))
;; 並び替え
(defvar *sorted-users* (execute-query
(query <user>
:as :user
:order-by ((:user :created-at :desc)))
'()))
;; LIMIT と OFFSET
(defvar *page-users* (execute-query
(query <user>
:as :user
:limit 10
:offset 20)
'())):=- 等しい:>- より大きい:<- より小さい:>=- 以上:<=- 以下:!=- 等しくない:null- NULL である:not-null- NULL でない:in- IN 句:not-in- NOT IN 句:between- BETWEEN 句:not-between- NOT BETWEEN 句:and- AND 条件:or- OR 条件:like- LIKE 句:not-like- NOT LIKE 句
;; 比較演算子
(query <user>
:as :user
:where (:> (:user :age) 20))
;; NULL チェック
(query <user>
:as :user
:where (:null (:user :phone)))
;; IN 句
(query <user>
:as :user
:where (:in (:user :id) (1 2 3)))
;; IN 句(パラメータ使用)
(execute-query
(query <user>
:as :user
:where (:in (:user :id) :ids))
'(:ids (1 2 3)))
;; NOT IN 句
(query <user>
:as :user
:where (:not-in (:user :status) ("inactive" "deleted")))
;; NOT IN 句(パラメータ使用)
(execute-query
(query <user>
:as :user
:where (:not-in (:user :id) :excluded-ids))
'(:excluded-ids (10 20 30)))
;; BETWEEN 句
(query <user>
:as :user
:where (:between (:user :age) 20 30))
;; BETWEEN 句(パラメータ使用)
(execute-query
(query <user>
:as :user
:where (:between (:user :created-at) :start-date :end-date))
'(:start-date "2024-01-01" :end-date "2024-12-31"))
;; NOT BETWEEN 句
(query <user>
:as :user
:where (:not-between (:user :age) 0 17))
;; AND 条件
(query <user>
:as :user
:where (:and (:= (:user :is-active) T)
(:>= (:user :age) 18)))
;; OR 条件
(query <user>
:as :user
:where (:or (:= (:user :age) 20)
(:= (:user :age) 30)))
;; LIKE 句
(query <user>
:as :user
:where (:like (:user :email) "%@example.com"))
;; NOT LIKE 句
(query <user>
:as :user
:where (:not-like (:user :email) "%@test.com"))execute-query では、WHERE句で使用されるパラメータに対して、データベース固有の型に自動的に変換する機能を提供しています。これにより、アプリケーション側では型を意識せずにCommon Lispの値をそのまま使用できます。
以下の演算子を使用した場合、パラメータ値が自動的に変換されます:
:=(equal):>(greater than):<(less than):>=(greater than or equal):<=(less than or equal):!=(not equal):in(in):not-in(not in):between(between):not-between(not between)
注意: LIMIT/OFFSET のパラメータには型変換は適用されません。
以下のカラム型に対して自動変換が行われます:
:string- 文字列型:text- テキスト型:integer- 整数型:float- 浮動小数点型:decimal- 固定小数点型:datetime- 日時型:date- 日付型:time- 時刻型:boolean- 真偽値型
Boolean型のカラムに対しては、Common Lispの t/nil を使用でき、データベースごとに適切な値に自動変換されます。
;; Boolean型の自動変換
(let* ((query (query <user> :as :user :where (:= (:user :is-active) :active)))
(result (execute-query query '(:active t))))
;; t が自動的にデータベースの boolean 型に変換される
;; MySQL: 1, PostgreSQL: true, SQLite3: 1
result)
;; 複数条件での自動変換
(let* ((query (query <task>
:as :task
:where (:and (:= (:task :done) :done)
(:> (:task :priority) :min-priority))))
(result (execute-query query '(:done nil :min-priority 10))))
;; done と priority がそれぞれの型に応じて自動変換される
result)Datetime型のカラムに対しては、universal-time(整数)を渡すことができ、データベースの日時形式に自動変換されます。
;; Datetime型の自動変換
(let* ((completed-time (encode-universal-time 0 0 10 24 1 2026))
(query (query <task>
:as :task
:where (:>= (:task :completed-at) :completed-time)))
(result (execute-query query `(:completed-time ,completed-time))))
;; universal-time が "2026-01-24 10:00:00" のような形式に変換される
result)IN句やBETWEEN句でも自動型変換が適用されます。
;; IN句での自動変換
(let* ((query (query <product>
:as :product
:where (:in (:product :category-id) :category-ids)))
(result (execute-query query '(:category-ids (1 2 3)))))
;; リスト内の各値が適切な型に変換される
result)
;; BETWEEN句での自動変換
(let* ((start-time (encode-universal-time 0 0 0 1 1 2026))
(end-time (encode-universal-time 0 0 0 31 12 2026))
(query (query <event>
:as :event
:where (:between (:event :event-date) :start-date :end-date)))
(result (execute-query query `(:start-date ,start-time :end-date ,end-time))))
result)型変換を無効化したい場合は、:convert-types nil を指定できます。
;; 型変換を無効化(従来の動作)
(let* ((query (query <user> :as :user :where (:= (:user :is-active) :active)))
(result (execute-query query '(:active 1) :convert-types nil)))
;; 値がそのまま渡される
result)- 型情報の取得: カラムの型情報が取得できない場合は、変換をスキップし、値はそのまま渡されます
- 既存コードとの互換性: デフォルトで型変換が有効ですが、型情報がない場合は従来通り動作するため、既存コードへの影響はありません
- データベース固有の変換: 各データベースの型変換は
cl-db-fnを通じて実装されており、一貫性が保たれています
;; 昇順
(query <user>
:as :user
:order-by ((:user :name)))
;; 降順
(query <user>
:as :user
:order-by ((:user :created-at :desc)))
;; 複数カラム
(query <user>
:as :user
:order-by ((:user :age :desc)
(:user :name)))query マクロは静的なクエリ定義に適していますが、実行時にクエリを動的に構築したい場合は query-builder 関数を使用します。
;; query-builder でクエリインスタンスを作成
(defvar *q* (query-builder '<user> :as :user))
;; set-* 関数でクエリを構築
(set-columns *q* '((user :id :name :email)))
(set-where *q* '(:= (:user :status) :status))
(set-order-by *q* '((:user :created-at :desc)))
(set-limit *q* 10)
;; execute-query で実行
(execute-query *q* '(:status "active"))すべての setter 関数は、設定されている内容を置き換えます。また、メソッドチェーンのためにクエリインスタンス自身を返します。
set-columns- SELECT するカラムを設定set-joins- JOIN 句を設定set-where- WHERE 句を設定(nil で削除)set-order-by- ORDER BY 句を設定set-limit- LIMIT 句を設定set-offset- OFFSET 句を設定
;; メソッドチェーンの例
(execute-query
(set-limit
(set-offset
(set-columns (query-builder '<user> :as :user)
'((user :id :name)))
10)
20)
'())(defun search-users (search-column keyword)
"指定されたカラムでユーザーを検索"
(let ((q (query-builder '<user> :as :user)))
;; 実行時に決まるカラムを指定
(set-columns q `((user :id ,search-column)))
(set-where q `(:like (:user ,search-column) :keyword))
(execute-query q (list :keyword (format nil "%~A%" keyword)))))
;; 使用例
(search-users :name "John") ; name カラムで検索
(search-users :email "example") ; email カラムで検索(defun find-users (params)
"パラメータに応じて動的に検索条件を追加"
(let ((q (query-builder '<user> :as :user))
(conditions nil))
(set-columns q '((user :id :name :email :status)))
;; 条件を動的に追加
(when (getf params :status)
(push '(:= (:user :status) :status) conditions))
(when (getf params :min-age)
(push '(:>= (:user :age) :min-age) conditions))
(when (getf params :keyword)
(push '(:or
(:like (:user :name) :keyword)
(:like (:user :email) :keyword))
conditions))
;; 条件を AND で結合
(when conditions
(set-where q `(:and ,@(nreverse conditions))))
(execute-query q params)))
;; 使用例
(find-users '(:status "active" :min-age 20))
(find-users '(:keyword "%test%"))
(find-users '(:status "active" :keyword "%admin%"))(defun list-users (sort-by sort-order)
"ソート順を動的に変更"
(let ((q (query-builder '<user> :as :user)))
(set-columns q '((user :id :name :created-at)))
;; 実行時にソート順を決定
(set-order-by q `((:user ,sort-by ,sort-order)))
(execute-query q '())))
;; 使用例
(list-users :name :asc) ; 名前の昇順
(list-users :created-at :desc) ; 作成日時の降順;; ログで確認(LOG_LEVEL=sql:debug を設定)
(execute-query q params)
;; generate-query で直接確認
(let ((q (query-builder '<user> :as :user)))
(set-columns q '((user :id :name)))
(set-where q '(:= (:user :status) :status))
(multiple-value-bind (sql params)
(generate-query q '(:status "active"))
(format t "SQL: ~A~%" sql)
(format t "Params: ~S~%" params)))
;; => SQL: SELECT USER.ID as "USER.ID", USER.NAME as "USER.NAME" FROM users as USER WHERE USER.STATUS = ?
;; => Params: ("active")-
query マクロ: 静的で型安全なクエリが必要な場合(推奨)
- クエリの構造がコンパイル時に確定している
- IDE のサポート(補完、エラー検出)を受けたい
- 最もパフォーマンスが良い
-
query-builder: 動的なクエリ構築が必要な場合
- 検索条件をユーザー入力に応じて変更
- カラムやソート順を実行時に決定
- 複雑な条件分岐でクエリを組み立てる
;; 部署と会社を JOIN
(defvar *departments*
(execute-query
(query <department>
:as :dept
:columns ((dept :id :name)
(company :name))
:joins ((:inner-join :company)))
'()))
;; 結果の取得
(dolist (dept *departments*)
(format t "Department: ~A, Company: ~A~%"
(ref dept :name)
(ref (ref dept :company) :name)))
;; ref-in を使った簡潔な書き方
(dolist (dept *departments*)
(format t "Department: ~A, Company: ~A~%"
(ref dept :name)
(ref-in dept :company :name)))(defvar *departments*
(execute-query
(query <department>
:as :dept
:joins ((:left-join :company)))
'()));; 従業員 → 部署 → 会社
(defvar *employees*
(execute-query
(query <employee>
:as :emp
:columns ((emp :id :name :employee-number)
(dept :name)
(company :name))
:joins ((:inner-join :department)
(:inner-join :company :through :department)))
'()))
;; 結果の取得
(dolist (emp *employees*)
(format t "Employee: ~A, Department: ~A, Company: ~A~%"
(ref emp :name)
(ref-in emp :department :name)
(ref-in emp :company :name)))(defvar *filtered-employees*
(execute-query
(query <employee>
:as :emp
:joins ((:inner-join :department))
:where (:= (:dept :name) "Sales"))
'()))JOIN したデータにアクセスするには ref 関数を使います。
;; 基本的な使い方
(ref dept :name) ; 部署名
(ref (ref dept :company) :name) ; 会社名(JOIN したデータ)
;; JOIN したデータの取得例
(dolist (dept *departments*)
(format t "Department: ~A, Company: ~A~%"
(ref dept :name)
(ref (ref dept :company) :name)))ref-in は複数回の ref 呼び出しを簡略化するマクロです。リレーションやリストのインデックスを組み合わせてネストしたデータに簡潔にアクセスできます。
;; ref-in の使い方
(ref-in employee :department :name) ; (ref (ref employee :department) :name) と同じ
(ref-in employee :company :name) ; (ref (ref employee :company) :name) と同じ
;; リストとリレーションの組み合わせ
(ref-in blog :comments 0 :comment-account :username)
;; => (ref (ref (nth 0 (ref blog :comments)) :comment-account) :username) に展開される
;; JOIN と ref-in の組み合わせ例
(dolist (emp *employees*)
(format t "Employee: ~A, Department: ~A, Company: ~A~%"
(ref emp :name)
(ref-in emp :department :name)
(ref-in emp :company :name)))save 関数は自動的に INSERT または UPDATE を判断します。
;; INSERT(ID が nil の場合)
(defvar *new-user* (make-record '<user>
:name "Hanako Suzuki"
:email "hanako@example.com"
:age 25))
(save *new-user*)
;; 保存後、ID が自動的に設定されます
(ref *new-user* :id) ; => 1
;; UPDATE(ID が存在し、dirty flag が立っている場合)
(setf (ref *new-user* :age) 26)
(save *new-user*)
;; dirty flag がない場合は何もしない
(save *new-user*) ; => *new-user* (変更がないため UPDATE は実行されない)明示的に値を設定しなかったカラムには、データベースで定義されたデフォルト値が適用されます。
;; Migration でデフォルト値を定義
;; ("status" :type :string :default-value "active")
;; status を設定せずに保存
(defvar *product* (make-record '<product> :name "Sample"))
(save *product*)
;; データベースのデフォルト値が適用される
(ref *product* :status) ; => "active"validate メソッドをオーバーライドしてバリデーションを実装します。
(defmethod validate ((inst <user>))
(when (or (null (ref inst :name))
(string= (ref inst :name) ""))
(setf (ref-error inst :name)
"Name is required"))
(when (or (null (ref inst :email))
(string= (ref inst :email) ""))
(setf (ref-error inst :email)
"Email is required")))
;; バリデーションエラーがある場合、save は NIL を返す
(defvar *invalid-user* (make-record '<user> :name "" :email ""))
(save *invalid-user*) ; => NIL
;; エラーの確認
(has-error-p *invalid-user*) ; => T
(ref-error *invalid-user* :name) ; => "Name is required"
(ref-error *invalid-user* :email) ; => "Email is required"楽観的ロックを使用するには、Migration でバージョン管理用のカラムを作成し、defmodel で :version オプションを指定します。
;; Migration: バージョン管理用のカラムを作成
(defmigration "20251001-000000-create-posts-table"
(:up #'(lambda (conn)
(create-table conn :table "posts"
:columns '(("title" :type :string :not-null T)
("content" :type :text)
("lock-version" :type :integer
:not-null T
:default-value 0))))
:down #'(lambda (conn)
(drop-table conn :table "posts"))))
;; Model: :version オプションでバージョン管理カラムを指定
(defmodel <post> (<base-model>)
(:table "posts"
:version :lock-version))楽観的ロックが有効な場合、更新時にバージョンが一致しないと optimistic-lock-error が発生します。
;; 同じレコードを2つのインスタンスで取得
(defvar *post1* (first (execute-query
(query <post>
:as :post
:where (:= (:post :id) 1))
'())))
(defvar *post2* (first (execute-query
(query <post>
:as :post
:where (:= (:post :id) 1))
'())))
;; post1 を更新(成功)
(setf (ref *post1* :title) "Updated by user 1")
(save *post1*)
(ref *post1* :lock-version) ; => 1
;; post2 を更新(失敗: バージョンが古い)
(setf (ref *post2* :title) "Updated by user 2")
(handler-case
(save *post2*)
(clails/condition:optimistic-lock-error ()
(format t "Record was modified by another process~%")))注意: lock-version という名前のカラムがあっても、defmodel で :version オプションを指定しなければ楽観的ロックは有効になりません。
悲観的ロックを使用することで、データベースレベルでレコードやデータベースをロックし、他のトランザクションによる同時更新を防ぐことができます。
clails では with-locked-transaction マクロを使用して悲観的ロックを実現します。
;; レコードをロックして更新
(clails/model/lock:with-locked-transaction (user
(query <user>
:as :user
:where (:= (:user :id) :user-id))
(list :user-id 1))
;; このトランザクション内で user を安全に更新できる
(setf (ref user :balance) (+ (ref user :balance) 100))
(save user))with-locked-transaction には2つの形式でクエリを指定できます。
(with-locked-transaction (users
(query <user>
:as :user
:where (:= (:user :status) :status))
(list :status "active"))
;; users を処理
(loop for user in users
do (process-user user)))(use-package :batis.macro)
;; cl-batis で SQL を定義
(defparameter *get-active-users*
(select "SELECT * FROM users WHERE status = #{:status}"))
;; with-locked-transaction で使用
(with-locked-transaction (users
*get-active-users*
(list :status "active"))
(loop for user in users
do (process-user user)))データベースごとにサポートされるロックモードが異なります。
:for-update- 行を更新または削除のためにロック(デフォルト):for-share- 行を読み取りのためにロック:for-no-key-update- キー以外の更新のためにロック:for-key-share- キーの読み取りのためにロック
;; FOR UPDATE でロック
(with-locked-transaction (user
(query <user> :as :user :where (:= (:user :id) :id))
(list :id 1)
:mode :for-update)
(setf (ref user :status) "processing")
(save user))
;; FOR SHARE でロック
(with-locked-transaction (user
(query <user> :as :user :where (:= (:user :id) :id))
(list :id 1)
:mode :for-share)
;; 読み取り専用の処理
(ref user :name)):for-update- 行を更新または削除のためにロック(デフォルト):for-share- 行を読み取りのためにロック
(with-locked-transaction (user
(query <user> :as :user :where (:= (:user :id) :id))
(list :id 1)
:mode :for-update)
(setf (ref user :balance) (+ (ref user :balance) 100))
(save user))SQLite3 は行レベルロックをサポートしていないため、データベースレベルでロックします。
:immediate- 読み取りを許可し、書き込みをロック(デフォルト、推奨):exclusive- すべてのアクセスをロック(注意して使用):deferred- 最初のクエリまでロックを遅延
;; IMMEDIATE モード(推奨)
(with-locked-transaction (user
(query <user> :as :user :where (:= (:user :id) :id))
(list :id 1)
:mode :immediate)
(setf (ref user :balance) (+ (ref user :balance) 100))
(save user))注意: SQLite3 で :exclusive モードを使用すると、ロックを使用していない他の接続も含めてすべての接続がエラーになります。ほとんどの場合、:immediate モードの使用を推奨します。
ロックが取得できない場合、待機せずにすぐにエラーを返します(PostgreSQL と MySQL でサポート)。
(handler-case
(with-locked-transaction (user
(query <user> :as :user :where (:= (:user :id) :id))
(list :id 1)
:nowait T)
(setf (ref user :balance) (+ (ref user :balance) 100))
(save user))
(error (e)
(format t "Could not acquire lock: ~A~%" e)))ロックされている行をスキップして、ロックされていない行のみを取得します(PostgreSQL と MySQL でサポート)。
;; ロックされていないユーザーを取得して処理
(with-locked-transaction (users
(query <user>
:as :user
:where (:= (:user :status) :status))
(list :status "pending")
:skip-locked T)
(loop for user in users
do (progn
(setf (ref user :status) "processing")
(save user))))- トランザクション:
with-locked-transactionは自動的にトランザクションを開始し、正常終了時にコミット、エラー時にロールバックします - デッドロック: 複数のレコードをロックする場合は、常に同じ順序でロックしてデッドロックを避けてください
- ロックの保持時間: トランザクション内の処理は可能な限り短時間で完了させてください
- SQLite3 の制約: SQLite3 は行レベルロックをサポートしていないため、データベース全体がロックされます
- リトライロジック: SQLite3 ではロックエラー時に自動的にリトライします(指数バックオフ付き)
clails では、cl-batis を使用してネイティブな SQL クエリを実行することができます。 複雑な集計処理や、クエリビルダーでは表現しにくいクエリを直接 SQL で記述できます。
cl-batis は MyBatis にインスパイアされた SQL マッパーライブラリです。 SQL を Common Lisp のコード内に直接記述し、動的にパラメータをバインドできます。
詳細は cl-batis のドキュメント を参照してください。
cl-batis では2つの形式でクエリを定義できます。
アノテーション形式(@select):
(use-package :cl-batis)
(cl-syntax:use-syntax :annot)
;; SELECT クエリを定義
@select
("SELECT * FROM users WHERE id = :id")
(defsql get-user-by-id (id))
;; クエリを実行
(defvar *user* (first (execute-query get-user-by-id
(list :id 1))))関数形式(select):
(use-package :cl-batis)
;; SELECT クエリを定義
(select
("SELECT * FROM users WHERE id = :id")
(defsql get-user-by-id (id)))
;; クエリを実行
(defvar *user* (first (execute-query get-user-by-id
(list :id 1))))アノテーション形式:
@select
("SELECT * FROM users"
(sql-where
(sql-cond (not (null name))
" AND name LIKE :name ")
(sql-cond (not (null email))
" AND email = :email "))
"ORDER BY created_at DESC")
(defsql search-users (name email))関数形式:
(select
("SELECT * FROM users"
(sql-where
(sql-cond (not (null name))
" AND name LIKE :name ")
(sql-cond (not (null email))
" AND email = :email "))
"ORDER BY created_at DESC")
(defsql search-users (name email)))使用例:
;; 名前で検索
(execute-query search-users
(list :name "%yamada%" :email nil))
;; 名前とメールで検索
(execute-query search-users
(list :name "%yamada%" :email "yamada@example.com"))
;; 条件なし
(execute-query search-users
(list :name nil :email nil));; アノテーション形式
@select
("SELECT u.*, d.name as department_name
FROM users u
INNER JOIN departments d ON u.department_id = d.id
WHERE u.is_active = :is_active")
(defsql get-users-with-departments (is_active))
;; 関数形式
(select
("SELECT u.*, d.name as department_name
FROM users u
INNER JOIN departments d ON u.department_id = d.id
WHERE u.is_active = :is_active")
(defsql get-users-with-departments (is_active)))
(execute-query get-users-with-departments
(list :is_active T))@select
("SELECT department_id, COUNT(*) as user_count
FROM users
WHERE created_at >= :start_date
AND created_at < :end_date
GROUP BY department_id
HAVING COUNT(*) > :min_count")
(defsql count-users-by-department (start_date end_date min_count))
(execute-query count-users-by-department
(list :start_date "2024-01-01"
:end_date "2024-12-31"
:min_count 5))UPDATE クエリには @update または update を使用します。
アノテーション形式:
@update
("UPDATE users
SET status = :status,
updated_at = :updated_at
WHERE id = :id")
(defsql update-user-status (status updated_at id))関数形式:
(update
("UPDATE users
SET status = :status,
updated_at = :updated_at
WHERE id = :id")
(defsql update-user-status (status updated_at id)))使用例:
;; クエリを実行(更新された行数が返される)
(defvar *affected-rows*
(execute-query update-user-status
(list :status "active"
:updated_at (get-universal-time)
:id 1)))sql-set を使用して、条件に応じて SET 句を動的に構築します。
@update
("UPDATE users"
(sql-set
(sql-cond (not (null name))
" name = :name, ")
(sql-cond (not (null email))
" email = :email, ")
" updated_at = :updated_at ")
(sql-where
" id = :id "))
(defsql update-user-fields (name email updated_at id))
;; 名前のみ更新
(execute-query update-user-fields
(list :id 1
:name "New Name"
:email nil
:updated_at (get-universal-time)))
;; 名前とメールを更新
(execute-query update-user-fields
(list :id 1
:name "New Name"
:email "newemail@example.com"
:updated_at (get-universal-time)))@update
("UPDATE users
SET is_active = FALSE,
updated_at = :updated_at
WHERE last_login_at < :threshold_date
AND is_active = TRUE")
(defsql deactivate-old-users (updated_at threshold_date))
(execute-query deactivate-old-users
(list :threshold_date "2023-01-01"
:updated_at (get-universal-time)))INSERT や DELETE クエリも同様に @update または update で定義できます(SQL の種類に関わらず、SELECT 以外は @update/update を使用します)。
;; INSERT(アノテーション形式)
@update
("INSERT INTO logs (user_id, action, created_at)
VALUES (:user_id, :action, :created_at)")
(defsql insert-log (user_id action created_at))
;; INSERT(関数形式)
(update
("INSERT INTO logs (user_id, action, created_at)
VALUES (:user_id, :action, :created_at)")
(defsql insert-log (user_id action created_at)))
(execute-query insert-log
(list :user_id 1
:action "login"
:created_at (get-universal-time)))
;; DELETE
@update
("DELETE FROM logs
WHERE created_at < :threshold_date")
(defsql delete-old-logs (threshold_date))
(execute-query delete-old-logs
(list :threshold_date "2023-01-01"))cl-batis は sql-cond、sql-where、sql-set を使用した動的な SQL 生成機能を提供しています。
条件に応じて WHERE 句の一部を含めるかどうかを制御します。
@select
("SELECT * FROM users"
(sql-where
(sql-cond (not (null name))
" AND name = :name ")
(sql-cond (not (null min_age))
" AND age >= :min_age ")))
(defsql find-users (name min_age))
;; name のみ指定
(execute-query find-users (list :name "Alice" :min_age nil))
;; => SELECT * FROM users WHERE AND name = ?
;; 両方指定
(execute-query find-users (list :name "Alice" :min_age 20))
;; => SELECT * FROM users WHERE AND name = ? AND age >= ?
;; 条件なし
(execute-query find-users (list :name nil :min_age nil))
;; => SELECT * FROM usersUPDATE 文で条件に応じて SET 句を構築します。
@update
("UPDATE users"
(sql-set
(sql-cond (not (null name))
" name = :name, ")
(sql-cond (not (null email))
" email = :email, ")
" updated_at = :updated_at ")
(sql-where
" id = :id "))
(defsql update-user (name email updated_at id))Native Query も通常のクエリと同様にトランザクション内で使用できます。
(with-transaction
;; 複数のクエリをアトミックに実行
(execute-query update-user-status
(list :status "active" :id 1))
(execute-query insert-log
(list :user_id 1
:action "status_changed"
:created_at (get-universal-time))))- SQL インジェクション対策: パラメータは必ず
:param_nameの形式で記述し、直接文字列連結しないでください - 戻り値の型:
- SELECT クエリ: plist のリストを返します
- UPDATE/INSERT/DELETE クエリ: 影響を受けた行数を返します
- パラメータ名: キーワードシンボルを使用してください(
:id、:nameなど) - データベース依存: ネイティブクエリを使用する場合、データベース固有の SQL 構文に注意してください
- @select/@update マクロ:
@selectと@updateはアノテーション形式のマクロで、(cl-syntax:use-syntax :annot)が必要です
clails では、データベーストランザクションを簡単に扱うための with-transaction マクロを提供しています。
(clails/model/transaction:with-transaction
;; トランザクション内で実行される処理
(let ((user (make-record '<user> :name "Taro" :email "taro@example.com")))
(save user))
(let ((profile (make-record '<profile> :user-id (ref user :id) :bio "Hello")))
(save profile)))- 自動コミット: ブロック内の処理がすべて正常に完了すると、自動的にコミットされます
- 自動ロールバック: ブロック内でエラーが発生すると、自動的にロールバックされます
- コネクション管理: コネクションの取得と解放は自動的に行われます
;; 正常終了の場合: 自動コミット
(with-transaction
(let ((user (make-record '<user> :name "Taro")))
(save user)))
;; => ここでコミットされる
;; エラー発生の場合: 自動ロールバック
(handler-case
(with-transaction
(let ((user (make-record '<user> :name "Taro")))
(save user))
(error "Something went wrong"))
(error (e)
;; トランザクションは自動的にロールバックされる
(format t "Transaction rolled back: ~A~%" e)))with-transaction はネストして使用でき、ネストされたトランザクションは自動的にセーブポイントとして実装されます。
(with-transaction
;; 外側のトランザクション
(let ((company (make-record '<company> :name "ACME Corp")))
(save company))
;; 内側のトランザクション(セーブポイント)
(handler-case
(with-transaction
(let ((department (make-record '<department>
:name "Sales"
:company-id (ref company :id))))
(save department))
(error "Department creation failed"))
(error (e)
;; 内側のトランザクションのみロールバックされる
;; company は保存される
(format t "Department transaction rolled back~%"))))(with-transaction
;; 外側のデータを保存
(save outer-record)
(with-transaction
;; 内側のデータを保存
(save inner-record)))
;; => 両方のデータが保存される(with-transaction
(save outer-record)
(handler-case
(with-transaction
(save inner-record)
(error "Inner error"))
(error (e) nil)))
;; => outer-record のみ保存される
;; => inner-record はロールバックされる(handler-case
(with-transaction
(save outer-record)
(with-transaction
(save inner-record))
(error "Outer error"))
(error (e) nil))
;; => すべてロールバックされる(defun create-user-with-profile (name email bio)
"ユーザーとプロフィールをトランザクション内で作成"
(with-transaction
(let* ((user (make-record '<user> :name name :email email))
(saved-user (save user)))
(when saved-user
(let ((profile (make-record '<profile>
:user-id (ref saved-user :id)
:bio bio)))
(save profile))))))
;; 使用例
(create-user-with-profile "Taro Yamada" "taro@example.com" "Hello, World!")
;; => ユーザーとプロフィールの両方が作成される
;; => どちらかが失敗すると、両方ロールバックされるトランザクション内では、通常通りクエリを実行できます。
(with-transaction
;; データの作成
(let ((user (make-record '<user> :name "Taro")))
(save user))
;; 作成したデータの検索
(let ((users (execute-query
(query <user>
:as :user
:where (:= (:user :name) :name))
'(:name "Taro"))))
(format t "Found ~A users~%" (length users))))トランザクションを明示的にロールバックしたい場合は、エラーを発生させます。
(handler-case
(with-transaction
(save user)
;; 何か条件によってロールバックしたい場合
(when (some-condition-p)
(error "Explicit rollback"))
(save profile))
(error (e)
(format t "Transaction aborted: ~A~%" e)))- ネストの深さ: 過度なネストは避け、必要な場合のみ使用してください
- 長時間のトランザクション: トランザクションは短時間で完了するように設計してください
- デッドロック: 複数のトランザクションが同じリソースにアクセスする場合、デッドロックに注意してください
- コネクション管理:
with-transactionはスレッドローカルなコネクションを使用します。同じスレッド内では同じコネクションが再利用されます
(defvar *user* (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) 1))
'())))
;; 削除
(destroy *user*)
;; 削除後は frozen-p が T になり、変更できなくなる
(frozen-p *user*) ; => T
(save *user*) ; => NIL (frozen なので保存できない)(defvar *users* (execute-query
(query <user>
:as :user
:where (:= (:user :is-active) NIL))
'()))
;; 一括削除
(destroy *users*):cascade T を指定すると、:has-many で定義された関連レコードも削除されます。
;; 会社を削除すると、その会社に属する部署もすべて削除される
(destroy *company* :cascade T)
;; 部署を削除すると、その部署に属する従業員もすべて削除される
(destroy *department* :cascade T)削除処理もトランザクション内で実行できます。
(with-transaction
;; 複数のレコードを削除
(destroy user1)
(destroy user2)
;; どちらかが失敗すると、両方ロールバックされる
)(defvar *user* (make-record '<user> :name "Test"))
(has-dirty-p *user*) ; => T (新規作成時は dirty)
(save *user*)
(has-dirty-p *user*) ; => NIL (保存後は dirty flag がクリアされる)
(setf (ref *user* :name) "New Name")
(has-dirty-p *user*) ; => T (変更があったので dirty)(clear-error *user*)
(has-error-p *user*) ; => NILdirty flag を手動でクリアします。変更を破棄したい場合に便利です。
(setf (ref *user* :name) "New Name")
(has-dirty-p *user*) ; => T
(clear-dirty-flag *user*)
(has-dirty-p *user*) ; => NILデバッグ目的で Model インスタンスのすべてのカラム値を表示します。
(show-model-data *user*)
;; 出力例:
;; ID: 1
;; NAME: "Taro Yamada"
;; EMAIL: "taro@example.com"
;; AGE: 30
;; ...Model のすべてのカラム定義を表示します。
(show-model-columns *user*)
;; カラム名、型、変換関数などの情報が出力されますModel 定義のデバッグ用に、テーブル情報レジストリ全体を表示します。
(debug-table-information)
;; 登録されているすべての Model のテーブル情報が出力されますModel インスタンスは自動的に JSON に変換できます。
(jonathan:to-json *user*)
;; => "{\"ID\":1,\"NAME\":\"Taro Yamada\",\"EMAIL\":\"taro@example.com\",...}"(defun get-users-page (page per-page)
(let ((offset (* (1- page) per-page)))
(execute-query
(query <user>
:as :user
:order-by ((:user :created-at :desc))
:limit per-page
:offset offset)
'())))
;; 使用例
(get-users-page 1 20) ; 1ページ目(1-20件)
(get-users-page 2 20) ; 2ページ目(21-40件)(defun search-users-by-name (keyword)
(execute-query
(query <user>
:as :user
:where (:like (:user :name) :keyword)
:order-by ((:user :name)))
(list :keyword (format nil "%~A%" keyword))))大量のデータを処理する場合は、トランザクションを適切に使用します。
(defun import-users (user-data-list)
"ユーザーデータを一括インポート"
(with-transaction
(loop for user-data in user-data-list
do (let ((user (make-record '<user>
:name (getf user-data :name)
:email (getf user-data :email))))
(unless (save user)
(error "Failed to save user: ~A" (getf user-data :name)))))))
;; 使用例
(import-users '((:name "User1" :email "user1@example.com")
(:name "User2" :email "user2@example.com")
(:name "User3" :email "user3@example.com")))
;; => すべて成功するか、すべて失敗する(defun update-user-if-not-modified (user-id new-name expected-version)
"バージョンが一致する場合のみユーザーを更新"
(with-transaction
(let ((user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
(list :id user-id)))))
(when user
(if (= (ref user :lock-version) expected-version)
(progn
(setf (ref user :name) new-name)
(save user)
T)
(progn
(format t "User was modified by another process~%")
NIL))))))バルク操作は大量データを効率的に処理するための機能です。オンライン処理ではなく、バッチ処理やバックグラウンドジョブでの使用を想定しています。
大量のSELECT結果をバッチ単位で効率的に処理します。全レコードをメモリに載せずにストリーミング処理を行います。
- データベース固有の最適化
- PostgreSQL: サーバーサイドカーソル(トランザクション自動管理)
- MySQL: ストリーミング結果セット
- SQLite3: LIMIT/OFFSET ページネーション
- バッチサイズの指定が可能(デフォルト: 1000)
queryマクロと cl-batis の両方に対応
;; query マクロを使用
(with-query-cursor (user-rows
(query <user> :as :u :order-by ((:u :id)))
nil
:batch-size 500)
(dolist (row user-rows)
(format t "User: ~A, Email: ~A~%"
(getf row :name)
(getf row :email))))(with-query-cursor (user-rows
(query <user>
:as :u
:where (:> (:u :age) :age)
:order-by ((:u :id)))
'(:age 20)
:batch-size 1000)
(dolist (row user-rows)
(process-user-row row)))(with-query-cursor (blog-rows
(query <blog>
:as :blog
:joins ((:left-join :account))
:order-by ((:blog :id)))
nil
:batch-size 500)
(dolist (row blog-rows)
(format t "Blog: ~A, Author: ~A~%"
(getf row :|BLOG.TITLE|)
(getf row :|ACCOUNT.NAME|))))(select ("SELECT * FROM users WHERE age > :age ORDER BY id")
(defsql find-users-by-age-greater-than (age)))
(with-query-cursor (user-rows
find-users-by-age-greater-than
'(:age 20)
:batch-size 500)
(dolist (row user-rows)
(format t "User: ~A, Age: ~A~%"
(getf row :name)
(getf row :age))));; PostgreSQL: トランザクションを明示的に開始(推奨)
(with-transaction
(with-query-cursor (user-rows
(query <user> :as :u :order-by ((:u :id)))
nil
:batch-size 500)
(dolist (row user-rows)
(process-user-row row)))
;; 他の処理も同じトランザクション内で実行可能
(save summary-data))
;; 外部コネクションを使用
(with-db-connection (conn)
(with-transaction-using-connection conn
(with-query-cursor (user-rows
(query <user> :as :u :order-by ((:u :id)))
nil
:batch-size 500
:connection conn)
(dolist (row user-rows)
(process-user-row row)))))(show-query-sql (query <user>
:as :u
:where (:and (:= (:u :status) :status)
(:> (:u :age) :age))
:order-by ((:u :id)))
'(:status "active" :age 20))
;; => "SELECT U.ID as \"U.ID\", U.NAME as \"U.NAME\", ...
;; WHERE U.STATUS = 'active' AND U.AGE > 20 ORDER BY U.ID"-
ORDER BY の指定は必須: すべてのデータベースで ORDER BY を指定することを強く推奨します。ORDER BY がない場合、バッチ間で結果の順序が保証されず、重複や欠落が発生する可能性があります。
-
データベース別の注意点:
- PostgreSQL: カーソルを使用するため、自動的にトランザクションが開始されます
- MySQL: ストリーミング結果セット使用中は接続が占有されます
- SQLite3: OFFSET が大きくなると性能が劣化します
-
バッチサイズの選択: デフォルトは 1000 行です。メモリ使用量とデータベース往復回数のトレードオフを考慮して調整してください(推奨範囲: 100〜5000 行)
大量データの挿入を効率的に行う2つの関数を提供します。
各モデルインスタンスに対して insert1 を実行し、INSERT後の id, created_at, updated_at が各インスタンスに設定されます。
(let* ((users (list (make-record '<user> :name "Alice" :age 30 :email "alice@example.com")
(make-record '<user> :name "Bob" :age 25 :email "bob@example.com")
(make-record '<user> :name "Charlie" :age 35 :email "charlie@example.com")))
(inserted-users (insert-all users)))
;; 各インスタンスの id が設定される
(dolist (user inserted-users)
(format t "Inserted ID: ~A, Name: ~A~%"
(slot-value user 'id)
(ref user :name))))
;; 出力:
;; Inserted ID: 1, Name: Alice
;; Inserted ID: 2, Name: Bob
;; Inserted ID: 3, Name: Charlieトランザクション内で実行:
(with-transaction (conn)
(let ((users (list (make-record '<user> :name "Alice" :age 30)
(make-record '<user> :name "Bob" :age 25))))
(insert-all users :connection conn)))バルク INSERT を実行し、挿入件数のみを返します。plist または model のリストを受け取ります。
plist を使用:
(insert-bulk '<user>
'(:name :age :email)
'((:name "Alice" :age 30 :email "alice@example.com")
(:name "Bob" :age 25 :email "bob@example.com")
(:name "Charlie" :age 35 :email "charlie@example.com"))
:batch-size 100)
;; => 3model インスタンスを使用:
(let ((users (list (make-record '<user> :name "Alice" :age 30 :email "alice@example.com")
(make-record '<user> :name "Bob" :age 25 :email "bob@example.com")
(make-record '<user> :name "Charlie" :age 35 :email "charlie@example.com"))))
(insert-bulk '<user>
'(:name :age :email)
users
:batch-size 100))
;; => 3タイムスタンプの自動設定:
;; created_at, updated_at は未指定でも自動的に設定される
(insert-bulk '<user>
'(:name :age) ; created_at, updated_at は自動追加
'((:name "Alice" :age 30)
(:name "Bob" :age 25))
:batch-size 100)トランザクション内で実行:
(with-transaction (conn)
(insert-bulk '<user>
'(:name :age :email)
large-data-list
:connection conn
:use-transaction nil)) ; 外部でトランザクション管理| 関数 | 用途 | 戻り値 | ID書き戻し | 速度 |
|---|---|---|---|---|
insert-all |
INSERT後のモデルを使用 | モデルのリスト | ✅ | 遅い |
insert-bulk |
CSVインポート等の高速挿入 | 挿入数(整数) | ❌ | 速い |
大量データの更新を効率的に行う2つの関数を提供します。
各モデルインスタンスに対して update1 を実行します。dirty-flag が設定されたカラムのみが更新されます。
(let* ((users (list user1 user2 user3)))
;; 各ユーザーの情報を変更
(setf (ref user1 :name) "Alice Updated")
(setf (ref user2 :age) 26)
(setf (ref user3 :email) "charlie-new@example.com")
;; 一括更新
(let ((updated-users (update-all users)))
(dolist (user updated-users)
(format t "Updated ID: ~A, updated-at: ~A~%"
(slot-value user 'id)
(ref user :updated-at)))))異なるmodelクラスの混在:
(let* ((user (make-record '<user> :name "Alice" :age 30))
(post (make-record '<post> :title "Hello" :content "World"))
(inserted-user (first (insert-all (list user))))
(inserted-post (first (insert-all (list post)))))
;; 両方のインスタンスを変更
(setf (ref inserted-user :age) 31)
(setf (ref inserted-post :title) "Hello World")
;; 一括更新(user は users テーブル、post は posts テーブルに UPDATE される)
(let ((updated (update-all (list inserted-user inserted-post))))
(format t "Updated ~A records~%" (length updated))))
;; => Updated 2 recordsトランザクション内で実行:
(with-transaction (conn)
(let ((users (list user1 user2)))
(update-all users :connection conn)))バルク UPDATE を実行し、更新件数を返します。更新するカラムを明示的に指定します。
(let ((users (list user1 user2 user3)))
;; 各ユーザーの情報を変更
(setf (ref user1 :name) "Alice Updated")
(setf (ref user2 :age) 26)
(setf (ref user3 :email) "charlie-new@example.com")
;; バルク更新(カラムを指定)
(let ((count (update-bulk '<user> '(:name :age :email) users)))
(format t "Updated ~A records~%" count)))
;; => Updated 3 recordsupdated_at は自動更新:
;; updated_at は未指定でも自動的に更新される
(update-bulk '<user> '(:name :age) users)
;; => name, age, updated_at が更新されるid と created_at は除外:
;; id と created_at は指定しても無視される
(update-bulk '<user> '(:id :name :created-at :age) users)
;; => name, age, updated_at が更新される(id と created_at は除外)型不一致時の動作指定:
;; デフォルトは :error - 異なる型があればエラー
(update-bulk '<user>
'(:name :age)
(list user1 post1 user2)) ; post1 は <post> インスタンス
;; => type-mismatch-error が発生
;; :skip - 異なる型はスキップ
(update-bulk '<user>
'(:name :age)
(list user1 post1 user2)
:on-type-mismatch :skip)
;; => 2 (user1 と user2 のみ更新、post1 はスキップ)| 関数 | 用途 | 戻り値 | カラム指定 | 速度 |
|---|---|---|---|---|
update-all |
dirty-flagに基づく更新 | モデルのリスト | dirty-flag | 遅い |
update-bulk |
カラムを明示的に指定して更新 | 更新数(整数) | 明示的 | 中速 |
大量データの削除を効率的に行う2つの関数を提供します。
各モデルインスタンスに対して destroy を実行します。カスケード削除に対応しています。
;; 基本的な削除
(let ((users (list user1 user2 user3)))
(let ((deleted-users (delete-all users)))
(format t "Deleted ~A users~%" (length deleted-users))))
;; => Deleted 3 usersカスケード削除:
;; カスケード削除を有効にする
(let ((users (list user1 user2)))
(delete-all users :cascade t))
;; => user1, user2 とそれらに関連するレコードを削除トランザクション内で実行:
(with-transaction (conn)
(let ((users (list user1 user2)))
(delete-all users :connection conn)))バルク DELETE を実行し、削除件数を返します。IN 句を使用した最適化版で実装されています。
;; 基本的な使い方
(let ((users (list user1 user2 user3)))
(let ((count (delete-bulk '<user> users)))
(format t "Deleted ~A records~%" count)))
;; => Deleted 3 recordsカスタムバッチサイズ:
(delete-bulk '<user> large-user-list :batch-size 50)型不一致時の動作指定:
;; デフォルトは :error - 異なる型があればエラー
(delete-bulk '<user> (list user1 product1 user2))
;; => type-mismatch-error が発生
;; :skip - 異なる型はスキップ
(delete-bulk '<user> (list user1 product1 user2) :on-type-mismatch :skip)
;; => 2 (user1 と user2 のみ削除、product1 はスキップ)トランザクション内で実行:
(with-transaction (conn)
(delete-bulk '<user> users
:connection conn
:use-transaction nil)) ; 外部でトランザクション管理| 関数 | 用途 | 戻り値 | カスケード | 速度 |
|---|---|---|---|---|
delete-all |
カスケード削除が必要 | モデルのリスト | ✅ | 遅い |
delete-bulk |
高速に削除 | 削除数(整数) | ❌ | 速い |
型不一致が発生した場合のエラーです。
(handler-case
(update-bulk '<user>
'(:name :age)
(list user1 post1 user2))
(type-mismatch-error (e)
(format t "Type mismatch: ~A~%" e))):error: エラーを発生させる(デフォルト):skip: スキップして続行
;; エラーを発生させずにスキップ
(let ((count (update-bulk '<user>
'(:name :age)
(list user1 post1 user2)
:on-type-mismatch :skip)))
(format t "Updated ~A records~%" count))
;; => Updated 2 recordsすべてのデータベースで ORDER BY を指定することを強く推奨します。
;; 推奨
(with-query-cursor (rows
(query <user> :as :u :order-by ((:u :id)))
nil)
(dolist (row rows)
(process-row row)))
;; 非推奨: ORDER BY なし(結果の順序が保証されない)
(with-query-cursor (rows
(query <user> :as :u)
nil)
(dolist (row rows)
(process-row row)))メモリ使用量とデータベース往復回数のトレードオフを考慮します。
;; 推奨範囲: 100〜5000 行
(with-query-cursor (rows
(query <user> :as :u :order-by ((:u :id)))
nil
:batch-size 1000) ; デフォルト
...)一貫性が必要な場合は with-transaction で囲みます。
(with-transaction
(with-query-cursor (rows
(query <user> :as :u :order-by ((:u :id)))
nil)
(dolist (row rows)
(process-row row)))
;; 他の処理も同じトランザクション内で
(save summary-data))バルク操作は長時間接続を占有したり、トランザクションを維持したりするため、オンライン処理での使用は非推奨です。バッチ処理やバックグラウンドジョブでの使用を想定しています。
;; 非推奨: オンライン処理での使用
(defun show-all-users ()
(with-query-cursor (rows
(query <user> :as :u :order-by ((:u :id)))
nil
:batch-size 10000) ; バッチサイズが大きすぎる
(render-html rows))) ; すべての結果を画面に表示
;; 推奨: バッチ処理での使用
(defun batch-process-users ()
(with-query-cursor (rows
(query <user> :as :u :order-by ((:u :id)))
nil
:batch-size 1000)
(dolist (row rows)
(process-user-row row))))clails では、クエリのパース結果をキャッシュすることで、同じクエリを繰り返し実行する際のパフォーマンスを向上させています。
<query> クラスは、クエリ全体(SELECT、JOIN、WHERE、ORDER BY、LIMIT、OFFSET)のパース結果を内部的にキャッシュします。これにより、execute-query でのクエリ実行時に、初回のみパース処理が行われ、2回目以降はキャッシュされた結果が使用されます。
クエリは初回実行時に自動的にキャッシュされます。ユーザーが明示的にキャッシュを管理する必要はありません。
;; クエリを定義
(defvar *user-query*
(query <user>
:as :user
:where (:= (:user :is-active) :active)
:order-by ((:user :created-at :desc))))
;; 初回実行: パース処理が実行され、結果がキャッシュされる
(execute-query *user-query* '(:active t))
;; 2回目以降: キャッシュされた結果が使用される(パース処理はスキップ)
(execute-query *user-query* '(:active t))
(execute-query *user-query* '(:active nil))クエリ定義を変更すると、キャッシュは自動的に無効化され、次回実行時に再度パース処理が行われます。
;; query-builder で動的にクエリを構築
(defvar *q* (query-builder '<user> :as :user))
(set-columns *q* '((user :id :name)))
(set-where *q* '(:= (:user :status) :status))
;; 初回実行: キャッシュ生成
(execute-query *q* '(:status "active"))
;; クエリ定義を変更: キャッシュが自動的に無効化される
(set-where *q* '(:> (:user :age) :min-age))
;; 次回実行: 新しいクエリでパース処理が実行され、キャッシュされる
(execute-query *q* '(:min-age 20))以下のクエリコンポーネントのパース結果がキャッシュされます:
- SELECT句: カラム選択のパース結果
- JOIN句: JOIN条件のパース結果
- WHERE句: 条件式のパース結果(動的IN句のテンプレートを含む)
- ORDER BY句: ソート条件のパース結果
- LIMIT/OFFSET句: ページネーションのパース結果
動的IN句(パラメータでリストを渡す場合)では、IN句のテンプレートがキャッシュされ、実際の展開は実行時に行われます。
(defvar *blog-query*
(query <blog>
:as :blog
:where (:in (:blog :id) :blog-ids)))
;; 初回実行: IN句のテンプレートがキャッシュされる
(execute-query *blog-query* '(:blog-ids (1 2 3)))
;; 2回目実行: キャッシュされたテンプレートから動的に展開される
(execute-query *blog-query* '(:blog-ids (4 5 6 7 8)))
;; 異なる数の値でも正しく展開される
(execute-query *blog-query* '(:blog-ids (10)))キャッシュ機能により、以下のような状況でパフォーマンスが向上します:
(defvar *page-query*
(query <user>
:as :user
:where (:= (:user :status) :status)
:order-by ((:user :created-at :desc))
:limit 20
:offset :offset))
;; 各ページの取得でパース処理がスキップされる
(execute-query *page-query* '(:status "active" :offset 0)) ; ページ1
(execute-query *page-query* '(:status "active" :offset 20)) ; ページ2
(execute-query *page-query* '(:status "active" :offset 40)) ; ページ3(defvar *batch-query*
(query <task>
:as :task
:where (:= (:task :status) :status)))
;; 大量のクエリ実行で効果を発揮
(loop for i from 1 to 1000
do (execute-query *batch-query* '(:status "pending")))複雑なJOINやWHERE句を持つクエリでは、パース処理のコストが大きいため、キャッシュの効果が顕著になります。
(defvar *complex-query*
(query <employee>
:as :emp
:joins ((:inner-join :department)
(:inner-join :company :through :department))
:where (:and (:= (:dept :name) :dept-name)
(:>= (:emp :salary) :min-salary)
(:in (:company :industry) :industries))
:order-by ((:emp :salary :desc))))
;; 複雑なクエリでも2回目以降はパース処理がスキップされる
(execute-query *complex-query*
'(:dept-name "Sales"
:min-salary 50000
:industries ("Tech" "Finance")))- クエリの再利用: 同じクエリを複数回実行する場合は、クエリオブジェクトを変数に保存して再利用してください
- メモリ使用量: キャッシュはクエリオブジェクトのライフサイクルと同期して管理されます。不要になったクエリオブジェクトは適切に破棄してください
- スレッドセーフティ: 現在の実装では、
<query>インスタンスを複数のスレッドから同時に使用することは想定していません
;; 推奨: クエリオブジェクトを再利用
(defvar *user-search-query*
(query <user>
:as :user
:where (:like (:user :name) :keyword)))
(defun search-users (keyword)
(execute-query *user-search-query*
(list :keyword (format nil "%~A%" keyword))))
;; 何度呼び出しても効率的
(search-users "Alice")
(search-users "Bob")
(search-users "Charlie");; 非推奨: 毎回クエリを生成(キャッシュの恩恵を受けられない)
(defun search-users-bad (keyword)
(execute-query
(query <user>
:as :user
:where (:like (:user :name) :keyword))
(list :keyword (format nil "%~A%" keyword))))clails の Model は以下の特徴を持ちます。
- 明示的な設計: カラム情報は自動取得され、アクセスは専用の関数を使用
- 効率的な更新: dirty flag により、変更されたカラムのみを更新
- 柔軟なクエリ: DSL によるクエリ構築で、JOIN や複雑な条件も記述可能
- 関連の管理:
:belongs-toと:has-manyによる親子関係の定義 - 安全性: バリデーション、楽観的ロック・悲観的ロック、トランザクションのサポート
- トランザクション管理:
with-transactionによる簡単なトランザクション制御とネストしたトランザクション(セーブポイント)のサポート - ネイティブクエリ: cl-batis を使用した柔軟な SQL クエリの実行
- バルク操作: 大量データを効率的に処理するためのストリーミング処理とバッチ操作のサポート
- クエリキャッシュ: クエリのパース結果を自動的にキャッシュし、同じクエリの繰り返し実行時のパフォーマンスを向上
詳細な API リファレンスについては、各関数の docstring を参照してください。