Files
Auto_Bangumi/docs/ja/dev/database.md
Estrella Pan 776abfa896 docs(4.0): 更新 API 参考与开发者文档,新增获取 SDK 页
- API 参考按实际路由重写,修正认证方式、请求方法与设置向导端点
- 数据库指南改为异步 Database 与 MIGRATIONS
- E2E 指南改为指向 e2e/README.md
- 插件快速上手补上安装 SDK 轮子的步骤,ab-plugin dev 加 --config-dir
- 发布流程改为手动推送标签触发
2026-10-08 17:36:53 +02:00

147 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# データベース開発ガイド
このページでは、AutoBangumi バックエンドのデータ層(エンジン、`Database` ファサード、リポジトリ、セッションの規約、スキーマのマイグレーション)を説明します。コードは `backend/src/module/database/` にあります。
## 概要
- データベースは SQLite ファイル `data/data.db`、ORM は SQLModel(Pydantic + SQLAlchemy)です。
- アプリケーション全体で非同期エンジン `sqlite+aiosqlite` だけを使います。各接続で `PRAGMA journal_mode=WAL`、`busy_timeout=5000`、`foreign_keys=ON` を設定します。複数の接続が同時に書き込め、競合した書き込みはすぐに `database is locked` を出さずに待ちます。
- 外部キー制約は有効です。ほかの行から参照されている行を削除する前に、参照している行を先に処理してください(例えば `BangumiDatabase.delete_all` は先にトレント記録を削除します)。
```
module/database/
├── __init__.py # Database、get_db(FastAPI の依存)
├── engine.py # async_engine、async_session_factory、接続ごとの PRAGMA
├── combine.py # Database:1 つのセッションとすべてのリポジトリ
├── migrations.py # MIGRATIONS テーブルとマイグレーションの実行
├── bangumi.py、rss.py、torrent.py、movie.py、user.py、auth.py、
├── inbox.py、llm_credential.py、rename_operation.py、aria2.py、
└── plugin_kv.py、passkey.py # テーブルごとのリポジトリ
```
## Database とセッション
`Database` は非同期コンテキストマネージャーです。`AsyncSession` を 1 つ作り、リポジトリを自身の属性として持ちます。`async with` ブロックを出るとセッションを閉じます。
```python
from module.database import Database
async with Database() as db:
rules = await db.bangumi.search_all()
feeds = await db.rss.search_all()
```
| 属性 | リポジトリ | テーブル |
| --- | --- | --- |
| `db.bangumi` | `BangumiDatabase` | `bangumi` |
| `db.rss` | `RSSDatabase` | `rssitem` |
| `db.torrent` | `TorrentDatabase` | `torrent` |
| `db.movie` | `MovieDatabase` | `movie` |
| `db.user` | `UserDatabase` | `user` |
| `db.auth` | `AuthDatabase` | `auth_session`、`api_token` |
| `db.inbox` | `InboxDatabase` | 通知センター |
| `db.llm_credential` | `LLMCredentialDatabase` | LLM のサブスクリプション認証情報 |
| `db.rename_operation` | `RenameOperationDatabase` | `rename_operation`(リネームの競合と進捗) |
| `db.aria2` | `Aria2GidDatabase` | `aria2_gid` |
| `db.plugin_kv` | `PluginKVDatabase` | `plugin_kv`(プラグインの `ctx.kv`) |
`PasskeyDatabase` は `Database` に含まれません。必要なときに `PasskeyDatabase(db.session)` で作ります。
`Database` はいくつかのセッションメソッドも転送します:`add`(同期。キューに入れるだけ)、`commit`、`rollback`、`refresh`、`close`。`begin_write()` は SQLite では `BEGIN IMMEDIATE` を実行します。「読んでから書き、その間に不変条件を保つ」場合に使い、そのセッションで最初にアクセスする前に呼び出します。
### セッションの規約
**1 つの操作に 1 つのセッション。** 1 回のリクエストや 1 回のループより長く生きるオブジェクトに、セッションや `Database` を保存しないでください。`AppContext` はセッションを持ちません。
- ルートでは依存性注入を使います。
```python
from fastapi import Depends
from module.database import Database, get_db
@router.get("/example")
async def example(db: Database = Depends(get_db)):
return await db.bangumi.search_all()
```
- 定期ループとサービスは、実行のたびに自分で開きます:`async with Database() as db:`。
- サービスは依存をコンストラクタで受け取ります:`RSSEngine(db)`、`TorrentManager(db)`。サービス内では `self.db.<リポジトリ>` を使い、リポジトリを 1 つだけ使う呼び出し側は `db.<リポジトリ>` を直接使います。
- セッションファクトリは `expire_on_commit=False` です。コミット後もオブジェクトの属性は読めますが、そのオブジェクトは新しいセッションには関連付いていません。変更するときは、新しいセッションでもう一度取得してください。
## リポジトリ
リポジトリは `AsyncSession` を受け取り、メソッドはすべて `async def` で、多くはメソッドの中でコミットします。クエリを追加するときは:
- そのテーブルのリポジトリに置きます。ルートやサービスに SQL を直接書かないでください。
- 複数行は `in_` で 1 回で取得します。ループの中で 1 行ずつ問い合わせないでください。
- 論理削除:`bangumi` と `movie` には `deleted` フィールドがあります。ルールの「無効化」(`disable_rule`)は `deleted` を `True` にするだけで、行を本当に削除するのは `delete_one` です。有効なルールを問い合わせるときは `deleted` で絞り込んでください。
### 複数のダウンローダー
4.0 はダウンローダーのインスタンスを複数持てます(設定 `plugins.instances`)。`bangumi`、`movie`、`rssitem`、`torrent` には `downloader_id` 列があります。
- ルールとフィードでは、空は「既定のインスタンスを使う」(`plugins.slots.downloader`)という意味です。
- トレント記録では、そのトレントが実際にあるインスタンスです。3.3 から引き継いだトレントは `default` です。
## スキーマのマイグレーション
マイグレーションは `migrations.py` の `MIGRATIONS` タプルにあり、テーブル駆動で実行されます。データベースの `schema_version` テーブルが適用済みのバージョンを記録します。
```python
Migration(
26,
"add downloader_id to bangumi, movie, rssitem and torrent",
(...SQL 文...),
already_applied=..., # ガード:このマイグレーションがすでに有効か
guarded_statements=(...), # 任意:文ごとのガード
)
```
起動時、AB はまず `SQLModel.metadata.create_all` を実行し、次に `run_migrations()` を実行します。
1. `schema_version` を読み、そのバージョン以下のマイグレーションを飛ばします。
2. 未適用のマイグレーションごとにデータベースの構造を調べ直し、ガードを呼びます。すでに有効な場合(例えば新しいデータベースを `create_all` がモデルから作った場合)は、バージョンだけを記録します。
3. そうでなければ、1 つの SAVEPOINT の中で文を実行します。どれかが失敗すると、そのマイグレーションをロールバックして例外を上に投げ、マイグレーションのトランザクション全体がロールバックされ、AB は起動に失敗します。AB は途中までしか移行していないスキーマでは動きません。
4. マイグレーションを実行した場合は、最後に既存の行の `NULL` をモデルの既定値で埋めます(`fill_null_with_defaults_conn`)。主キー、`default_factory` のフィールド、既定値が `None` の Optional フィールドは対象外です。
### マイグレーションを追加する
1. 先に `module/models/` のモデルを変更します。新しいデータベースは `create_all` で新しい構造になります。
2. `MIGRATIONS` の末尾に `Migration` を追加します。バージョンは直前の値に 1 を足したものです。`CURRENT_SCHEMA_VERSION` はリストから求まるので、手で変更しないでください。
3. `already_applied` ガードを指定します:`column_exists(テーブル, 列)`、`table_exists(テーブル)`、`index_exists(テーブル, インデックス)`。条件を組み合わせるには `all_checks(...)` を使います。1 つのマイグレーションで複数のテーブルを変更し、テーブルごとに状態が違う可能性がある場合は、`guarded_statements` で文ごとにガードを付けます。
4. `backend/src/test/` にマイグレーションのテストを追加します。古いスキーマから始め、マイグレーションを実行して結果を確認します。
```python
Migration(
27,
"add note to bangumi",
("ALTER TABLE bangumi ADD COLUMN note TEXT DEFAULT ''",),
column_exists("bangumi", "note"),
)
```
## テスト
`backend/src/test/conftest.py` にインメモリのデータベースがあります。
- `db_engine`:`sqlite+aiosqlite://` と `StaticPool`。テストごとに新しく作り、モデルからテーブルを作成します。
- `db_session`:`db_engine` に結び付いた `AsyncSession`。
リポジトリを直接テストするときは、`Database(engine=db_engine)` を使うか、`db_session` からリポジトリを 1 つ作ります。
```python
from module.database import Database
from module.database.bangumi import BangumiDatabase
from test.factories import make_bangumi
async def test_add_bangumi(db_engine):
async with Database(engine=db_engine) as db:
assert await db.bangumi.add(make_bangumi())
async def test_search(db_session):
repo = BangumiDatabase(db_session)
assert await repo.search_all() == []
```
バックエンドの pytest は `asyncio_mode = "auto"` なので、非同期のテスト関数に追加のデコレーターは不要です。実行するには:`cd backend && uv run pytest src/test/test_xxx.py -v`。