mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-10-29 19:57:14 +08:00
- API 参考按实际路由重写,修正认证方式、请求方法与设置向导端点 - 数据库指南改为异步 Database 与 MIGRATIONS - E2E 指南改为指向 e2e/README.md - 插件快速上手补上安装 SDK 轮子的步骤,ab-plugin dev 加 --config-dir - 发布流程改为手动推送标签触发
147 lines
9.7 KiB
Markdown
147 lines
9.7 KiB
Markdown
# データベース開発ガイド
|
||
|
||
このページでは、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`。
|