
Orchestrating Api Implementation
- 10 installs
- 3 repo stars
- Updated June 5, 2026
- xtone/ai_development_tools
Helps with backend & apis tasks.
About
orchestrating-api-implementation is a Claude Code skill for backend & apis. It helps solo builders move faster with AI-assisted development.
- orchestrating-api-implementation
- Backend & APIs
- AI-coding skill
Orchestrating Api Implementation by the numbers
- 10 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,590 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xtone/ai_development_tools --skill orchestrating-api-implementationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 5, 2026 |
| Repository | xtone/ai_development_tools ↗ |
What it does
Helps with backend & apis tasks.
Files
概要
このスキルは、JSONで定義されたAPI仕様に基づいて、Ruby on Rails + PostgreSQLでAPIと管理画面を実装します。
このスキルを使用するタイミング
Claudeは以下の状況でこのスキルを使用します:
- ユーザーがJSON形式のAPI仕様を提供し、Rails APIの実装を依頼した場合
- microCMSなどのHeadless CMSの仕様からRails APIを生成する場合
- 既存のJSON仕様ファイル(app.jsonなど)を参照してAPI実装を依頼された場合
- 「APIを実装して」「管理画面を作成して」といったリクエストでJSON仕様が存在する場合
技術スタック
| 項目 | 技術 |
|---|---|
| 言語 | Ruby 3.4 |
| フレームワーク | Ruby on Rails 8.1 |
| データベース | PostgreSQL 18 |
| ORM | Active Record |
ステップ
各ステップの詳細は steps/ ディレクトリ内のファイルを参照してください。
1. アプリケーション仕様を確認する
詳細: @steps/01_check_specification.md
Claudeは、JSON仕様ファイルを読み込み、モデル構造・フィールド定義・リレーションを把握します。 仕様形式: @references/01_json_specification.md
2. 技術スタックを決定する
詳細: @steps/02_select_tech_stack.md
Claudeは、環境確認と管理画面の方式(ActiveAdmin / Administrate / Hotwire)を決定します。
3. ユースケースを洗い出す
詳細: @steps/03_define_usecases.md
Claudeは、JSON仕様のアクター・ユースケースを確認し、各モデルのCRUD操作、ページネーション、フィルタリング、ソート、リレーション取得方法を定義します。
4. OpenAPI定義を作成する
詳細: @steps/04_define_openapi.md
Claudeは、ユースケースとモデル定義に基づいて、APIの仕様をOpenAPI 3.1形式で定義します。
5. DBスキーマを設計する
詳細: @steps/05_design_db_schema.md
Claudeは、JSON仕様の型をPostgreSQLの型にマッピングし、テーブル設計を行います。
6. SQLとインデックスを定義する
詳細: @steps/06_define_sql_and_indexes.md
Claudeは、ユースケースで実行されるSQLを洗い出し、通常インデックスと全文検索インデックス(GIN + tsvector)を定義します。
7. プロジェクトを初期化する
詳細: @steps/07_initialize_project.md
Claudeは、Rails 8.1プロジェクトを作成し、必要なGemと設定をセットアップします。
8. DBマイグレーションを実装する
詳細: @steps/08_implement_migration.md
Claudeは、設計に基づいてマイグレーションファイルを作成・実行します。
9. ORマッピングを実装する
詳細: @steps/09_implement_orm.md
Claudeは、Active Recordモデルにリレーション、スコープ、クエリメソッドを実装します。
10. バリデーションを実装する
詳細: @steps/10_implement_validation.md
Claudeは、JSON仕様のvalidation設定に基づいてActive Recordバリデーションを実装します。
11. APIエンドポイントを実装する
詳細: @steps/11_implement_api_endpoints.md
Claudeは、RESTful APIエンドポイント(CRUD、ページネーション、フィルタリング、ソート)を実装します。
12. APIの動作確認を行う
詳細: @steps/12_verify_api.md
Claudeは、curlとRSpecでAPIの動作を検証し、N+1問題がないことを確認します。
13. 管理画面を実装する
Claudeは、選択した方式で管理画面を実装します。方式に応じて以下のファイルを参照してください:
- ActiveAdmin: @steps/13a_admin_activeadmin.md
- Administrate: @steps/13b_admin_administrate.md
- Hotwire: @/backend_development/skills/implementing-hotwire-admin/SKILL.md (独立スキルを使用)
- 概要とクイックリファレンス: @steps/13c_admin_hotwire.md
- 共通設定・トラブルシューティング: @steps/13d_admin_common.md
Note: Hotwire管理画面を実装する場合は、E2Eテスト設計・実装も含む包括的な implementing-hotwire-admin スキルを使用してください。14. API Playgroundを実装する(オプション)
詳細: @steps/14_implement_api_playground.md
Claudeは、OpenAPI定義を活用してSwagger UIまたはカスタムPlaygroundを実装し、APIを対話的にテストできる環境を構築します。
ファイル構成
implementing-rails-api/
├── SKILL.md # このファイル
├── references/
│ └── 01_json_specification.md # JSON仕様の定義
└── steps/
├── 01_check_specification.md
├── 02_select_tech_stack.md
├── 03_define_usecases.md
├── 04_define_openapi.md
├── 05_design_db_schema.md
├── 06_define_sql_and_indexes.md
├── 07_initialize_project.md
├── 08_implement_migration.md
├── 09_implement_orm.md
├── 10_implement_validation.md
├── 11_implement_api_endpoints.md
├── 12_verify_api.md
├── 13a_admin_activeadmin.md
├── 13b_admin_administrate.md
├── 13c_admin_hotwire.md
├── 13d_admin_common.md
└── 14_implement_api_playground.md{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApiProject",
"type": "object",
"required": [
"projectName",
"version",
"models"
],
"properties": {
"projectName": {
"type": "string",
"description": "Project name"
},
"version": {
"type": "string",
"description": "Project version (e.g., '1.0.0')"
},
"authConfig": {
"$ref": "#/definitions/AuthConfig"
},
"roles": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of global roles"
},
"customTypes": {
"type": "array",
"items": {
"$ref": "#/definitions/CustomType"
},
"description": "List of custom type definitions"
},
"models": {
"type": "array",
"items": {
"$ref": "#/definitions/Model"
},
"description": "List of data models"
},
"actors": {
"type": "array",
"items": {
"$ref": "#/definitions/Actor"
},
"description": "List of actors"
},
"useCases": {
"type": "array",
"items": {
"$ref": "#/definitions/UseCase"
},
"description": "List of use cases"
}
},
"definitions": {
"AuthConfig": {
"type": "object",
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether user management is enabled"
},
"userModelName": {
"type": "string",
"default": "User",
"description": "Name of the model used for user information"
},
"authFields": {
"$ref": "#/definitions/AuthFields"
}
}
},
"AuthFields": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"email": {
"type": "string"
}
}
},
"CustomType": {
"type": "object",
"required": [
"name",
"fields"
],
"properties": {
"name": {
"type": "string",
"description": "Name of the custom type"
},
"fields": {
"type": "array",
"items": {
"$ref": "#/definitions/Field"
}
}
}
},
"Model": {
"type": "object",
"required": [
"id",
"name",
"fields"
],
"properties": {
"id": {
"type": "string",
"description": "Unique ID (UUID)"
},
"name": {
"type": "string",
"description": "Model name (Table name)"
},
"description": {
"type": "string"
},
"accessControl": {
"$ref": "#/definitions/AccessControl"
},
"webhooks": {
"type": "array",
"items": {
"$ref": "#/definitions/Webhook"
}
},
"fields": {
"type": "array",
"items": {
"$ref": "#/definitions/Field"
}
}
}
},
"AccessControl": {
"type": "object",
"properties": {
"read": {
"type": "array",
"items": {
"type": "string"
}
},
"write": {
"type": "array",
"items": {
"type": "string"
}
},
"delete": {
"type": "array",
"items": {
"type": "string"
}
},
"rowLevel": {
"$ref": "#/definitions/RowLevelAccess"
}
}
},
"RowLevelAccess": {
"type": "object",
"properties": {
"ownerField": {
"type": "string",
"description": "Field name holding the owner (e.g., 'createdBy')"
},
"rules": {
"type": "array",
"items": {
"$ref": "#/definitions/AccessRule"
}
}
}
},
"AccessRule": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"role": {
"type": "string"
},
"condition": {
"type": "object"
}
}
},
"Webhook": {
"type": "object",
"required": [
"event",
"url"
],
"properties": {
"event": {
"type": "string",
"enum": [
"onCreate",
"onUpdate",
"onDelete"
]
},
"url": {
"type": "string"
}
}
},
"Field": {
"type": "object",
"required": [
"id",
"name",
"type"
],
"properties": {
"id": {
"type": "string",
"description": "Unique Field ID"
},
"name": {
"type": "string"
},
"type": {
"$ref": "#/definitions/FieldType"
},
"isPrimary": {
"type": "boolean"
},
"isList": {
"type": "boolean"
},
"isIndex": {
"type": "boolean"
},
"validation": {
"$ref": "#/definitions/FieldValidation"
},
"options": {
"$ref": "#/definitions/FieldOptions"
},
"relationTo": {
"type": "string",
"description": "Target model name if type is 'relation'"
},
"customTypeName": {
"type": "string",
"description": "Custom type name if type is 'custom'"
},
"enumValues": {
"type": "array",
"items": {
"type": "string"
},
"description": "Enum values if type is 'enum'"
},
"apiOptions": {
"$ref": "#/definitions/ApiOptions"
},
"relationOptions": {
"$ref": "#/definitions/RelationOptions"
}
}
},
"FieldType": {
"type": "string",
"enum": [
"string",
"text",
"richText",
"number",
"integer",
"boolean",
"date",
"uuid",
"image",
"enum",
"relation",
"custom",
"role"
]
},
"FieldValidation": {
"type": "object",
"properties": {
"required": {
"type": "boolean"
},
"min": {
"type": "number"
},
"max": {
"type": "number"
},
"pattern": {
"type": "string"
},
"unique": {
"type": "boolean"
}
}
},
"FieldOptions": {
"type": "object",
"properties": {
"resize": {
"type": "boolean"
},
"format": {
"type": "string",
"enum": [
"webp",
"jpeg",
"png"
]
},
"default": {
"description": "Default value for the field (primitives only)",
"oneOf": [
{ "type": "string" },
{ "type": "number" },
{ "type": "integer" },
{ "type": "boolean" },
{ "type": "null" }
]
}
}
},
"Actor": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "string",
"description": "Unique Actor ID"
},
"name": {
"type": "string",
"description": "Actor name"
},
"description": {
"type": "string",
"description": "Actor description"
}
}
},
"UseCase": {
"type": "object",
"required": [
"id",
"name",
"actorIds"
],
"properties": {
"id": {
"type": "string",
"description": "Unique UseCase ID"
},
"name": {
"type": "string",
"description": "UseCase name"
},
"description": {
"type": "string",
"description": "UseCase description"
},
"actorIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "IDs of actors who can perform this use case"
},
"preconditions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Preconditions for the use case"
},
"postconditions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Postconditions for the use case"
},
"modelInteractions": {
"type": "array",
"items": {
"$ref": "#/definitions/ModelInteraction"
},
"description": "Interactions with models"
},
"notes": {
"type": "string",
"description": "Additional notes"
}
}
},
"ApiOptions": {
"type": "object",
"properties": {
"filterable": {
"type": "boolean"
},
"sortable": {
"type": "boolean"
},
"searchable": {
"type": "boolean"
}
}
},
"RelationOptions": {
"type": "object",
"properties": {
"expandable": {
"type": "boolean"
},
"defaultExpand": {
"type": "boolean"
},
"onDelete": {
"type": "string",
"enum": [
"cascade",
"nullify",
"restrict"
]
}
}
},
"ModelInteraction": {
"type": "object",
"required": [
"model",
"description"
],
"properties": {
"model": {
"type": "string"
},
"description": {
"type": "string"
},
"hint": {
"type": "string",
"enum": [
"read",
"write",
"delete",
"search",
"aggregate",
"other"
]
}
}
}
}
}モデル永続化仕様 (JSON Specification)
API Designerで作成されるモデルデータのJSON構造仕様です。 このデータ構造は、プロジェクトの保存・読み込み(インポート/エクスポート)に使用されます。
[!TIP]
JSON Schema: model-persistence-schema.json
ルートオブジェクト (ApiProject)
プロジェクト全体を表すルートオブジェクトです。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
projectName | string | Yes | プロジェクト名 |
version | string | Yes | プロジェクトのバージョン (例: "1.0.0") |
models | Model[] | Yes | 定義されたモデルのリスト |
customTypes | CustomType[] | No | [New] カスタム型定義のリスト |
authConfig | AuthConfig | No | [New] 認証・ユーザー管理設定 |
roles | string[] | No | [New] グローバルロールのリスト |
actors | Actor[] | No | [New] アクターのリスト |
useCases | UseCase[] | No | [New] ユースケースのリスト |
{
"projectName": "My CMS",
"version": "1.0.0",
"models": [ ... ],
"customTypes": [ ... ],
"authConfig": { ... }
}モデル (Model)
1つのデータモデル(テーブル定義)を表します。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | Yes | モデルの一意なID (UUID) |
name | string | Yes | モデル名(テーブル名) |
displayName | string | No | [New] モデルの表示名 |
description | string | No | モデルの説明 |
fields | Field[] | Yes | フィールド定義のリスト |
accessControl | AccessControl | No | アクセス制御設定 |
webhooks | Webhook[] | No | Webhook設定 |
アクセス制御 (AccessControl)
| プロパティ名 | 型 | 説明 |
|---|---|---|
read | string[] | 読み取り権限を持つロール (例: ['public', 'admin']) |
write | string[] | 書き込み権限を持つロール |
delete | string[] | 削除権限を持つロール |
rowLevel | RowLevelAccess | [New] 行レベルのアクセス制御ルール |
行レベルアクセス制御 (RowLevelAccess)
特定の条件に一致するレコードのみアクセスを許可する設定です。
| プロパティ名 | 型 | 説明 |
|---|---|---|
ownerField | string | 作成者(所有者)を保持するフィールド名 (例: createdBy) |
rules | AccessRule[] | 詳細な条件ルール |
"rowLevel": {
"ownerField": "author", // authorフィールドが現在のユーザーと一致する場合のみアクセス可
"rules": [
{
"action": "read",
"role": "member",
"condition": { "status": "published" } // memberロールはstatusがpublishedのものだけ読める
}
]
}Webhook
| プロパティ名 | 型 | 説明 |
|---|---|---|
event | string | イベントタイプ (onCreate, onUpdate, onDelete) |
url | string | 通知先URL |
フィールド (Field)
モデル内の各フィールド(カラム)定義です。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | Yes | フィールドの一意なID |
name | string | Yes | フィールド名 |
displayName | string | No | [New] フィールドの表示名 |
type | FieldType | Yes | データ型 |
isPrimary | boolean | No | プライマリキーかどうか |
isList | boolean | No | 配列(リスト)かどうか |
isIndex | boolean | No | インデックスを作成するかどうか |
validation | FieldValidation | No | バリデーションルール |
options | FieldOptions | No | その他のオプション |
relationTo | string | No | リレーション先のモデル名 (typeがrelationの場合) |
customTypeName | string | No | [New] カスタム型名 (typeがcustomの場合) |
enumValues | string[] | No | Enumの選択肢 (typeがenumの場合) |
apiOptions | ApiOptions | No | [New] API公開オプション |
relationOptions | RelationOptions | No | [New] リレーション設定オプション |
データ型 (FieldType)
以下の文字列のいずれか:
string: 文字列 (一行)text: [New] 文字列 (複数行)richText: [New] リッチテキスト (HTML)number: 数値 (浮動小数点数)integer: [New] 数値 (整数)boolean: 真偽値date: 日時uuid: UUIDimage: 画像enum: 列挙型relation: リレーションcustom: [New] カスタム型role: [New] ロール型 (Global Rolesと連携)
バリデーション (FieldValidation)
| プロパティ名 | 型 | 説明 |
|---|---|---|
required | boolean | 必須項目かどうか |
min | number | 最小値(数値)または最小長(文字列) |
max | number | 最大値(数値)または最大長(文字列) |
pattern | string | 正規表現パターン |
unique | boolean | ユニーク制約 |
オプション (FieldOptions)
| プロパティ名 | 型 | 説明 |
|---|---|---|
resize | boolean | 画像のリサイズを行うか |
format | string | 画像フォーマット (webp, jpeg, png) |
default | any | デフォルト値 |
APIオプション (ApiOptions) [New]
API経由でのアクセス制御に関するオプションです。
| プロパティ名 | 型 | 説明 |
|---|---|---|
filterable | boolean | フィルタリング可能にするか |
sortable | boolean | ソート可能にするか |
searchable | boolean | 検索対象にするか |
リレーションオプション (RelationOptions) [New]
リレーションフィールドに対する追加設定です。
| プロパティ名 | 型 | 説明 |
|---|---|---|
expandable | boolean | APIレスポンスで展開(join)可能にするか |
defaultExpand | boolean | デフォルトで展開するか |
onDelete | string | 参照先削除時の挙動 (cascade, nullify, restrict) |
カスタム型 (CustomType) [New]
再利用可能なフィールドの集合定義です(例: 住所、SEO設定など)。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | Yes | カスタム型名 (例: "Address") |
fields | Field[] | Yes | フィールド定義のリスト |
{
"name": "Address",
"fields": [
{ "name": "zipCode", "type": "string" },
{ "name": "city", "type": "string" }
]
}ユーザー管理・認証 (AuthConfig) [New]
ユーザー管理機能と認証に関する設定です。 有効にすると、システム内部で User モデルが自動的に管理されます。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
enabled | boolean | Yes | ユーザー管理機能を有効にするか |
userModelName | string | No | ユーザー情報を格納するモデル名 (デフォルト: "User") |
authFields | AuthFields | No | 認証に使用するフィールド設定 |
ユーザーモデルとの連携
ユーザー管理を有効にすると、他のモデルからユーザーモデルへのリレーションを定義することで、 「誰が作成したか」「誰が閲覧できるか」 といった行レベルのアクセス制御が可能になります。
例: Post モデルに author フィールド (Userへのリレーション) を追加し、 AccessControl.rowLevel.ownerField に author を指定する。
アクター (Actor) [New]
システムを利用するユーザーや外部システムの役割を定義します。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | Yes | アクターの一意なID |
name | string | Yes | アクター名 |
description | string | No | アクターの説明 |
ユースケース (UseCase) [New]
システムが提供する機能や振る舞いを定義します。自然言語による設計をサポートするため、構造化された各フィールドを持ちます。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | Yes | ユースケースの一意なID |
name | string | Yes | ユースケース名 |
description | string | No | ユースケースの説明 |
actorIds | string[] | Yes | このユースケースを実行できるアクターのIDリスト |
preconditions | string[] | No | 事前条件 |
postconditions | string[] | No | 事後条件 |
modelInteractions | ModelInteraction[] | No | モデルとのインタラクション |
notes | string | No | 設計に関するメモ・特記事項 |
モデルインタラクション (ModelInteraction)
ユースケース内でどのモデルに対してどのような操作を行うかを定義します。
| プロパティ名 | 型 | 必須 | 説明 |
|---|---|---|---|
model | string | Yes | 対象モデル名 |
description | string | Yes | 操作の説明(自然言語) |
hint | string | No | 操作の種類のヒント (read, write, delete, search, aggregate, other) |
JSONサンプル (拡張版)
{
"projectName": "E-commerce API",
"version": "1.1.0",
"authConfig": {
"enabled": true,
"userModelName": "User"
},
"roles": ["public", "admin", "user"],
"actors": [
{
"id": "actor-1",
"name": "Customer",
"description": "一般顧客"
}
],
"useCases": [
{
"id": "uc-1",
"name": "Purchase Product",
"description": "商品を購入する",
"actorIds": ["actor-1"],
"preconditions": ["User must be logged in", "Product must be in stock"],
"postconditions": ["Order created", "Stock decreased", "Email sent"],
"modelInteractions": [
{
"model": "Product",
"description": "Check availability and price",
"hint": "read"
},
{
"model": "Order",
"description": "Create new order record",
"hint": "write"
}
]
}
],
"customTypes": [
{
"name": "SEO",
"fields": [
{ "id": "seo1", "name": "title", "type": "string" },
{ "id": "seo2", "name": "description", "type": "text" }
]
}
],
"models": [
{
"id": "uuid-1",
"name": "Product",
"description": "商品情報",
"accessControl": {
"read": ["public"],
"write": ["admin"],
"rowLevel": {
"ownerField": "createdBy"
}
},
"fields": [
{
"id": "f1",
"name": "id",
"type": "uuid",
"isPrimary": true
},
{
"id": "f2",
"name": "description",
"type": "richText",
"apiOptions": {
"searchable": true
}
},
{
"id": "f3",
"name": "stock",
"type": "integer",
"validation": { "min": 0 },
"apiOptions": {
"sortable": true,
"filterable": true
}
},
{
"id": "f4",
"name": "seoSettings",
"type": "custom",
"customTypeName": "SEO"
},
{
"id": "f5",
"name": "createdBy",
"type": "relation",
"relationTo": "User",
"relationOptions": {
"onDelete": "restrict"
}
}
]
}
]
}ステップ1: アプリケーション仕様を確認する
目次
- 目的
- 手順
- 1.1 JSON仕様ファイルを読み込む
- 1.2 プロジェクト情報を確認する
- 1.3 モデル構造を把握する
- 1.4 フィールド定義を確認する
- 1.5 リレーションを確認する
- 1.6 カスタム型を確認する
- 1.7 アクターとユースケースを確認する
- 出力
- 注意点
---
目的
JSON仕様ファイルを読み込み、実装に必要な情報を正確に把握する。
手順
1.1 JSON仕様ファイルを読み込む
ユーザーから提供されたJSON仕様ファイルを読み込む。 仕様の形式は @references/01_json_specification.md を参照。
1.2 プロジェクト情報を確認する
{
"projectName": "プロジェクト名",
"version": "バージョン",
"roles": ["public", "admin", "user"]
}1.3 モデル構造を把握する
各モデルについて以下を確認:
name: モデル名(テーブル名として使用)displayName: 表示名(管理画面で使用)description: モデルの説明fields: フィールド定義のリストaccessControl: アクセス制御設定
1.4 フィールド定義を確認する
各フィールドについて以下を確認:
| プロパティ | 確認内容 |
|---|---|
name | フィールド名(カラム名) |
displayName | 表示名 |
type | データ型 |
isPrimary | プライマリキーか |
isIndex | インデックスが必要か |
validation | バリデーションルール |
relationTo | リレーション先モデル |
apiOptions | API公開オプション(後述) |
relationOptions | リレーション設定(後述) |
apiOptions の確認
apiOptionsが設定されているフィールドを抽出し、API動作を把握する:
| プロパティ | 意味 | 後続ステップでの活用 |
|---|---|---|
filterable: true | フィルタリング可能 | ステップ3, 4, 6, 11 |
sortable: true | ソート可能 | ステップ3, 4, 6, 11 |
searchable: true | 全文検索対象 | ステップ3, 4, 6, 11 |
{
"name": "title",
"type": "string",
"apiOptions": {
"filterable": true,
"sortable": true,
"searchable": true
}
}relationOptions の確認
type: "relation"のフィールドでrelationOptionsが設定されている場合を確認:
| プロパティ | 意味 | 後続ステップでの活用 |
|---|---|---|
expandable: true | API応答で展開可能 | ステップ3, 4, 11 |
defaultExpand: true | デフォルトで展開 | ステップ11 |
onDelete | 削除時動作 | ステップ5, 8, 9 |
{
"name": "author",
"type": "relation",
"relationTo": "User",
"relationOptions": {
"expandable": true,
"defaultExpand": false,
"onDelete": "nullify"
}
}1.5 リレーションを確認する
type: "relation" のフィールドを抽出し、モデル間の関係を把握する。
リレーションの種類
- 1対1
- 1対多
- 多対多(中間テーブルが必要)
削除時動作(onDelete)
| 値 | 動作 | Railsでの実装 |
|---|---|---|
cascade | 連動削除 | dependent: :destroy |
nullify | NULLに設定 | dependent: :nullify |
restrict | 削除禁止(デフォルト) | dependent: :restrict_with_error |
1.6 カスタム型を確認する
customTypes が定義されている場合、その構造を把握する。 カスタム型は複数のフィールドをまとめた再利用可能な型定義。
{
"customTypes": [
{
"name": "SEO",
"fields": [
{ "name": "title", "type": "string" },
{ "name": "description", "type": "text" }
]
}
]
}1.7 アクターとユースケースを確認する
アクター(Actor)の確認
システムを利用するユーザーや外部システムの役割を把握:
{
"actors": [
{
"id": "actor-1",
"name": "Customer",
"description": "一般顧客"
}
]
}ユースケース(UseCase)の確認
各ユースケースについて以下を確認:
| プロパティ | 説明 |
|---|---|
id | ユースケースの一意なID |
name | ユースケース名 |
description | 説明 |
actorIds | 実行可能なアクターのIDリスト |
preconditions | 事前条件(自然言語のリスト) |
postconditions | 事後条件(自然言語のリスト) |
modelInteractions | モデルとのインタラクション |
notes | 備考・補足説明 |
モデルインタラクション(ModelInteraction)の確認
各ユースケースがどのモデルに対してどのような操作を行うかを把握:
{
"modelInteractions": [
{
"model": "Product",
"description": "在庫数を確認し、購入可能かどうかを判定する",
"hint": "read"
},
{
"model": "Order",
"description": "注文レコードを作成し、カート内の商品を注文明細として登録する",
"hint": "write"
}
]
}| hint値 | 意味 |
|---|---|
read | 読み取り操作 |
write | 作成・更新操作 |
delete | 削除操作 |
search | 検索操作 |
aggregate | 集計操作 |
other | その他 |
出力
確認した内容を以下の形式でまとめる:
## 仕様確認結果
### プロジェクト情報
- プロジェクト名: xxx
- バージョン: x.x.x
- ロール: public, admin, user
### モデル一覧
1. モデル名A - 説明
2. モデル名B - 説明
### リレーション
| 元モデル | フィールド | 先モデル | 関係 | 削除時動作 |
|---------|-----------|---------|------|-----------|
| Post | author | User | 多対1 | nullify |
| Post | category | Category | 多対1 | restrict |
### API公開設定サマリ
#### フィルタ可能フィールド(filterable: true)
| モデル | フィールド | 型 |
|--------|-----------|-----|
| Post | status | enum |
| Post | category_id | relation |
#### ソート可能フィールド(sortable: true)
| モデル | フィールド | 型 |
|--------|-----------|-----|
| Post | created_at | date |
| Post | title | string |
#### 全文検索対象フィールド(searchable: true)
| モデル | フィールド | 型 |
|--------|-----------|-----|
| Post | title | string |
| Post | content | richText |
#### 展開可能リレーション(expandable: true)
| モデル | フィールド | 先モデル | デフォルト展開 |
|--------|-----------|---------|---------------|
| Post | author | User | false |
| Post | category | Category | true |
### カスタム型
- SEO: title, description
### アクター一覧
| ID | 名前 | 説明 |
|----|------|------|
| actor-1 | Customer | 一般顧客 |
| actor-2 | Admin | 管理者 |
### ユースケース一覧
| ID | 名前 | アクター | 関連モデル |
|----|------|---------|-----------|
| uc-1 | Purchase Product | Customer | Product, Order, OrderItem |
| uc-2 | Manage Products | Admin | Product, Category |
### ユースケース詳細
#### uc-1: Purchase Product
- **説明**: 商品を購入する
- **アクター**: Customer
- **事前条件**:
- 顧客がログイン済みであること
- カートに1つ以上の商品が入っていること
- **事後条件**:
- 注文レコードが作成される
- 在庫数が減少する
- **モデルインタラクション**:
| モデル | 操作 | 説明 |
|--------|------|------|
| Product | read | 在庫数を確認し、購入可能かどうかを判定する |
| Order | write | 注文レコードを作成する |
| Product | write | 在庫数を減らす |
- **備考**: 決済処理は外部サービスを利用注意点
- この段階では認証設定(authConfig)は確認のみ。実装は後のフェーズで行う。
- accessControl、webhooks の設定も確認のみに留める。
apiOptionsとrelationOptionsは後続ステップで重要な情報源となるため、漏れなく抽出する。modelInteractionsのdescriptionは自然言語で記述されており、LLMが実装時に参照する重要な情報。
ステップ2: 技術スタックを決定する
目次
- 技術スタック(固定)
- バックエンド
- データベース
- 開発環境
- 管理画面
- 手順
- 2.1 Docker Compose環境を構築する
- 2.2 Docker環境を起動・確認する
- 2.3 よく使うDockerコマンド
- 2.4 Docker環境のトラブルシューティング
- 2.5 管理画面の方式を決定する
- 2.6 追加Gemを検討する
- 出力
---
技術スタック(固定)
このスキルでは以下の技術スタックを使用する。
バックエンド
| 項目 | 技術 |
|---|---|
| 言語 | Ruby 3.4 |
| フレームワーク | Ruby on Rails 8.1 |
| APIモード | Rails API mode |
データベース
| 項目 | 技術 |
|---|---|
| RDBMS | PostgreSQL 18 |
| マイグレーション | Active Record Migrations |
| ORM | Active Record |
開発環境
| 項目 | 技術 |
|---|---|
| コンテナ | Docker + Docker Compose |
| Ruby環境 | Dockerコンテナ内 |
| DB環境 | PostgreSQLコンテナ |
管理画面
| 項目 | 技術 |
|---|---|
| 方式 | Rails一体型 または 別アプリ |
| UIライブラリ | ユーザーの希望に応じて選定 |
手順
2.1 Docker Compose環境を構築する
2.1.1 Dockerfileを作成する
Dockerfile:
FROM ruby:3.4-slim
# 必要なパッケージをインストール
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
git \
curl \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
# 作業ディレクトリを設定
WORKDIR /app
# Bundlerの設定
ENV BUNDLE_PATH=/usr/local/bundle
ENV BUNDLE_JOBS=4
# GemfileとGemfile.lockをコピー
COPY Gemfile Gemfile.lock ./
# Gemをインストール
RUN bundle install
# アプリケーションのソースをコピー
COPY . .
# ポートを公開
EXPOSE 3000
# デフォルトのコマンド
CMD ["rails", "server", "-b", "0.0.0.0"]2.1.2 docker-compose.ymlを作成する
docker-compose.yml:
services:
db:
image: postgres:18
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_development
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
web:
build: .
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails server -b '0.0.0.0'"
volumes:
- .:/app
- bundle_data:/usr/local/bundle
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/app_development
RAILS_ENV: development
stdin_open: true
tty: true
volumes:
postgres_data:
bundle_data:2.1.3 開発用の.env.exampleを作成する
.env.example:
# Database
DATABASE_URL=postgres://postgres:postgres@db:5432/app_development
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
# Rails
RAILS_ENV=development
RAILS_MASTER_KEY=your_master_key_here2.1.4 .dockerignoreを作成する
.dockerignore:
.git
.gitignore
log/*
tmp/*
storage/*
.docker-compose*
Dockerfile*
README.md
.env*
node_modules2.2 Docker環境を起動・確認する
# イメージをビルド
docker compose build
# コンテナを起動
docker compose up -d
# バージョンを確認
docker compose exec web ruby -v # 3.4.x であること
docker compose exec web rails -v # 8.1.x であること
docker compose exec db psql --version # 18.x であること
# ログを確認
docker compose logs -f web2.3 よく使うDockerコマンド
# Railsコマンドを実行
docker compose exec web rails db:create
docker compose exec web rails db:migrate
docker compose exec web rails console
# Bundleコマンドを実行
docker compose exec web bundle install
docker compose exec web bundle update
# テストを実行
docker compose exec web rspec
# コンテナを停止
docker compose down
# ボリュームも含めて削除(データベースもリセット)
docker compose down -v2.4 Docker環境のトラブルシューティング
2.4.1 ポート競合エラー
エラー: Bind for 0.0.0.0:5432 failed: port is already allocated
ホストでPostgreSQLが既に起動している場合に発生する。
解決方法1: ホストのPostgreSQLを停止
# Ubuntu/Debian
sudo systemctl stop postgresql
# macOS (Homebrew)
brew services stop postgresql解決方法2: docker-compose.ymlのポートを変更
services:
db:
ports:
- "5433:5432" # ホスト側を5433に変更この場合、直接接続時は localhost:5433 を使用する。
ポート使用状況の確認:
# Linux/macOS
lsof -i :5432
# または
ss -tlnp | grep 54322.4.2 ファイル権限エラー
エラー: EACCES: permission denied
Dockerコンテナ内でrootユーザーとして作成されたファイルに、ホストからアクセスできない。
解決方法1: コマンド実行時にユーザーを指定
# ファイルを作成するコマンドは --user オプションを使用
docker compose run --rm --user "$(id -u):$(id -g)" web rails generate model User解決方法2: docker-compose.ymlにユーザー設定を追加
services:
web:
user: "${UID:-1000}:${GID:-1000}"
# ... 他の設定起動前に環境変数を設定:
export UID=$(id -u)
export GID=$(id -g)
docker compose up -d解決方法3: 既存ファイルの権限を修正
# sudoが使える場合
sudo chown -R $(id -u):$(id -g) .
# Dockerコンテナ内から修正
docker compose run --rm web chown -R $(id -u):$(id -g) /app2.4.3 sudoなしでDockerを実行する設定
# ユーザーをdockerグループに追加
sudo usermod -aG docker $USER
# 変更を反映(再ログインまたは以下を実行)
newgrp docker
# 確認
docker ps2.5 管理画面の方式を決定する
ユーザーに以下の選択肢を提示:
| 方式 | 特徴 | 推奨ケース |
|---|---|---|
| Rails一体型 | Railsアプリ内でViewを実装 | シンプル、即座に利用可能 |
| React/Next.js別アプリ | フロントエンドを分離 | リッチなUI、SPAが必要 |
Rails一体型の場合の選択肢
| ライブラリ | 特徴 |
|---|---|
| Hotwire (Turbo + Stimulus) | Rails 8標準、サーバーサイドレンダリング |
| ActiveAdmin | 管理画面特化、即座に構築可能 |
| Administrate | シンプル、カスタマイズ性高 |
2.6 追加Gemを検討する
| Gem | 用途 |
|---|---|
jbuilder または alba | JSONシリアライザ |
kaminari または pagy | ページネーション |
ransack | 検索・フィルタリング |
rack-cors | CORS対応(API利用時) |
rspec-rails | テスト |
出力
選定した管理画面の方式と追加Gemを記録する。
ステップ3: ユースケースを洗い出す
目次
- 目的
- 手順
- 3.1 JSON仕様からアクターとユースケースを抽出する
- 3.2 ユースケースの詳細を確認する
- 3.3 各モデルの基本CRUD操作を定義する
- 3.4 apiOptionsからAPI設定を自動導出する
- 3.5 relationOptionsから展開設定を自動導出する
- 3.6 アクセス制御とユースケースの関連付け
- 3.7 ユースケース実装計画を作成する
- 出力
---
目的
JSON仕様で定義されたアクターとユースケースを確認し、apiOptionsとrelationOptionsから自動導出できる情報と組み合わせて、各モデルに対するAPI操作とエンドポイントを定義する。
手順
3.1 JSON仕様からアクターとユースケースを抽出する
JSON仕様の actors と useCases プロパティを確認し、システムの利用者と提供機能を把握する。
アクター (Actor) の確認
"actors": [
{
"id": "actor-1",
"name": "Customer",
"description": "一般顧客"
},
{
"id": "actor-2",
"name": "Admin",
"description": "管理者"
}
]3.2 ユースケースの詳細を確認する
各ユースケースの詳細プロパティを確認する:
{
"id": "uc-1",
"name": "Purchase Product",
"description": "商品を購入する",
"actorIds": ["actor-1"],
"preconditions": [
"顧客がログイン済みであること",
"カートに1つ以上の商品が入っていること"
],
"postconditions": [
"注文レコードが作成される",
"在庫数が減少する",
"確認メールが送信される"
],
"modelInteractions": [
{
"model": "Cart",
"description": "現在のユーザーのカート情報を取得する",
"hint": "read"
},
{
"model": "Product",
"description": "在庫数を確認し、購入可能かどうかを判定する",
"hint": "read"
},
{
"model": "Order",
"description": "注文レコードを作成し、カート内の商品を注文明細として登録する",
"hint": "write"
}
],
"notes": "決済処理は外部決済サービス(Stripe)を利用"
}ユースケースプロパティの活用方法
| プロパティ | 活用方法 |
|---|---|
preconditions | バリデーション、認可チェックの実装に活用 |
postconditions | テストケースの期待結果として活用 |
modelInteractions | 必要なエンドポイント・サービス層の設計に活用 |
notes | 実装時の補足情報、非機能要件の把握に活用 |
3.3 各モデルの基本CRUD操作を定義する
JSON仕様の各モデルに対して、以下の基本操作を定義:
| 操作 | HTTPメソッド | エンドポイント例 | 説明 |
|---|---|---|---|
| 一覧取得 | GET | /api/v1/posts | ページネーション、フィルタ、ソート対応 |
| 単体取得 | GET | /api/v1/posts/:id | 関連データの展開オプション |
| 作成 | POST | /api/v1/posts | バリデーション実行 |
| 更新 | PATCH/PUT | /api/v1/posts/:id | 部分更新対応 |
| 削除 | DELETE | /api/v1/posts/:id | 論理削除 or 物理削除 |
3.4 apiOptionsからAPI設定を自動導出する
JSON仕様の各フィールドのapiOptionsから、API動作設定を自動導出する。
フィルタ可能フィールドの抽出
apiOptions.filterable: true のフィールドを抽出:
## フィルタ可能フィールド(自動導出)
| モデル | フィールド | 型 | クエリパラメータ例 |
|--------|-----------|-----|-------------------|
| Post | status | enum | `?status=published` |
| Post | category_id | relation | `?category_id=uuid` |
| Post | created_at | date | `?created_at_from=2024-01-01&created_at_to=2024-12-31` |
| Product | price | integer | `?price_min=1000&price_max=5000` |ソート可能フィールドの抽出
apiOptions.sortable: true のフィールドを抽出:
## ソート可能フィールド(自動導出)
| モデル | フィールド | 型 |
|--------|-----------|-----|
| Post | created_at | date |
| Post | updated_at | date |
| Post | title | string |
| Product | price | integer |
| Product | stock | integer |全文検索対象フィールドの抽出
apiOptions.searchable: true のフィールドを抽出:
## 全文検索対象フィールド(自動導出)
| モデル | フィールド | 型 |
|--------|-----------|-----|
| Post | title | string |
| Post | content | richText |
| Product | name | string |
| Product | description | text |全文検索が有効なモデルには ?q=検索キーワード パラメータを追加。
3.5 relationOptionsから展開設定を自動導出する
relationOptions から展開可能なリレーションを自動導出する。
展開可能リレーションの抽出
relationOptions.expandable: true のフィールドを抽出:
## 展開可能リレーション(自動導出)
| モデル | フィールド | 先モデル | デフォルト展開 | includeパラメータ |
|--------|-----------|---------|---------------|------------------|
| Post | author | User | false | `?include=author` |
| Post | category | Category | true | 自動展開 |
| Post | tags | Tag | false | `?include=tags` |
| Order | customer | User | false | `?include=customer` |
| Order | items | OrderItem | true | 自動展開 |デフォルト展開の扱い
defaultExpand: trueのリレーションは、明示的な指定なしで常に展開defaultExpand: falseの場合は?include=xxxで明示的に指定
3.6 アクセス制御とユースケースの関連付け
JSON仕様の roles と accessControl を確認し、各ユースケースの権限を整理する。
## アクセス制御マトリクス
| ユースケース | アクター | 必要なロール | 行レベル制御 |
|-------------|---------|-------------|-------------|
| Purchase Product | Customer | user | - |
| Manage Products | Admin | admin | - |
| View Own Orders | Customer | user | ownerField: customerId |3.7 ユースケース実装計画を作成する
各ユースケースのmodelInteractionsから、実装に必要な要素を整理する。
ユースケースからエンドポイントへの変換
modelInteractionsのhintに基づいて、必要なエンドポイントを導出:
| hint | 導出されるエンドポイント |
|---|---|
read (単体) | GET /api/v1/{model}/{id} |
read (一覧) | GET /api/v1/{model} |
write (作成) | POST /api/v1/{model} |
write (更新) | PATCH /api/v1/{model}/{id} |
delete | DELETE /api/v1/{model}/{id} |
search | GET /api/v1/{model}?q=xxx |
aggregate | カスタムエンドポイントまたはサービス層で実装 |
実装計画の例
## ユースケース実装計画: uc-1 Purchase Product
### 必要なエンドポイント
| エンドポイント | 由来 | 説明 |
|---------------|------|------|
| GET /api/v1/carts/current | Cart (read) | 現在のユーザーのカートを取得 |
| GET /api/v1/products/:id | Product (read) | 商品の在庫確認 |
| POST /api/v1/orders | Order (write) | 注文を作成 |
| PATCH /api/v1/products/:id | Product (write) | 在庫を減らす |
| DELETE /api/v1/cart_items | CartItem (delete) | カートをクリア |
### バリデーション(preconditionsから導出)
- ユーザー認証チェック
- カート内商品の存在チェック
- 在庫数の確認
### テスト期待結果(postconditionsから導出)
- [ ] 注文レコードが作成されること
- [ ] 在庫数が購入数量分減少すること
- [ ] カートが空になること
- [ ] 確認メールが送信されること
### 補足(notesから)
- 決済処理はStripe APIを利用
- 決済失敗時のロールバック処理が必要出力
自動導出された設定
## API設定サマリ(JSON仕様から自動導出)
### フィルタ可能フィールド
[apiOptions.filterable: true のフィールド一覧]
### ソート可能フィールド
[apiOptions.sortable: true のフィールド一覧]
### 全文検索対象フィールド
[apiOptions.searchable: true のフィールド一覧]
### 展開可能リレーション
[relationOptions.expandable: true のフィールド一覧]ユースケース実装計画
## ユースケース一覧
| ID | 名前 | アクター | 関連モデル | エンドポイント数 |
|----|------|---------|-----------|----------------|
| uc-1 | Purchase Product | Customer | Cart, Product, Order, CartItem | 5 |
| uc-2 | Manage Products | Admin | Product, Category | 8 |
## 各ユースケースの実装計画
### uc-1: Purchase Product
[modelInteractionsから導出した実装計画]
### uc-2: Manage Products
[modelInteractionsから導出した実装計画]モデルごとのエンドポイント一覧
## Posts モデル
### エンドポイント一覧
| メソッド | パス | 説明 | 関連ユースケース |
|---------|------|------|-----------------|
| GET | /api/v1/posts | 記事一覧取得 | View Posts |
| GET | /api/v1/posts/:id | 記事詳細取得 | View Posts |
| POST | /api/v1/posts | 記事作成 | Create Post |
| PATCH | /api/v1/posts/:id | 記事更新 | Edit Post |
| DELETE | /api/v1/posts/:id | 記事削除 | Delete Post |
### フィルタ可能なフィールド(自動導出)
- status (enum) - `apiOptions.filterable: true`
- category_id (relation) - `apiOptions.filterable: true`
- created_at (date range) - `apiOptions.filterable: true`
### ソート可能なフィールド(自動導出)
- created_at - `apiOptions.sortable: true`
- updated_at - `apiOptions.sortable: true`
- title - `apiOptions.sortable: true`
### 全文検索(自動導出)
- 検索対象: title, content
- クエリパラメータ: `?q=検索キーワード`
### 展開可能なリレーション(自動導出)
- author (User) - `relationOptions.expandable: true`, defaultExpand: false
- category (Category) - `relationOptions.expandable: true`, defaultExpand: true
- tags (Tag[]) - `relationOptions.expandable: true`, defaultExpand: falseこれらを次のステップで使用する。
ステップ4: OpenAPI定義を作成する
目次
- 目的
- 手順
- 4.1 OpenAPIドキュメントの基本構造を作成する
- 4.2 認証スキームを定義する
- 4.3 共通コンポーネントを定義する
- 4.4 モデルスキーマを定義する
- 4.5 apiOptionsからパラメータを自動生成する
- 4.6 relationOptionsから展開パラメータを自動生成する
- 4.7 エンドポイントを定義する
- 4.8 アクセス制御をOpenAPIに反映する
- 4.9 ユースケースとエンドポイントの対応表を作成する
- 4.10 OpenAPIファイルを出力する
- 出力
- 検証
- 次のステップ
---
目的
ユースケースとモデル定義に基づいて、APIの仕様をOpenAPI 3.1形式で定義する。 apiOptionsとrelationOptionsから自動導出できる情報を活用し、一貫性のあるAPI仕様を生成する。
手順
4.1 OpenAPIドキュメントの基本構造を作成する
openapi: 3.1.0
info:
title: {projectName} API
version: {version}
description: |
{projectName}のREST API仕様
servers:
- url: http://localhost:3000/api/v1
description: 開発環境
- url: https://api.example.com/v1
description: 本番環境4.2 認証スキームを定義する
JSON仕様の authConfig に基づいて認証方式を定義:
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT認証トークン
security:
- bearerAuth: []4.3 共通コンポーネントを定義する
エラーレスポンス
components:
schemas:
Error:
type: object
properties:
error:
type: string
description: エラーメッセージ
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
responses:
BadRequest:
description: リクエストが不正です
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: 認証が必要です
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: アクセスが拒否されました
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: リソースが見つかりません
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
UnprocessableEntity:
description: バリデーションエラー
content:
application/json:
schema:
$ref: '#/components/schemas/Error'ページネーション
components:
schemas:
PaginationMeta:
type: object
properties:
total_count:
type: integer
description: 総件数
total_pages:
type: integer
description: 総ページ数
current_page:
type: integer
description: 現在のページ
per_page:
type: integer
description: 1ページあたりの件数
parameters:
PageParam:
name: page
in: query
schema:
type: integer
default: 1
minimum: 1
description: ページ番号
PerPageParam:
name: per_page
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
description: 1ページあたりの件数4.4 モデルスキーマを定義する
JSON仕様の各モデルをOpenAPIスキーマに変換する。
型マッピング
| JSON仕様の型 | OpenAPI型 | format |
|---|---|---|
string | string | - |
text | string | - |
richText | string | html |
number | number | double |
integer | integer | int64 |
boolean | boolean | - |
date | string | date-time |
uuid | string | uuid |
image | string | uri |
enum | string | enum値を列挙 |
relation | object または string | 関連モデルの$refまたはID |
custom | object | カスタム型の$ref |
role | string | rolesの値を列挙 |
スキーマ例
components:
schemas:
Product:
type: object
required:
- id
- name
- price
properties:
id:
type: string
format: uuid
description: 商品ID
readOnly: true
name:
type: string
minLength: 1
maxLength: 255
description: 商品名
description:
type: string
format: html
description: 商品説明(リッチテキスト)
price:
type: integer
format: int64
minimum: 0
description: 価格
status:
type: string
enum: [draft, published, archived]
description: 公開状態
category:
$ref: '#/components/schemas/Category'
created_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
ProductInput:
type: object
required:
- name
- price
properties:
name:
type: string
minLength: 1
maxLength: 255
description:
type: string
price:
type: integer
minimum: 0
status:
type: string
enum: [draft, published, archived]
category_id:
type: string
format: uuid4.5 apiOptionsからパラメータを自動生成する
JSON仕様のapiOptionsに基づいて、クエリパラメータを自動生成する。
フィルタパラメータの自動生成
apiOptions.filterable: true のフィールドに対してフィルタパラメータを生成:
# 各モデルに対して、filterable: true のフィールドからパラメータを生成
components:
parameters:
# enum型のフィルタ
ProductStatusFilter:
name: status
in: query
schema:
type: string
enum: [draft, published, archived]
description: ステータスでフィルタ(filterable: true から自動生成)
# relation型のフィルタ
ProductCategoryIdFilter:
name: category_id
in: query
schema:
type: string
format: uuid
description: カテゴリIDでフィルタ(filterable: true から自動生成)
# date型のフィルタ(範囲指定)
ProductCreatedAtFromFilter:
name: created_at_from
in: query
schema:
type: string
format: date-time
description: 作成日時(開始)でフィルタ(filterable: true から自動生成)
ProductCreatedAtToFilter:
name: created_at_to
in: query
schema:
type: string
format: date-time
description: 作成日時(終了)でフィルタ(filterable: true から自動生成)
# integer/number型のフィルタ(範囲指定)
ProductPriceMinFilter:
name: price_min
in: query
schema:
type: integer
description: 価格(最小)でフィルタ(filterable: true から自動生成)
ProductPriceMaxFilter:
name: price_max
in: query
schema:
type: integer
description: 価格(最大)でフィルタ(filterable: true から自動生成)ソートパラメータの自動生成
apiOptions.sortable: true のフィールドからソートオプションを生成:
components:
parameters:
ProductSortParam:
name: sort
in: query
schema:
type: string
# sortable: true のフィールドをenumに列挙
enum: [created_at, updated_at, name, price, stock]
default: created_at
description: ソート項目(sortable: true のフィールドから自動生成)
SortOrderParam:
name: order
in: query
schema:
type: string
enum: [asc, desc]
default: desc
description: ソート順全文検索パラメータの自動生成
apiOptions.searchable: true のフィールドが1つ以上ある場合、検索パラメータを追加:
components:
parameters:
SearchQueryParam:
name: q
in: query
schema:
type: string
description: |
全文検索クエリ(searchable: true のフィールドから自動生成)
検索対象: title, content, name, description4.6 relationOptionsから展開パラメータを自動生成する
relationOptions.expandable: true のリレーションからincludeパラメータを生成。
components:
parameters:
ProductIncludeParam:
name: include
in: query
schema:
type: string
description: |
展開するリレーション(カンマ区切り)
利用可能な値(expandable: true から自動生成):
- category (デフォルト展開: true)
- created_by (デフォルト展開: false)
example: category,created_byデフォルト展開の説明追加
paths:
/products:
get:
description: |
商品の一覧をページネーション付きで取得します。
**デフォルトで展開されるリレーション:**
- category (relationOptions.defaultExpand: true)
**明示的に指定が必要なリレーション:**
- created_by (?include=created_by)4.7 エンドポイントを定義する
ユースケースで定義した各エンドポイントをOpenAPIのpathsに変換する。
一覧取得 (GET /resources)
paths:
/products:
get:
summary: 商品一覧を取得
description: |
商品の一覧をページネーション付きで取得します。
**フィルタ可能なフィールド(apiOptions.filterable: true):**
- status, category_id, created_at, price
**ソート可能なフィールド(apiOptions.sortable: true):**
- created_at, updated_at, name, price, stock
**全文検索対象(apiOptions.searchable: true):**
- name, description
operationId: listProducts
tags:
- Products
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/PerPageParam'
# apiOptions.filterable: true から自動生成
- $ref: '#/components/parameters/ProductStatusFilter'
- $ref: '#/components/parameters/ProductCategoryIdFilter'
- $ref: '#/components/parameters/ProductPriceMinFilter'
- $ref: '#/components/parameters/ProductPriceMaxFilter'
# apiOptions.sortable: true から自動生成
- $ref: '#/components/parameters/ProductSortParam'
- $ref: '#/components/parameters/SortOrderParam'
# apiOptions.searchable: true から自動生成
- $ref: '#/components/parameters/SearchQueryParam'
# relationOptions.expandable: true から自動生成
- $ref: '#/components/parameters/ProductIncludeParam'
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Product'
meta:
$ref: '#/components/schemas/PaginationMeta'
'401':
$ref: '#/components/responses/Unauthorized'単体取得 (GET /resources/:id)
paths:
/products/{id}:
get:
summary: 商品詳細を取得
operationId: getProduct
tags:
- Products
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
- $ref: '#/components/parameters/ProductIncludeParam'
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'404':
$ref: '#/components/responses/NotFound'作成 (POST /resources)
post:
summary: 商品を作成
operationId: createProduct
tags:
- Products
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductInput'
responses:
'201':
description: 作成成功
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'422':
$ref: '#/components/responses/UnprocessableEntity'更新 (PATCH /resources/:id)
patch:
summary: 商品を更新
operationId: updateProduct
tags:
- Products
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductInput'
responses:
'200':
description: 更新成功
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/UnprocessableEntity'削除 (DELETE /resources/:id)
delete:
summary: 商品を削除
operationId: deleteProduct
tags:
- Products
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'204':
description: 削除成功
'404':
$ref: '#/components/responses/NotFound'4.8 アクセス制御をOpenAPIに反映する
ロールベースのセキュリティ
JSON仕様の accessControl と roles に基づいて、エンドポイントごとにセキュリティ要件を設定:
paths:
/admin/products:
get:
summary: 商品一覧(管理者用)
security:
- bearerAuth: []
x-roles: [admin] # カスタム拡張で必要ロールを明示行レベルセキュリティの説明
paths:
/my/orders:
get:
summary: 自分の注文一覧を取得
description: |
現在認証されているユーザーの注文のみを取得します。
行レベルアクセス制御により、他ユーザーの注文は参照できません。4.9 ユースケースとエンドポイントの対応表を作成する
ユースケースのmodelInteractionsから、エンドポイントとの対応を記録する。
# OpenAPIのx-拡張を使用してユースケースとの対応を記録
paths:
/products:
get:
x-use-cases:
- id: uc-1
name: Browse Products
interaction: "商品一覧を閲覧し、在庫を確認する"
x-actors:
- actor-1 # Customer
- actor-2 # Admin
/orders:
post:
x-use-cases:
- id: uc-1
name: Purchase Product
interaction: "注文レコードを作成し、カート内の商品を注文明細として登録する"
x-actors:
- actor-1 # Customer
x-preconditions:
- "顧客がログイン済みであること"
- "カートに1つ以上の商品が入っていること"
x-postconditions:
- "注文レコードが作成される"
- "在庫数が減少する"4.10 OpenAPIファイルを出力する
以下の構成でファイルを出力:
docs/
└── api/
├── openapi.yaml # メインファイル
└── schemas/
├── common.yaml # 共通スキーマ
├── products.yaml # 商品関連
├── orders.yaml # 注文関連
└── users.yaml # ユーザー関連ファイル分割の例
# openapi.yaml
openapi: 3.1.0
info:
title: E-commerce API
version: 1.0.0
paths:
/products:
$ref: './paths/products.yaml#/products'
/products/{id}:
$ref: './paths/products.yaml#/products~1{id}'
components:
schemas:
Product:
$ref: './schemas/products.yaml#/Product'
ProductInput:
$ref: './schemas/products.yaml#/ProductInput'出力
docs/api/openapi.yaml- OpenAPI 3.1定義ファイル- 分割されたスキーマファイル(必要に応じて)
自動生成されたパラメータの記録
## 自動生成パラメータ一覧
### フィルタパラメータ(apiOptions.filterable: true から生成)
| モデル | フィールド | パラメータ名 | 型 |
|--------|-----------|-------------|-----|
| Product | status | status | enum |
| Product | category_id | category_id | uuid |
| Product | price | price_min, price_max | integer |
| Product | created_at | created_at_from, created_at_to | date-time |
### ソートパラメータ(apiOptions.sortable: true から生成)
| モデル | 利用可能なソートフィールド |
|--------|-------------------------|
| Product | created_at, updated_at, name, price, stock |
| Post | created_at, updated_at, title |
### 全文検索(apiOptions.searchable: true から生成)
| モデル | 検索対象フィールド |
|--------|-------------------|
| Product | name, description |
| Post | title, content |
### 展開パラメータ(relationOptions.expandable: true から生成)
| モデル | リレーション | デフォルト展開 |
|--------|-------------|---------------|
| Product | category | true |
| Product | created_by | false |
| Post | author | false |
| Post | category | true |検証
作成したOpenAPI定義を以下のツールで検証:
# OpenAPI仕様の検証
npx @redocly/cli lint docs/api/openapi.yaml
# ドキュメント生成(確認用)
npx @redocly/cli preview-docs docs/api/openapi.yaml次のステップ
作成したOpenAPI定義は以下のステップで活用:
- ステップ5(DBスキーマ設計): スキーマ定義の参照
- ステップ11(APIエンドポイント実装): エンドポイント実装のガイド
- ステップ12(API動作確認): テスト仕様として使用
ステップ5: DBスキーマを設計する
目次
- 目的
- 手順
- 5.1 フィールド型のマッピング
- 5.2 テーブル設計のルール
- 5.3 リレーションの設計
- 5.4 relationOptionsに基づく外部キー制約
- 5.5 カスタム型の展開
- 5.6 Enumの定義
- 5.7 Role型(配列)の定義
- 5.8 スキーマ設計書を作成する
- 出力
---
目的
JSON仕様をPostgreSQLのテーブル定義に変換する。 relationOptions.onDeleteに基づいて外部キー制約を適切に設定する。
手順
5.1 フィールド型のマッピング
JSON仕様のtypeをPostgreSQLの型にマッピング:
| JSON仕様の型 | PostgreSQL型 | Railsマイグレーション |
|---|---|---|
string | varchar(255) | string |
text | text | text |
richText | text | text |
number | decimal | decimal |
integer | integer / bigint | integer / bigint |
boolean | boolean | boolean |
date | timestamp | datetime |
uuid | uuid | uuid |
image | varchar(255) | string (URLを格納) |
enum | varchar(50) | string + enum定義 |
relation | bigint / uuid (FK) | references |
custom | 展開してカラム化 | 複数カラム |
role | varchar[] (配列) | string, array: true |
5.2 テーブル設計のルール
プライマリキー
# UUID を使用する場合
create_table :posts, id: :uuid do |t|
# ...
end
# BIGINT を使用する場合(デフォルト)
create_table :posts do |t|
# ...
endタイムスタンプ
全テーブルに created_at, updated_at を追加:
t.timestamps5.3 リレーションの設計
基本的な外部キー
リレーション型のフィールドには外部キー制約を設定:
t.references :author, foreign_key: { to_table: :users }5.4 relationOptionsに基づく外部キー制約
JSON仕様のrelationOptions.onDeleteに基づいて、外部キー制約の削除時動作を設定する。
onDeleteの値とRails/PostgreSQLマッピング
| onDelete値 | PostgreSQL制約 | Railsマイグレーション | 動作 |
|---|---|---|---|
cascade | ON DELETE CASCADE | on_delete: :cascade | 親削除時に子も削除 |
nullify | ON DELETE SET NULL | on_delete: :nullify | 親削除時にNULL設定 |
restrict | ON DELETE RESTRICT | on_delete: :restrict | 子がある場合は削除禁止(デフォルト) |
マイグレーション例
# JSON仕様
# {
# "name": "author",
# "type": "relation",
# "relationTo": "User",
# "relationOptions": {
# "onDelete": "nullify"
# }
# }
# マイグレーション
create_table :posts, id: :uuid do |t|
# onDelete: nullify の場合
t.references :author,
type: :uuid,
foreign_key: { to_table: :users, on_delete: :nullify },
null: true # nullifyの場合はNULL許可が必要
# onDelete: cascade の場合
t.references :category,
type: :uuid,
foreign_key: { to_table: :categories, on_delete: :cascade },
null: false
# onDelete: restrict の場合(デフォルト)
t.references :department,
type: :uuid,
foreign_key: { to_table: :departments, on_delete: :restrict },
null: false
t.timestamps
end既存テーブルへの外部キー追加
# 外部キーの追加(on_delete指定あり)
add_foreign_key :posts, :users, column: :author_id, on_delete: :nullify
add_foreign_key :posts, :categories, column: :category_id, on_delete: :cascade
# 外部キーの変更(既存の制約を置き換え)
remove_foreign_key :posts, :users
add_foreign_key :posts, :users, column: :author_id, on_delete: :nullifyonDelete設定の注意事項
| 設定 | 注意点 |
|---|---|
cascade | 意図しないデータ削除に注意。子テーブルのデータも完全に削除される |
nullify | カラムにnull: trueが必要。必須フィールドには使用不可 |
restrict | 参照しているレコードがある場合、親レコードは削除できない |
5.5 カスタム型の展開
カスタム型はプレフィックス付きのカラムとして展開:
{
"name": "seoSettings",
"type": "custom",
"customTypeName": "SEO"
}↓
t.string :seo_settings_title
t.text :seo_settings_description5.6 Enumの定義
Rails 8のenumを使用:
# モデル内で定義
enum :status, { draft: 'draft', published: 'published', archived: 'archived' }5.7 Role型(配列)の定義
isList: true の role 型はPostgreSQLの配列カラムとして定義:
# マイグレーション
t.string :roles, array: true, default: []
# インデックス(GINインデックスで高速検索)
add_index :accounts, :roles, using: :ginモデルでの使用:
# app/models/account.rb
class Account < ApplicationRecord
# 配列カラムはそのまま使用可能
# account.roles = ['admin', 'user']
# account.roles << 'editor'
# ロールのバリデーション
VALID_ROLES = %w[public admin user editor].freeze
validate :validate_roles
def has_role?(role)
roles.include?(role.to_s)
end
def admin?
has_role?('admin')
end
private
def validate_roles
return if roles.blank?
invalid_roles = roles - VALID_ROLES
if invalid_roles.any?
errors.add(:roles, "に無効な値が含まれています: #{invalid_roles.join(', ')}")
end
end
endクエリ例:
# 特定のロールを持つユーザーを検索(固定値の場合)
Account.where("'admin' = ANY(roles)")
# ❌ 危険 - SQLインジェクションのリスク
# role = params[:role]
# Account.where("'#{role}' = ANY(roles)") # 絶対にこうしないこと!
# ❌ 危険 - 複数の値を検索する場合も同様
# roles = params[:roles] # ["admin", "editor"]
# Account.where("roles && ARRAY[#{roles.map { |r| "'#{r}'" }.join(',')}]") # 危険!
# ✅ 安全 - プレースホルダーを使用(単一値)
role = params[:role]
Account.where("? = ANY(roles)", role)
# ✅ 安全 - PostgreSQL配列演算子を使用(包含チェック)
role = params[:role]
Account.where("roles @> ARRAY[?]::varchar[]", role)
# ✅ 安全 - 複数ロールの検索(共通要素チェック)
roles = params[:roles] # ['admin', 'editor']
Account.where("roles && ARRAY[?]::varchar[]", roles)
# Ransackでの検索設定
ransacker :roles do
Arel.sql("array_to_string(roles, ',')")
endセキュリティ注意事項:
- 動的な値をSQL文字列に直接埋め込まないこと
- 必ずプレースホルダー(
?)を使用してパラメータを渡すこと - PostgreSQLの配列演算子(
@>は包含、&&は共通要素チェック)を活用
セキュリティチェックリスト:
- [ ] 動的な値を直接SQL文字列に埋め込んでいないか
- [ ] プレースホルダー(
?)を使用しているか - [ ] 配列操作時も適切にエスケープされているか
5.8 スキーマ設計書を作成する
以下の形式でまとめる:
## テーブル: posts
| カラム名 | 型 | NULL | デフォルト | 説明 |
|---------|-----|------|-----------|------|
| id | uuid | NO | gen_random_uuid() | PK |
| title | varchar(255) | NO | - | タイトル |
| content | text | YES | - | 本文 |
| status | varchar(50) | NO | 'draft' | ステータス |
| author_id | uuid | YES | - | FK: users |
| category_id | uuid | NO | - | FK: categories |
| created_at | timestamp | NO | - | 作成日時 |
| updated_at | timestamp | NO | - | 更新日時 |
### 外部キー(relationOptionsから自動導出)
| カラム | 参照先 | 削除時動作 | 由来 |
|--------|--------|-----------|------|
| author_id | users(id) | SET NULL | relationOptions.onDelete: "nullify" |
| category_id | categories(id) | CASCADE | relationOptions.onDelete: "cascade" |
### Enum定義
- status: draft, published, archived出力
全テーブルのスキーマ設計書を作成し、マイグレーション実装時に使用する。
出力形式
## スキーマ設計書
### 1. postsテーブル
[テーブル定義]
#### 外部キー制約(relationOptionsから導出)
| フィールド | 参照先 | onDelete | Railsオプション |
|-----------|--------|----------|----------------|
| author_id | users | nullify | `on_delete: :nullify, null: true` |
| category_id | categories | cascade | `on_delete: :cascade` |
#### マイグレーション例
create_table :posts, id: :uuid do |t| t.string :title, null: false t.text :content t.string :status, null: false, default: 'draft' t.references :author, type: :uuid, foreign_key: { to_table: :users, on_delete: :nullify }, null: true t.references :category, type: :uuid, foreign_key: { to_table: :categories, on_delete: :cascade }, null: false t.timestamps end
### 2. usersテーブル
[テーブル定義]
...ステップ6: SQLとインデックスを定義する
目次
- 目的
- 手順
- 6.1 基本CRUDのSQLを確認する
- 6.2 apiOptionsからインデックスを自動導出する
- 6.3 インデックスの設計方針
- 6.4 通常インデックスの定義
- 6.5 全文検索インデックスの定義
- 6.6 インデックス一覧を作成する
- 6.7 パフォーマンス考慮事項
- 出力
---
目的
ユースケースで実行されるSQLを洗い出し、apiOptionsから必要なインデックスを自動導出して定義する。
手順
6.1 基本CRUDのSQLを確認する
Active Recordが生成するSQLを把握する。
一覧取得
-- 基本
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 0;
-- フィルタリング
SELECT * FROM posts WHERE status = 'published' ORDER BY created_at DESC;
-- 関連データ取得(N+1回避)
SELECT * FROM posts WHERE id IN (...);
SELECT * FROM users WHERE id IN (...);単体取得
SELECT * FROM posts WHERE id = $1 LIMIT 1;作成
INSERT INTO posts (title, content, status, author_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;更新
UPDATE posts SET title = $1, updated_at = $2 WHERE id = $3 RETURNING *;削除
DELETE FROM posts WHERE id = $1;6.2 apiOptionsからインデックスを自動導出する
JSON仕様のapiOptionsに基づいて、必要なインデックスを自動的に導出する。
導出ルール
| apiOptions設定 | 導出されるインデックス | 理由 |
|---|---|---|
filterable: true | 単一カラムインデックス | WHERE句での高速検索 |
sortable: true | 単一カラムインデックス | ORDER BY での高速ソート |
searchable: true | GINインデックス(全文検索) | 全文検索の高速化 |
フィルタ用インデックスの導出
apiOptions.filterable: true のフィールドに対してインデックスを作成:
## フィルタ用インデックス(apiOptions.filterable: true から自動導出)
| テーブル | カラム | インデックス種類 | 導出元 |
|---------|--------|-----------------|--------|
| posts | status | BTREE | apiOptions.filterable: true |
| posts | category_id | BTREE | apiOptions.filterable: true (relation) |
| posts | created_at | BTREE | apiOptions.filterable: true (date range) |
| products | price | BTREE | apiOptions.filterable: true (range) |# マイグレーション
add_index :posts, :status # filterable: true から導出
add_index :posts, :category_id # filterable: true (relation) から導出
add_index :posts, :created_at # filterable: true (date) から導出
add_index :products, :price # filterable: true (range) から導出ソート用インデックスの導出
apiOptions.sortable: true のフィールドに対してインデックスを作成:
## ソート用インデックス(apiOptions.sortable: true から自動導出)
| テーブル | カラム | インデックス種類 | 導出元 |
|---------|--------|-----------------|--------|
| posts | created_at | BTREE | apiOptions.sortable: true |
| posts | updated_at | BTREE | apiOptions.sortable: true |
| posts | title | BTREE | apiOptions.sortable: true |
| products | price | BTREE | apiOptions.sortable: true |注意: filterableとsortableの両方がtrueの場合、インデックスは1つで十分。
全文検索インデックスの導出
apiOptions.searchable: true のフィールドに対してGINインデックスを作成:
## 全文検索インデックス(apiOptions.searchable: true から自動導出)
| テーブル | 対象カラム | インデックス種類 | 導出元 |
|---------|-----------|-----------------|--------|
| posts | title, content | GIN (tsvector) | apiOptions.searchable: true |
| products | name, description | GIN (tsvector) | apiOptions.searchable: true |6.3 インデックスの設計方針
必須インデックス
| 対象 | 理由 |
|---|---|
| プライマリキー | 自動作成 |
| 外部キー | JOINの高速化 |
isIndex: true のフィールド | 仕様で指定 |
unique: true のフィールド | ユニーク制約 |
apiOptions.filterable: true | 自動導出 |
apiOptions.sortable: true | 自動導出 |
apiOptions.searchable: true | 自動導出 |
推奨インデックス
| 対象 | 理由 |
|---|---|
| 複合条件 | 複合インデックス検討 |
6.4 通常インデックスの定義
# 単一カラムインデックス(apiOptionsから自動導出)
add_index :posts, :status # filterable: true
add_index :posts, :created_at # filterable: true, sortable: true
# 外部キーインデックス
add_index :posts, :author_id
# ユニークインデックス(validation.unique: true から導出)
add_index :users, :email, unique: true
# 複合インデックス(複数のfilterableフィールドの組み合わせ)
add_index :posts, [:status, :created_at]
# 部分インデックス(PostgreSQL)
add_index :posts, :created_at, where: "status = 'published'", name: 'index_posts_published_on_created_at'6.5 全文検索インデックスの定義(PostgreSQL)
apiOptions.searchable: true のフィールドに対して全文検索インデックスを作成する。
tsvectorカラムの追加
# マイグレーション
add_column :posts, :searchable, :tsvector
# GINインデックスの作成
add_index :posts, :searchable, using: :ginsearchableフィールドからトリガーを生成
JSON仕様のapiOptions.searchable: trueのフィールドを対象にトリガーを作成:
# マイグレーション内でSQL実行
# searchable: true のフィールド: title, content
execute <<-SQL
CREATE OR REPLACE FUNCTION posts_searchable_trigger() RETURNS trigger AS $$
BEGIN
NEW.searchable :=
setweight(to_tsvector('japanese', coalesce(NEW.title, '')), 'A') ||
setweight(to_tsvector('japanese', coalesce(NEW.content, '')), 'B');
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER posts_searchable_update
BEFORE INSERT OR UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION posts_searchable_trigger();
SQL全文検索クエリ
-- 基本検索
SELECT * FROM posts
WHERE searchable @@ plainto_tsquery('japanese', '検索キーワード');
-- ランキング付き
SELECT *, ts_rank(searchable, query) AS rank
FROM posts, plainto_tsquery('japanese', '検索キーワード') query
WHERE searchable @@ query
ORDER BY rank DESC;Railsモデルでのスコープ
class Post < ApplicationRecord
scope :search, ->(query) {
return all if query.blank?
# プレースホルダーとsanitize_sql_arrayを使用した安全な実装
where("searchable @@ plainto_tsquery('japanese', ?)", query)
.order(
Arel.sql(
sanitize_sql_array([
"ts_rank(searchable, plainto_tsquery('japanese', ?)) DESC",
query
])
)
)
}
endセキュリティ注意事項:
- WHERE句ではプレースホルダー(
?)を使用 - ORDER BY句では
sanitize_sql_arrayでパラメータをサニタイズ plainto_tsquery自体も入力をサニタイズするため二重の保護
6.6 インデックス一覧を作成する
## テーブル: posts
### インデックス一覧
| インデックス名 | カラム | 種類 | 用途 | 導出元 |
|--------------|--------|------|------|--------|
| posts_pkey | id | PRIMARY | PK | - |
| index_posts_on_author_id | author_id | BTREE | FK | relation |
| index_posts_on_status | status | BTREE | フィルタ | apiOptions.filterable: true |
| index_posts_on_category_id | category_id | BTREE | フィルタ | apiOptions.filterable: true |
| index_posts_on_created_at | created_at | BTREE | フィルタ/ソート | apiOptions.filterable: true, sortable: true |
| index_posts_on_updated_at | updated_at | BTREE | ソート | apiOptions.sortable: true |
| index_posts_on_title | title | BTREE | ソート | apiOptions.sortable: true |
| index_posts_on_searchable | searchable | GIN | 全文検索 | apiOptions.searchable: true (title, content) |6.7 パフォーマンス考慮事項
- インデックスは更新性能に影響するため、必要最小限に
filterableとsortableの両方がtrueの場合、インデックスは共有可能- 複合インデックスのカラム順序は選択性の高い順
- 全文検索インデックスは更新コストが高いため、検索頻度と更新頻度のバランスを考慮
- PostgreSQLのEXPLAIN ANALYZEで実行計画を確認
出力
全テーブルのインデックス定義をまとめ、マイグレーション実装時に使用する。
出力形式
## インデックス設計書
### apiOptionsから自動導出されたインデックス
#### フィルタ用(filterable: true)
| テーブル | カラム | 型 |
|---------|--------|-----|
| posts | status | enum |
| posts | category_id | relation |
| posts | created_at | date |
| products | price | integer |
#### ソート用(sortable: true)
| テーブル | カラム | 型 |
|---------|--------|-----|
| posts | created_at | date |
| posts | title | string |
| products | price | integer |
#### 全文検索用(searchable: true)
| テーブル | 対象カラム | 型 |
|---------|-----------|-----|
| posts | title | string |
| posts | content | richText |
| products | name | string |
| products | description | text |
### マイグレーション例
class AddIndexesToPosts < ActiveRecord::Migration[8.0] def change
フィルタ用インデックス(apiOptions.filterable: true から導出)
add_index :posts, :status add_index :posts, :category_id
フィルタ/ソート兼用インデックス
add_index :posts, :created_at
ソート用インデックス(apiOptions.sortable: true から導出)
add_index :posts, :updated_at add_index :posts, :title
全文検索用(apiOptions.searchable: true から導出)
add_column :posts, :searchable, :tsvector add_index :posts, :searchable, using: :gin
全文検索トリガー
execute <<-SQL CREATE OR REPLACE FUNCTION posts_searchable_trigger() RETURNS trigger AS $$ BEGIN NEW.searchable := setweight(to_tsvector('japanese', coalesce(NEW.title, '')), 'A') || setweight(to_tsvector('japanese', coalesce(NEW.content, '')), 'B'); RETURN NEW; END $$ LANGUAGE plpgsql;
CREATE TRIGGER posts_searchable_update BEFORE INSERT OR UPDATE ON posts FOR EACH ROW EXECUTE FUNCTION posts_searchable_trigger(); SQL end end
ステップ7: プロジェクトを初期化する
目次
- 目的
- 開発環境
- 方式A: Docker環境でのセットアップ(推奨)
- A.0 空のプロジェクトからのセットアップ
- A.1 既存のRailsプロジェクトのセットアップ
- A.2 Gemを追加する
- A.3 データベースを設定する
- A.4 API設定を行う
- 方式B: ローカル環境でのセットアップ
- 出力
---
目的
Rails 8.1プロジェクトを作成し、必要な設定とGemをセットアップする。
開発環境
このスキルではDocker Compose環境を標準として使用する。
重要: ステップ2で作成したDockerfile/docker-compose.ymlを使用してプロジェクトを初期化する。
---
方式A: Docker環境でのセットアップ(推奨)
A.0 空のプロジェクトからのセットアップ
Gemfile/Gemfile.lockが存在しない新規プロジェクトの場合、以下の手順で初期化する:
1. 最小限のGemfileを作成する
# Gemfile
source "https://rubygems.org"
gem "rails", "~> 8.1"2. 空のGemfile.lockを作成する
touch Gemfile.lock3. Dockerイメージをビルドする
docker compose build4. bundle installを実行する
docker compose run --rm web bundle install5. rails newを実行する
# 管理画面を含む場合(推奨)
docker compose run --rm web bundle exec rails new . \
--database=postgresql \
--skip-test \
--css=tailwind \
--force
# APIのみの場合
docker compose run --rm web bundle exec rails new . \
--api \
--database=postgresql \
--skip-test \
--force6. ファイル権限を修正する(必要な場合)
# rootで作成されたファイルの権限を修正
sudo chown -R $(id -u):$(id -g) .7. 再度bundle installを実行する
docker compose run --rm web bundle install8. データベースを作成する
docker compose run --rm web bundle exec rails db:create---
A.1 Dockerfile.devを作成する(推奨)
vendor/bundleをローカルに配置することで、ファイル権限問題を完全に回避する方式:
# Dockerfile.dev
FROM ruby:3.4-slim
# 必要なパッケージをインストール
# 重要: libyaml-dev はpsychゲムのビルドに必須
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y \
build-essential \
git \
libpq-dev \
libyaml-dev \
pkg-config \
nodejs \
npm \
curl \
postgresql-client \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# yarnをインストール(Tailwind CSS等で必要)
RUN npm install -g yarn
# 作業ディレクトリを設定
WORKDIR /app
# bundlerの設定(ローカルvendor/bundleを使用)
# これにより、gemがプロジェクト内に保存され、権限問題を回避
ENV BUNDLE_PATH=/app/vendor/bundle
ENV BUNDLE_BIN=/app/vendor/bundle/bin
ENV PATH="${BUNDLE_BIN}:${PATH}"
EXPOSE 3000
CMD ["bash", "-c", "rm -f tmp/pids/server.pid && bundle exec rails server -b 0.0.0.0"]利点:
- UID/GIDの設定が不要
- gemがプロジェクト内に保存され、ホストからも参照可能
.gitignoreにvendor/bundleを追加して管理
A.2 docker-compose.ymlを作成する
services:
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: app_development
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
# ポート5432が既に使用されている場合は5433等に変更
- "${DB_PORT:-5432}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
web:
build:
context: .
dockerfile: Dockerfile.dev
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails server -b 0.0.0.0"
volumes:
- .:/app
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:password@db:5432/app_development
RAILS_ENV: development
tty: true
stdin_open: true
volumes:
postgres_data:A.3 ポート衝突への対応
PostgreSQLのデフォルトポート5432が既に使用されている場合:
# .envファイルでポートを変更
echo "DB_PORT=5433" >> .envまたは、docker-compose.ymlで直接指定:
ports:
- "5433:5432"A.4 .gitignoreの設定
# vendor/bundleをGit管理から除外
/vendor/bundle
# その他Docker関連
.envA.5 既存ファイルの権限を修正
既にrootで作成されたファイルがある場合:
# ホスト側で権限を変更
sudo chown -R $(id -u):$(id -g) .
# または、コンテナ内でrootとして実行
docker compose exec -u root web chown -R 1000:1000 /appA.4 Dockerでプロジェクトを作成する
# イメージをビルド
docker compose build
# Railsプロジェクトを生成(管理画面を含む場合)
docker compose run --rm web rails new . \
--database=postgresql \
--skip-test \
--css=tailwind \
--force
# APIのみの場合
docker compose run --rm web rails new . \
--api \
--database=postgresql \
--skip-test \
--force
# コンテナを起動
docker compose up -d
# DBを作成
docker compose exec web rails db:createA.5 Docker開発での注意点
| 項目 | コマンド |
|---|---|
| railsコマンド実行 | docker compose exec web rails ... |
| bundleコマンド | docker compose exec web bundle ... |
| コンソール | docker compose exec web rails console |
| ログ確認 | docker compose logs -f web |
| コンテナ再起動 | docker compose restart web |
---
方式B: ローカル環境でのセットアップ
B.1 Railsプロジェクトを作成する
rails new project_name \
--api \
--database=postgresql \
--skip-test \
--skip-action-mailbox \
--skip-action-text \
--skip-active-storage \
--skip-action-cableオプション説明
| オプション | 説明 |
|---|---|
--api | APIモードで作成(View関連を除外) |
--database=postgresql | PostgreSQLを使用 |
--skip-test | デフォルトのテストをスキップ(RSpecを使用) |
| その他skip | 不要な機能を除外 |
管理画面を含める場合(通常モード)
rails new project_name \
--database=postgresql \
--skip-test \
--css=tailwind注意: 通常モード(--apiなし)で作成した場合、APIコントローラでCSRF保護を無効化する必要がある。詳細はステップ10参照。
---
共通設定(Docker/ローカル共通)
6.2 Gemfileを編集する
# Gemfile
# API
gem 'alba' # JSONシリアライザ
gem 'kaminari' # ページネーション
gem 'ransack' # 検索・フィルタリング
gem 'rack-cors' # CORS対応
# 管理画面(ActiveAdminを使用する場合)
gem 'activeadmin'
gem 'devise'
gem 'dartsass-rails' # ActiveAdminがSassに依存
group :development, :test do
gem 'rspec-rails' # テストフレームワーク
gem 'factory_bot_rails' # テストデータ生成
gem 'faker' # ダミーデータ生成
gem 'rubocop-rails-omakase', require: false # Linter
end
group :development do
gem 'annotate' # モデルにスキーマ情報追加
end注意: ActiveAdminはSassコンパイラを必要とするため、dartsass-railsを追加する。
6.3 Gemをインストールする
bundle install6.4 RSpecをセットアップする
rails generate rspec:install6.5 データベースを作成する
config/database.yml を確認し、必要に応じて編集:
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: project_name_development
test:
<<: *default
database: project_name_test
production:
<<: *default
database: project_name_production
username: project_name
password: <%= ENV["PROJECT_NAME_DATABASE_PASSWORD"] %>rails db:create6.6 CORSを設定する(API利用時)
config/initializers/cors.rb:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*' # 本番環境では適切に制限する
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end6.7 ディレクトリ構成を確認する
app/
├── controllers/
│ └── api/
│ └── v1/ # APIバージョニング
├── models/
├── serializers/ # Alba用(作成)
└── views/ # 管理画面用(必要な場合)
config/
db/
spec/APIバージョニング用のディレクトリを作成:
mkdir -p app/controllers/api/v1
mkdir -p app/serializers6.8 ベースコントローラを作成する
app/controllers/api/v1/base_controller.rb:
module Api
module V1
class BaseController < ApplicationController
include Pagy::Backend
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
private
def not_found
render json: { error: 'Not Found' }, status: :not_found
end
def unprocessable_entity(exception)
render json: { errors: exception.record.errors }, status: :unprocessable_entity
end
end
end
end6.9 ルーティングの基本設定
config/routes.rb:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# リソースはここに追加
end
end
end出力
- Railsプロジェクトが作成され、起動可能な状態
rails serverでサーバーが起動することを確認
ステップ8: DBマイグレーションを実装する
目次
- 目的
- 手順
- 7.1 マイグレーションファイルを生成する
- 7.2 マイグレーションを実装する
- 7.3 全文検索用マイグレーションを実装する
- 7.4 カスタム型のカラムを実装する
- 7.5 Enumカラムのチェック制約を追加
- 7.6 マイグレーションを実行する
- 7.7 マイグレーションの命名規則
- 7.8 ロールバック対応
- トラブルシューティング
- 出力
---
目的
ステップ5, 6で設計したスキーマとインデックスをRailsマイグレーションとして実装する。
手順
7.1 マイグレーションファイルを生成する
各モデルごとにマイグレーションを生成:
rails generate migration CreatePosts7.2 マイグレーションを実装する
db/migrate/YYYYMMDDHHMMSS_create_posts.rb:
class CreatePosts < ActiveRecord::Migration[8.1]
def change
# UUID拡張を有効化(必要な場合)
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
create_table :posts, id: :uuid do |t|
t.string :title, null: false
t.text :content
t.string :status, null: false, default: 'draft'
t.references :author, type: :uuid, foreign_key: { to_table: :users }, null: false
t.timestamps
end
# 通常インデックス
add_index :posts, :status
add_index :posts, :created_at
add_index :posts, [:status, :created_at]
end
end7.3 全文検索用マイグレーションを実装する
テキスト検索が必要なテーブルに対して:
class AddSearchableToPosts < ActiveRecord::Migration[8.1]
def up
# tsvectorカラムを追加
add_column :posts, :searchable, :tsvector
# GINインデックスを作成
add_index :posts, :searchable, using: :gin
# トリガー関数を作成
execute <<-SQL
CREATE OR REPLACE FUNCTION posts_searchable_trigger() RETURNS trigger AS $$
BEGIN
NEW.searchable :=
setweight(to_tsvector('japanese', coalesce(NEW.title, '')), 'A') ||
setweight(to_tsvector('japanese', coalesce(NEW.content, '')), 'B');
RETURN NEW;
END
$$ LANGUAGE plpgsql;
SQL
# トリガーを作成
execute <<-SQL
CREATE TRIGGER posts_searchable_update
BEFORE INSERT OR UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION posts_searchable_trigger();
SQL
# 既存データを更新
execute <<-SQL
UPDATE posts SET searchable =
setweight(to_tsvector('japanese', coalesce(title, '')), 'A') ||
setweight(to_tsvector('japanese', coalesce(content, '')), 'B');
SQL
end
def down
execute "DROP TRIGGER IF EXISTS posts_searchable_update ON posts"
execute "DROP FUNCTION IF EXISTS posts_searchable_trigger()"
remove_column :posts, :searchable
end
end7.4 カスタム型のカラムを実装する
カスタム型は展開してカラム化:
class AddSeoSettingsToPosts < ActiveRecord::Migration[8.1]
def change
add_column :posts, :seo_settings_title, :string
add_column :posts, :seo_settings_description, :text
end
end7.5 Enumカラムのチェック制約を追加(オプション)
データベースレベルでのEnum値の制約:
class AddStatusCheckConstraintToPosts < ActiveRecord::Migration[8.1]
def up
execute <<-SQL
ALTER TABLE posts
ADD CONSTRAINT posts_status_check
CHECK (status IN ('draft', 'published', 'archived'));
SQL
end
def down
execute <<-SQL
ALTER TABLE posts DROP CONSTRAINT posts_status_check;
SQL
end
end7.6 マイグレーションを実行する
# マイグレーションを実行
rails db:migrate
# ステータス確認
rails db:migrate:status
# スキーマ確認
rails db:schema:dump7.7 マイグレーションの命名規則
| 操作 | 命名パターン |
|---|---|
| テーブル作成 | CreateTableName |
| カラム追加 | AddColumnNameToTableName |
| カラム削除 | RemoveColumnNameFromTableName |
| インデックス追加 | AddIndexToTableName |
| 参照追加 | AddReferenceToTableName |
7.8 ロールバック対応
change メソッドで自動的にロールバック可能な操作を使用。 複雑な操作は up / down メソッドを分けて実装。
def up
# 適用時の処理
end
def down
# ロールバック時の処理
endトラブルシューティング
8.1 テーブル/インデックスが既に存在するエラー
エラー: PG::DuplicateTable: ERROR: relation "xxx" already exists
マイグレーションが途中で失敗した後、再実行した場合に発生する。
解決方法1: データベースをリセット(開発環境のみ)
# データベースを削除して再作成
docker compose exec web rails db:drop db:create db:migrate
# または
docker compose exec web rails db:migrate:reset解決方法2: 問題のあるマイグレーションを手動で調整
# マイグレーション状態を確認
docker compose exec web rails db:migrate:status
# 特定のマイグレーションのステータスを変更(upに設定)
docker compose exec web rails db:migrate:up VERSION=20241128123456解決方法3: 条件付きでテーブル作成
class CreateUsers < ActiveRecord::Migration[8.1]
def change
# テーブルが存在しない場合のみ作成
unless table_exists?(:users)
create_table :users, id: :uuid do |t|
t.string :name
t.timestamps
end
end
end
end8.2 インデックス重複エラー
エラー: PG::DuplicateObject: ERROR: relation "index_xxx" already exists
class AddIndexToUsers < ActiveRecord::Migration[8.1]
def change
# インデックスが存在しない場合のみ作成
unless index_exists?(:users, :email)
add_index :users, :email, unique: true
end
end
end8.3 外部キー制約エラー
エラー: テーブルの作成順序が原因で外部キーが設定できない
解決方法: 外部キーを別マイグレーションで追加
# 1. まずテーブルを作成(外部キーなし)
class CreatePosts < ActiveRecord::Migration[8.1]
def change
create_table :posts, id: :uuid do |t|
t.uuid :author_id # 外部キー制約なしで作成
t.timestamps
end
end
end
# 2. 依存テーブル作成後に外部キーを追加
class AddForeignKeysToPosts < ActiveRecord::Migration[8.1]
def change
add_foreign_key :posts, :users, column: :author_id
end
end8.4 マイグレーションの状態を確認・修復
# 現在のマイグレーション状態を確認
docker compose exec web rails db:migrate:status
# 出力例:
# Status Migration ID Migration Name
# --------------------------------------------------
# up 20241128050000 Enable pgcrypto extension
# up 20241128050100 Create users
# down 20241128050200 Create posts # ← 失敗している
# 特定バージョンまでロールバック
docker compose exec web rails db:rollback STEP=1
# 特定バージョンを実行
docker compose exec web rails db:migrate:up VERSION=202411280502008.5 スキーマの差分を確認
# 現在のスキーマをダンプ
docker compose exec web rails db:schema:dump
# スキーマの差分を確認(Git使用時)
git diff db/schema.rb出力
- 全テーブルのマイグレーションファイルが作成されている
rails db:migrateが正常に完了するdb/schema.rbが期待通りのスキーマを反映している
ステップ10: バリデーションを実装する
目次
- 目的
- 手順
- 9.1 JSON仕様とRailsバリデーションのマッピング
- 9.2 基本バリデーションの実装
- 9.3 Enum値のバリデーション
- 9.4 日本語enumValuesの対応方法
- 9.5 リレーションのバリデーション
- 9.6 カスタムバリデーション
- 9.7 エラーメッセージのカスタマイズ
- 出力
---
目的
JSON仕様のvalidation設定に基づいて、Active Recordバリデーションを実装する。
手順
9.1 JSON仕様とRailsバリデーションのマッピング
| JSON仕様 | Railsバリデーション |
|---|---|
required: true | validates :field, presence: true |
min (数値) | validates :field, numericality: { greater_than_or_equal_to: min } |
max (数値) | validates :field, numericality: { less_than_or_equal_to: max } |
min (文字列) | validates :field, length: { minimum: min } |
max (文字列) | validates :field, length: { maximum: max } |
pattern | validates :field, format: { with: /pattern/ } |
unique: true | validates :field, uniqueness: true |
9.2 基本バリデーションの実装
class Post < ApplicationRecord
# 必須
validates :title, presence: true
validates :status, presence: true
# 文字列長
validates :title, length: { maximum: 255 }
validates :content, length: { maximum: 65535 }, allow_blank: true
# 数値
validates :view_count, numericality: {
only_integer: true,
greater_than_or_equal_to: 0
}, allow_nil: true
# ユニーク
validates :slug, uniqueness: true, allow_blank: true
# フォーマット
validates :email, format: {
with: URI::MailTo::EMAIL_REGEXP,
message: 'は有効なメールアドレス形式で入力してください'
}, allow_blank: true
end9.3 Enum値のバリデーション
英語キーの場合(推奨)
class Post < ApplicationRecord
enum :status, {
draft: 'draft',
published: 'published',
archived: 'archived'
}
# Enumは自動的にバリデーションされるが、明示的に追加も可能
validates :status, inclusion: { in: statuses.keys }
end日本語enumValuesの対応方法
JSON仕様で enumValues: ["ドラフト", "公開"] のように日本語が定義されている場合、以下の方法で対応する:
方法1: 英語キーを使用し、i18nで日本語表示(推奨)
# app/models/article.rb
class Article < ApplicationRecord
# DBには英語キーで保存
enum :publish_status, {
draft: 'draft',
published: 'published'
}
end# config/locales/ja.yml
ja:
activerecord:
attributes:
article:
publish_status: 公開ステータス
enums:
article:
publish_status:
draft: ドラフト
published: 公開# 表示時
Article.human_attribute_name("publish_status.#{article.publish_status}")
# => "ドラフト" または "公開"
# フォームでの選択肢
Article.publish_statuses.keys.map { |k| [Article.human_attribute_name("publish_status.#{k}"), k] }
# => [["ドラフト", "draft"], ["公開", "published"]]方法2: string型でinclusion validationを使用
日本語の値をそのままDBに保存する場合:
# app/models/article.rb
class Article < ApplicationRecord
PUBLISH_STATUSES = %w[ドラフト 公開].freeze
validates :publish_status, inclusion: {
in: PUBLISH_STATUSES,
message: "は「#{PUBLISH_STATUSES.join('」「')}」のいずれかを選択してください"
}, allow_blank: true
# スコープ
scope :draft, -> { where(publish_status: 'ドラフト') }
scope :published, -> { where(publish_status: '公開') }
def draft?
publish_status == 'ドラフト'
end
def published?
publish_status == '公開'
end
end注意: 方法2はDBに日本語が保存されるため、将来の多言語対応が困難になります。特別な理由がない限り、方法1(英語キー + i18n)を推奨します。
enumとinclusion validationの衝突を避ける
Rails enumを使用する場合、enumが自動的にバリデーションを行うため、別途inclusion validationを追加すると衝突する可能性があります:
# NG: enumとinclusionを併用するとエラーになる場合がある
enum :status, { draft: 'draft', published: 'published' }
validates :status, inclusion: { in: %w[draft published] } # 不要
# OK: enumのみ使用
enum :status, { draft: 'draft', published: 'published' }9.4 リレーションのバリデーション
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
# belongs_toはRails 5以降デフォルトでrequired
# optional: true で任意に変更可能
belongs_to :category, optional: true
# 存在確認を明示的に
validates :author, presence: true
end9.5 カスタムバリデーションの実装
class Post < ApplicationRecord
validate :published_at_must_be_in_past, if: :published?
private
def published_at_must_be_in_past
if published_at.present? && published_at > Time.current
errors.add(:published_at, '公開日は現在時刻より前である必要があります')
end
end
end9.6 条件付きバリデーション
class Post < ApplicationRecord
# 公開時のみ必須
validates :content, presence: true, if: :published?
# 下書き以外は必須
validates :slug, presence: true, unless: :draft?
end9.7 ネストした属性のバリデーション(カスタム型)
class Post < ApplicationRecord
# カスタム型のバリデーション
validate :validate_seo_settings
private
def validate_seo_settings
if seo_settings_title.present? && seo_settings_title.length > 60
errors.add(:seo_settings_title, 'は60文字以内で入力してください')
end
if seo_settings_description.present? && seo_settings_description.length > 160
errors.add(:seo_settings_description, 'は160文字以内で入力してください')
end
end
end9.8 バリデーションヘルパーの作成
共通のバリデーションロジックをconcernに抽出:
# app/models/concerns/validatable.rb
module Validatable
extend ActiveSupport::Concern
class_methods do
def validates_json_spec(field, spec)
validations = {}
validations[:presence] = true if spec[:required]
if spec[:min] || spec[:max]
if column_for_attribute(field).type == :string
validations[:length] = {}
validations[:length][:minimum] = spec[:min] if spec[:min]
validations[:length][:maximum] = spec[:max] if spec[:max]
else
validations[:numericality] = {}
validations[:numericality][:greater_than_or_equal_to] = spec[:min] if spec[:min]
validations[:numericality][:less_than_or_equal_to] = spec[:max] if spec[:max]
end
end
validations[:format] = { with: Regexp.new(spec[:pattern]) } if spec[:pattern]
validations[:uniqueness] = true if spec[:unique]
validates field, validations unless validations.empty?
end
end
end9.9 エラーメッセージのカスタマイズ
config/locales/ja.yml:
ja:
activerecord:
errors:
messages:
blank: を入力してください
too_short: は%{count}文字以上で入力してください
too_long: は%{count}文字以内で入力してください
invalid: は不正な値です
taken: はすでに使用されています
models:
post:
attributes:
title:
blank: タイトルを入力してください出力
- 全モデルにJSON仕様に基づくバリデーションが実装されている
rails consoleでmodel.valid?が正しく動作する- エラーメッセージが適切に表示される