業務の中で「DSL」「DDL」「DML」という用語に出会い、なんとなく聞いたことはあるけれど、正確には説明できないなと感じました。せっかくの機会なので改めて整理してみたところ、普段当たり前のように使っている ORM との関係まで一本の線で繋がり、「そういうことだったのか」と思いました。
この記事では、DSL / DDL / DML という用語と、普段書いている ORM コードがどう関係しているのかを整理します。初学者〜若手エンジニアで、ORM は毎日触っているけれど用語の関係性は言語化したことがない、という方向けの内容です。
普段書いている ORM コード、裏で何が起きてる?
例えば Ruby(ActiveRecord)でこんなコードを書いたとします。
User.where(active: true).order(:created_at).limit(10)
これは Ruby のコードですが、最終的に DB に届くときには次のような SQL になっています。
SELECT * FROM users WHERE active = true ORDER BY created_at LIMIT 10;
つまり、私たちは Ruby を書いているつもりでも、裏では ORM が SQL を組み立てて DB に送ってくれています。ここではまず「ORM が裏で SQL を発行している」という事実だけ押さえておきます。次の章で、その SQL に種類があることを見ていきます。
その SQL には種類がある(DDL / DML)
SQL は用途によっていくつかのカテゴリに分かれます。よく出てくるのが DDL と DML です。
DDL(Data Definition Language)
テーブルなどの「構造」を定義するための SQL です。
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name VARCHAR(255),
active BOOLEAN
);
ALTER TABLE users ADD COLUMN email VARCHAR(255);
「テーブルを作る」「カラムを足す」「テーブルを消す」といった、入れ物そのものを操作するイメージです。
DML(Data Manipulation Language)
テーブルの中の「データ」を操作するための SQL です。
SELECT * FROM users WHERE active = true;
INSERT INTO users (name, active) VALUES ('Alice', true);
UPDATE users SET active = false WHERE id = 1;
DELETE FROM users WHERE id = 1;
「データを取り出す」「データを入れる」「データを更新する」「データを消す」といった、入れ物の中身を操作するイメージです。
ORM での対応
ここで普段の ORM の使い方を思い出すと、ちょうどこの DDL / DML に綺麗に対応していることに気づきます。
- マイグレーション(
rails db:migrateやpython manage.py migrate)→ 裏で発行されるのは DDL -
User.where(...)やUser.create(...)などのモデル操作 → 裏で発行されるのは DML
つまり「マイグレーションを書くとき」と「クエリを書くとき」で、ORM が組み立てている SQL の種類自体が違うわけです。普段は意識しないところですが、改めて並べてみるとたしかにそうだなと思いました。
なお SQL には他にも DCL(権限制御:GRANT / REVOKE)や TCL(トランザクション制御:COMMIT / ROLLBACK)といったカテゴリもあります。今回は DDL と DML に絞って整理します。
そして SQL 自体も "DSL の一種" だった
SQL は DSL(Domain-Specific Language) の一種です。DSL を日本語に直すと「特定の目的に特化したミニ言語」になります。
対になる概念として 汎用言語(General-Purpose Language) があります。これは Ruby や Python、Java のように「何でも書ける」言語のことです。ループも、関数定義も、ファイル入出力も、Web サーバも、何でも書けます。
一方、DSL は「特定の目的のためだけ」に作られた言語です。身近な例を挙げると次のようなものがあります。
- SQL:DB を操作するためだけの言語
- 正規表現:文字列のパターンマッチをするためだけの言語
- HTML / CSS:文書の構造やスタイルを表現するためだけの言語
どれも「何でもできる」言語ではなく、用途を絞ることで簡潔に書けるようになっています。SQL も同じで、「DB 操作」というドメインに特化した DSL です。SQL でループや関数定義をゴリゴリ書こうとは普通しません(拡張機能を使えば書けますが、本来の役割ではありません)。
ORM の DSL も DSL(ただし内部 DSL)
最初に出した ORM のコードをもう一度見てみます。
User.where(active: true)
これは何でしょうか。Ruby のメソッド呼び出しに見えますが、実は これも DSL です。ただし SQL とは性質が違います。
DSL は、ホスト言語との関係でさらに 2 種類に分けられます。
| 種類 | 概要 | 例 |
|---|---|---|
| 外部 DSL | ホスト言語の外にある独立した言語。専用のパーサが必要 | SQL、正規表現、HTML |
| 内部 DSL | ホスト言語の中に埋め込まれた API。見た目はホスト言語のコードそのもの | ActiveRecord の where、RSpec の describe / it
|
User.where(active: true) は、文法的には Ruby のメソッド呼び出しそのものです。けれど ActiveRecord が「DB クエリを表現するため」にわざわざ設計した API であり、Ruby というホスト言語の中に埋め込まれた「DB 操作専用のミニ言語」になっています。これが 内部 DSL です。
そう考えると、ORM が提供しているもの自体が DSL(内部 DSL)だった、ということになります。
つまり ORM は「DSL → DSL の変換器」
【あなたが書く Ruby DSL(内部 DSL)】
User.where(active: true)
.order(:created_at)
.limit(10)
↓ ORM が変換
【DB が受け取る SQL DSL(外部 DSL)】
SELECT * FROM users
WHERE active = true
ORDER BY created_at
LIMIT 10;
ORM は、内部 DSL(Ruby に埋め込まれた API) を受け取って、外部 DSL(SQL) に変換し、DB に送る変換器だと言えます。
ではなぜわざわざ 2 層を経由するのでしょうか。SQL を直接書けば一段で済むはずです。少なくとも 3 つの理由が思いつきます。
- 安全性:文字列結合で SQL を組み立てる必要がなくなり、SQL インジェクションを防ぎやすくなる
- 開発体験:ホスト言語のエディタ補完が効き、戻り値もオブジェクトとして扱える
- テスタビリティ:Ruby のオブジェクトとしてテストコードから組み立てたり検証したりできる
「内部 DSL を一度挟む」というワンクッションが、こういう恩恵を生んでいるわけです。
まとめ
用語の関係を一枚にまとめると、こんな形になります。
DSL(特定目的に特化したミニ言語の総称)
├─ 外部 DSL
│ └─ SQL
│ ├─ DDL(CREATE / ALTER / DROP …)
│ └─ DML(SELECT / INSERT / UPDATE / DELETE …)
└─ 内部 DSL
└─ ActiveRecord の where, order, limit, …
ORM の役割:内部 DSL(Ruby)→ 外部 DSL(SQL)への変換器
毎日のように書いていた User.where(...) は内部 DSL で、その裏では外部 DSL である SQL(DDL / DML)が発行されている。改めて並べてみると当たり前のことを言っているだけなのですが、自分の中ではバラバラだった用語が一本の線で繋がって、普段書いているコードの見え方が少し変わりました。
似たような違和感をお持ちの方の整理のきっかけになれば嬉しいです。