はじめに
普段はWebアプリケーションの開発をメインでやっていて、ここ2〜3年で開発時のドキュメント作成環境が結構変わって来ました。生成AIが開発プロセスに組み込まれつつあり、仕様駆動開発という考え方も注目されている中でドキュメントを運用していく環境も日々模索・変化している今日この頃ですが、ここで現状を共有しておきたいと思います。
以下の構成になる前(2年ほど前)までは、ConfluenceとGliffyというアドオンを主に使っていました。
ある時クラウド版に移行せざるを得なくなって色々問題が発生し、代わりを探してたどり着いた現状がここんな感じです。
- Obsidianを使い、markdown形式で仕様書を書く
- 図はmermaidやplantumlやdraw.ioで書く
- テーブル定義書はtblsコマンドを使って取り込む
- dataviewプラグインを使ってテーブル定義とプログラム仕様書を結びつけてCRUDマトリクスにする
- API仕様書を書くときはOpenAPI rendererのSwagger Viewerでプレビューする
余談ですが、confluenceのページをobsidian markdownにコンバートしてくれるCLIツールが存在したので、移行自体は比較的楽でした。
Obsidianのざっくり概要
Obsidianの特徴について解説しているサイトや書籍は様々あると思うので、ここではソフトウェア開発で仕様書を書くのに便利な機能性について少し挙げてみます。
※ VSCodeにMarkdown All in Oneなどを入れれば同じ事が出来たりはしますが
- ファイルにメタデータを付与できる
- frontmatterと呼ばれるyaml形式のブロックを付けて、ページのメタデータとして扱えます。これを活用して、様々な分類に使ったり、他の資料との関係性を定義したり出来ます。本記事ではCRUDマトリクスを作るのにこれを活用しています。
- リンクが張りやすく埋め込みやすい
- 他のページにリンクを張ることは良くありますが、埋め込んでしまえばリンククリックの手間も省けます。リンクだったとしてもhover時にプレビューをポップアップする事もできるので便利。
- dataviewプラグインが強力
- 本体にBases機能が追加されるまではページから情報を収集して表示するにはこれを使うしか無かったのですが、Basesも便利ですがdataviewjsほど強力ではないので、まだ手放せません。
- 図の埋め込みにも強い
- 標準でmermaid、プラグインを入れればplantumlも書け、draw.ioのファイル埋め込み・編集も可能です。
※ drawioファイルの編集は出来ますが、そのまま埋め込めないので埋め込みたい部分をsvgでエクスポートが必要という若干残念な状況ではあります。
- 標準でmermaid、プラグインを入れればplantumlも書け、draw.ioのファイル埋め込み・編集も可能です。
- データがローカルにあること
- 完全無料であること(商用でも2025年12月現在は無料)
具体的なページ例は割愛しますが、一般的なmarkdownで書ける範囲はもとより、以下の様にmermaidで簡単に図を描いたりも出来ますし、これならgithubやgitlabでもレンダリングしてくれるので良いです。
sequenceDiagram
autonumber
A->>+B:処理1
B->>+C:処理2
C-->>-B:戻る
B-->>-A:戻る
顧客に提出する必要がある場合、単ページなら標準機能でPDF出力できます。
Vault全体を1枚のPDFにまとめたい場合は、Pandocプラグインと組み合わせるのが良いでしょう。
アプリとプラグイン(と外部コマンド)
アプリとプラグインは以下の様なものを使っています。(もっと色々使いますが仕様書のVaultではこのくらい)
※ プラグインはObsidianの中で検索すれば出てくるのでリンクを張ってません
| 名前 | 種類 | 説明 |
|---|---|---|
| Obsidian | アプリ | markdownベースのドキュメントエディタ。 |
| Automatic Table Of Contents | Obsidianプラグイン | 目次を自動生成するプラグイン。 |
| Dataview | Obsidianプラグイン | Vault内のページを様々な条件で抽出し、 結果を加工して表示できるプラグイン。 |
| Diagrams | Obsidianプラグイン | draw.ioファイルをObsidian内で編集できるようにする プラグイン。 |
| Git | Obsidianプラグイン | VaultがGitリポジトリである場合にgitコマンドを GUIから実行できるようにするプラグイン。 |
| Note Refactor | Obsidianプラグイン | ページ内の範囲を別ページに分割したり 複数ページを1つにマージしたりするプラグイン。 |
| Paste URL into Selection | Obsidianプラグイン | クリップボードにURLがあるとき、選択範囲にペースト するとmarkdownリンクにしてくれるプラグイン |
| Smart Composer | Obsidianプラグイン | AIチャットパネルを追加するプラグイン。 |
| Tag Wrangler | Obsidianプラグイン | タグを整理しやすくするプラグイン。 |
| draw.io.app | アプリ | draw.ioのデスクトップアプリ。 |
| tbls | CLIツール | 稼働しているDBに接続してテーブル定義を収集し markdown形式で出力するコマンドラインツール。 |
tblsコマンドでテーブル定義書を作る
tblsコマンドを使うとテーブルが1ページのmdファイルになります。使い方は配布元であるリンク先を見てもらうのが一番なのでここでは割愛します。
テーブル一覧のmdファイルも、ER図のsvgファイルもできるのでとても便利です。
また、各テーブル定義書ページと一覧ページのそれぞれにテンプレートファイルを指定できるので、後述のdataviewスクリプトを埋め込んでおくことも出来、開発のワークフローに組み込みやすいです。
CRUDマトリクスの作り方
どの機能やAPIからどのテーブルにアクセス(CREATE/READ/UPDATE/DELETE)があるかが分かると、システムの改修が積み重なる都度、影響範囲を把握しやすくなります。
ページ側の準備
以下の様なfrontmatterを仕様書ページに記載しておきます。
tags:
- db/{テーブル名}/read
- db/{テーブル名}/write
dataviewスクリプト
以下のdataviewjsスクリプトをページに書くと、以下の様な表が出来ます。
スクリプト
※ Gemini3.0出力(動作確認はしましたがご利用は自己責任でm(_ _)m)
/* dataviewjs */
const pages = dv.pages('"specs"') // ★ ここを実際のパスに書き換えてください
.where(p => p.tags && p.tags.some(t => t.startsWith("db/")));
const tables = new Set();
const matrix = new Map();
// 1. データ収集とマトリクスの初期化
for (const page of pages) {
const programName = page.file.name;
if (page.tags) {
for (const tag of page.tags) {
if (tag.startsWith("db/")) {
// 例: 'db/user_table/read' -> ['db', 'user_table', 'read']
const parts = tag.split('/');
if (parts.length === 3) {
const tableName = parts[1];
const accessType = parts[2].toUpperCase(); // READ, WRITE, UPDATE, DELETE
tables.add(tableName);
if (!matrix.has(tableName)) {
matrix.set(tableName, new Map());
}
if (matrix.get(tableName).has(programName)) {
// 既存のアクセスタイプを結合 (例: 'R' + 'U' -> 'RU')
let existingAccess = matrix.get(tableName).get(programName);
// CRUDの優先順位と重複排除を考慮して結合
if (accessType === 'READ' && !existingAccess.includes('R')) existingAccess += 'R';
else if (accessType === 'WRITE' && !existingAccess.includes('C')) existingAccess += 'C'; // WRITEをCと見なす
else if (accessType === 'UPDATE' && !existingAccess.includes('U')) existingAccess += 'U';
else if (accessType === 'DELETE' && !existingAccess.includes('D')) existingAccess += 'D';
// C/R/U/D の順にソートして表示
const sortedAccess = ['C', 'R', 'U', 'D']
.filter(char => existingAccess.includes(char))
.join('');
matrix.get(tableName).set(programName, sortedAccess);
} else {
// 新しいアクセスタイプを設定
let accessChar = '';
if (accessType === 'READ') accessChar = 'R';
else if (accessType === 'WRITE') accessChar = 'C'; // WRITEはCREATEと見なすことが多い
else if (accessType === 'UPDATE') accessChar = 'U';
else if (accessType === 'DELETE') accessChar = 'D';
if (accessChar) {
matrix.get(tableName).set(programName, accessChar);
}
}
}
}
}
}
}
// テーブル名とプログラム名のリストをソート
const sortedTables = Array.from(tables).sort();
const programNames = Array.from(new Set(pages.map(p => p.file.name))).sort();
// 2. マトリクス(テーブル)の生成
if (sortedTables.length === 0) {
dv.el("p", "対象となるプログラム仕様書が見つかりませんでした。パスを確認してください。");
} else {
const header = ["**テーブル名**", ...programNames.map(name => `**[[${name}|${name}]]**`)];
const rows = [];
for (const tableName of sortedTables) {
const row = [
`**[[${tableName}]]**` // テーブル定義書へのリンク (ファイル名がテーブル物理名なので)
];
for (const programName of programNames) {
const access = matrix.has(tableName) && matrix.get(tableName).has(programName)
? matrix.get(tableName).get(programName)
: ""; // アクセスがない場合は空欄
row.push(access);
}
rows.push(row);
}
dv.table(header, rows);
}
終わりに
開発にAIがどんどん入り込んできているので、このような工夫が陳腐化するのも時間の問題だろうなという気はしています。
とはいえ、人間がシステムを理解していることが大前提だと思いますので、人間のための情報整理というのは基本無くならないはずと信じて、ドキュメント整備の環境作りはこれからも改善し続けるつもりです。
引き続き、以下の様なことを進めていきたいと考えてます。
- pandocや静的サイトジェネレータで、CRUD含めた全体を静的HTMLサイトとしてpublishする
- AstroのStarlightを使ってみたのですが、何故かtblsのテーブル定義書同士のリンクが上手く繋がらず一旦断念。VitePressを試そうと思ってます。
- これが出来ると、AIに食わせる情報として使いやすくなるかも、と想像……
以上

