mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-02-03 10:14:07 +08:00
Add complete Japanese translation for all documentation pages including: - Home and about pages - Deployment guides (Docker CLI, Docker Compose, DSM, Local) - Configuration pages (RSS, Downloader, Parser, Notifier, Manager, Proxy, Experimental) - Feature documentation (RSS Management, Bangumi, Calendar, Rename, Search) - FAQ and troubleshooting - API reference - Changelogs (2.6, 3.0, 3.1, 3.2) - Developer guide Also updates VitePress config to add Japanese locale with full sidebar navigation. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
395 lines
13 KiB
Markdown
395 lines
13 KiB
Markdown
# データベース開発者ガイド
|
||
|
||
このガイドでは、AutoBangumiのデータベースアーキテクチャ、モデル、および操作について説明します。
|
||
|
||
## 概要
|
||
|
||
AutoBangumiはデータベースとして**SQLite**を使用し、ORMには**SQLModel**(Pydantic + SQLAlchemyハイブリッド)を使用しています。データベースファイルは`data/data.db`にあります。
|
||
|
||
### アーキテクチャ
|
||
|
||
```
|
||
module/database/
|
||
├── engine.py # SQLAlchemyエンジン設定
|
||
├── combine.py # Databaseクラス、マイグレーション、セッション管理
|
||
├── bangumi.py # Bangumi(アニメ購読)操作
|
||
├── rss.py # RSSフィード操作
|
||
├── torrent.py # トレント追跡操作
|
||
└── user.py # ユーザー認証操作
|
||
```
|
||
|
||
## コアコンポーネント
|
||
|
||
### Databaseクラス
|
||
|
||
`combine.py`の`Database`クラスがメインエントリーポイントです。SQLModelの`Session`を継承し、すべてのサブデータベースへのアクセスを提供します:
|
||
|
||
```python
|
||
from module.database import Database
|
||
|
||
with Database() as db:
|
||
# サブデータベースへのアクセス
|
||
bangumis = db.bangumi.search_all()
|
||
rss_items = db.rss.search_active()
|
||
torrents = db.torrent.search_all()
|
||
```
|
||
|
||
### サブデータベースクラス
|
||
|
||
| クラス | モデル | 目的 |
|
||
|-------|-------|------|
|
||
| `BangumiDatabase` | `Bangumi` | アニメ購読ルール |
|
||
| `RSSDatabase` | `RSSItem` | RSSフィードソース |
|
||
| `TorrentDatabase` | `Torrent` | ダウンロードしたトレントの追跡 |
|
||
| `UserDatabase` | `User` | 認証 |
|
||
|
||
## モデル
|
||
|
||
### Bangumiモデル
|
||
|
||
アニメ購読のコアモデル:
|
||
|
||
```python
|
||
class Bangumi(SQLModel, table=True):
|
||
id: int # 主キー
|
||
official_title: str # 表示名(例:"無職転生")
|
||
title_raw: str # トレントマッチング用の生タイトル(インデックス付き)
|
||
season: int = 1 # シーズン番号
|
||
episode_offset: int = 0 # エピソード番号調整
|
||
season_offset: int = 0 # シーズン番号調整
|
||
rss_link: str # カンマ区切りRSSフィードURL
|
||
filter: str # 除外フィルター(例:"720,\\d+-\\d+")
|
||
poster_link: str # TMDBポスターURL
|
||
save_path: str # ダウンロード先パス
|
||
rule_name: str # qBittorrent RSSルール名
|
||
added: bool = False # ルールがダウンローダーに追加されたかどうか
|
||
deleted: bool = False # ソフト削除フラグ(インデックス付き)
|
||
archived: bool = False # 完結シリーズ用(インデックス付き)
|
||
needs_review: bool = False # オフセット不一致検出
|
||
needs_review_reason: str # レビューの理由
|
||
suggested_season_offset: int # 提案されたシーズンオフセット
|
||
suggested_episode_offset: int # 提案されたエピソードオフセット
|
||
air_weekday: int # 放送日(0=日曜日、6=土曜日)
|
||
```
|
||
|
||
### RSSItemモデル
|
||
|
||
RSSフィード購読:
|
||
|
||
```python
|
||
class RSSItem(SQLModel, table=True):
|
||
id: int # 主キー
|
||
name: str # 表示名
|
||
url: str # フィードURL(ユニーク、インデックス付き)
|
||
aggregate: bool = True # トレントを解析するかどうか
|
||
parser: str = "mikan" # パーサータイプ:mikan、dmhy、nyaa
|
||
enabled: bool = True # アクティブフラグ
|
||
connection_status: str # "healthy"または"error"
|
||
last_checked_at: str # ISOタイムスタンプ
|
||
last_error: str # 最後のエラーメッセージ
|
||
```
|
||
|
||
### Torrentモデル
|
||
|
||
ダウンロードしたトレントを追跡:
|
||
|
||
```python
|
||
class Torrent(SQLModel, table=True):
|
||
id: int # 主キー
|
||
name: str # トレント名(インデックス付き)
|
||
url: str # トレント/マグネットURL(ユニーク、インデックス付き)
|
||
rss_id: int # ソースRSSフィードID
|
||
bangumi_id: int # リンクされたBangumi ID(nullable)
|
||
qb_hash: str # qBittorrentインフォハッシュ(インデックス付き)
|
||
downloaded: bool = False # ダウンロード完了
|
||
```
|
||
|
||
## 一般的な操作
|
||
|
||
### BangumiDatabase
|
||
|
||
```python
|
||
with Database() as db:
|
||
# 作成
|
||
db.bangumi.add(bangumi) # 単一挿入
|
||
db.bangumi.add_all(bangumi_list) # バッチ挿入(重複排除)
|
||
|
||
# 読み取り
|
||
db.bangumi.search_all() # 全レコード(キャッシュ、5分TTL)
|
||
db.bangumi.search_id(123) # IDで検索
|
||
db.bangumi.match_torrent("torrent name") # title_rawマッチで検索
|
||
db.bangumi.not_complete() # 未完了シリーズ
|
||
db.bangumi.get_needs_review() # レビューフラグ付き
|
||
|
||
# 更新
|
||
db.bangumi.update(bangumi) # 単一レコード更新
|
||
db.bangumi.update_all(bangumi_list) # バッチ更新
|
||
|
||
# 削除
|
||
db.bangumi.delete_one(123) # ハード削除
|
||
db.bangumi.disable_rule(123) # ソフト削除(deleted=True)
|
||
```
|
||
|
||
### RSSDatabase
|
||
|
||
```python
|
||
with Database() as db:
|
||
# 作成
|
||
db.rss.add(rss_item) # 単一挿入
|
||
db.rss.add_all(rss_items) # バッチ挿入(重複排除)
|
||
|
||
# 読み取り
|
||
db.rss.search_all() # 全フィード
|
||
db.rss.search_active() # 有効なフィードのみ
|
||
db.rss.search_aggregate() # 有効 + aggregate=True
|
||
|
||
# 更新
|
||
db.rss.update(id, rss_update) # 部分更新
|
||
db.rss.enable(id) # フィード有効化
|
||
db.rss.disable(id) # フィード無効化
|
||
db.rss.enable_batch([1, 2, 3]) # バッチ有効化
|
||
db.rss.disable_batch([1, 2, 3]) # バッチ無効化
|
||
```
|
||
|
||
### TorrentDatabase
|
||
|
||
```python
|
||
with Database() as db:
|
||
# 作成
|
||
db.torrent.add(torrent) # 単一挿入
|
||
db.torrent.add_all(torrents) # バッチ挿入
|
||
|
||
# 読み取り
|
||
db.torrent.search_all() # 全トレント
|
||
db.torrent.search_by_qb_hash(hash) # qBittorrentハッシュで検索
|
||
db.torrent.search_by_url(url) # URLで検索
|
||
db.torrent.check_new(torrents) # 既存のものをフィルター
|
||
|
||
# 更新
|
||
db.torrent.update_qb_hash(id, hash) # qb_hashを設定
|
||
```
|
||
|
||
## キャッシング
|
||
|
||
### Bangumiキャッシュ
|
||
|
||
`search_all()`の結果はモジュールレベルで5分のTTLでキャッシュされます:
|
||
|
||
```python
|
||
# bangumi.pyのモジュールレベルキャッシュ
|
||
_bangumi_cache: list[Bangumi] | None = None
|
||
_bangumi_cache_time: float = 0
|
||
_BANGUMI_CACHE_TTL: float = 300.0 # 5分
|
||
|
||
# キャッシュ無効化
|
||
def _invalidate_bangumi_cache():
|
||
global _bangumi_cache, _bangumi_cache_time
|
||
_bangumi_cache = None
|
||
_bangumi_cache_time = 0
|
||
```
|
||
|
||
**重要:** キャッシュは以下で自動的に無効化されます:
|
||
- `add()`、`add_all()`
|
||
- `update()`、`update_all()`
|
||
- `delete_one()`、`delete_all()`
|
||
- `archive_one()`、`unarchive_one()`
|
||
- 任意のRSSリンク更新操作
|
||
|
||
### セッションExpunge
|
||
|
||
キャッシュされたオブジェクトは`DetachedInstanceError`を防ぐためにセッションから**expunge**されます:
|
||
|
||
```python
|
||
for b in bangumis:
|
||
self.session.expunge(b) # セッションから切り離す
|
||
```
|
||
|
||
## マイグレーションシステム
|
||
|
||
### スキーマバージョニング
|
||
|
||
マイグレーションは`schema_version`テーブルを介して追跡されます:
|
||
|
||
```python
|
||
CURRENT_SCHEMA_VERSION = 7
|
||
|
||
# 各マイグレーション:(バージョン、説明、[SQLステートメント])
|
||
MIGRATIONS = [
|
||
(1, "add air_weekday column", [...]),
|
||
(2, "add connection status columns", [...]),
|
||
(3, "create passkey table", [...]),
|
||
(4, "add archived column", [...]),
|
||
(5, "rename offset to episode_offset", [...]),
|
||
(6, "add qb_hash column", [...]),
|
||
(7, "add suggested offset columns", [...]),
|
||
]
|
||
```
|
||
|
||
### 新しいマイグレーションの追加
|
||
|
||
1. `combine.py`の`CURRENT_SCHEMA_VERSION`をインクリメント
|
||
2. `MIGRATIONS`リストにマイグレーションタプルを追加:
|
||
|
||
```python
|
||
MIGRATIONS = [
|
||
# ... 既存のマイグレーション ...
|
||
(
|
||
8,
|
||
"add my_new_column to bangumi",
|
||
[
|
||
"ALTER TABLE bangumi ADD COLUMN my_new_column TEXT DEFAULT NULL",
|
||
],
|
||
),
|
||
]
|
||
```
|
||
|
||
3. `run_migrations()`に冪等性チェックを追加:
|
||
|
||
```python
|
||
if "bangumi" in tables and version == 8:
|
||
columns = [col["name"] for col in inspector.get_columns("bangumi")]
|
||
if "my_new_column" in columns:
|
||
needs_run = False
|
||
```
|
||
|
||
4. `module/models/`の対応するPydanticモデルを更新
|
||
|
||
### デフォルト値のバックフィル
|
||
|
||
マイグレーション後、`_fill_null_with_defaults()`がモデルのデフォルトに基づいてNULL値を自動的に埋めます:
|
||
|
||
```python
|
||
# モデルが定義している場合:
|
||
class Bangumi(SQLModel, table=True):
|
||
my_field: bool = False
|
||
|
||
# NULLの既存行はFalseに更新されます
|
||
```
|
||
|
||
## パフォーマンスパターン
|
||
|
||
### バッチクエリ
|
||
|
||
`add_all()`は、Nクエリの代わりに単一のクエリを使用して重複をチェックします:
|
||
|
||
```python
|
||
# 効率的:単一SELECT
|
||
keys_to_check = [(d.title_raw, d.group_name) for d in datas]
|
||
conditions = [
|
||
and_(Bangumi.title_raw == tr, Bangumi.group_name == gn)
|
||
for tr, gn in keys_to_check
|
||
]
|
||
statement = select(Bangumi.title_raw, Bangumi.group_name).where(or_(*conditions))
|
||
```
|
||
|
||
### 正規表現マッチング
|
||
|
||
`match_list()`は、すべてのタイトルマッチ用に単一の正規表現パターンをコンパイルします:
|
||
|
||
```python
|
||
# 一度コンパイル、多くマッチ
|
||
sorted_titles = sorted(title_index.keys(), key=len, reverse=True)
|
||
pattern = "|".join(re.escape(title) for title in sorted_titles)
|
||
title_regex = re.compile(pattern)
|
||
|
||
# トレントごとにO(n)ではなくO(1)ルックアップ
|
||
for torrent in torrent_list:
|
||
match = title_regex.search(torrent.name)
|
||
```
|
||
|
||
### インデックス付きカラム
|
||
|
||
以下のカラムには高速ルックアップ用のインデックスがあります:
|
||
|
||
| テーブル | カラム | インデックスタイプ |
|
||
|---------|--------|------------------|
|
||
| `bangumi` | `title_raw` | 通常 |
|
||
| `bangumi` | `deleted` | 通常 |
|
||
| `bangumi` | `archived` | 通常 |
|
||
| `rssitem` | `url` | ユニーク |
|
||
| `torrent` | `name` | 通常 |
|
||
| `torrent` | `url` | ユニーク |
|
||
| `torrent` | `qb_hash` | 通常 |
|
||
|
||
## テスト
|
||
|
||
### テストデータベースセットアップ
|
||
|
||
テストはインメモリSQLiteデータベースを使用します:
|
||
|
||
```python
|
||
# conftest.py
|
||
@pytest.fixture
|
||
def db_engine():
|
||
engine = create_engine("sqlite:///:memory:")
|
||
SQLModel.metadata.create_all(engine)
|
||
yield engine
|
||
engine.dispose()
|
||
|
||
@pytest.fixture
|
||
def db_session(db_engine):
|
||
with Session(db_engine) as session:
|
||
yield session
|
||
```
|
||
|
||
### ファクトリ関数
|
||
|
||
テストデータ作成にはファクトリ関数を使用:
|
||
|
||
```python
|
||
from test.factories import make_bangumi, make_torrent, make_rss_item
|
||
|
||
def test_bangumi_search():
|
||
bangumi = make_bangumi(title_raw="Test Title", season=2)
|
||
# ... テストロジック
|
||
```
|
||
|
||
## 設計ノート
|
||
|
||
### 外部キーなし
|
||
|
||
SQLite外部キー強制はデフォルトで無効になっています。リレーションシップ(`Torrent.bangumi_id`など)はデータベース制約ではなくアプリケーションロジックで管理されます。
|
||
|
||
### ソフト削除
|
||
|
||
`Bangumi.deleted`フラグはソフト削除を可能にします。クエリはユーザー向けデータには`deleted=False`でフィルターする必要があります:
|
||
|
||
```python
|
||
statement = select(Bangumi).where(Bangumi.deleted == false())
|
||
```
|
||
|
||
### トレントタグ付け
|
||
|
||
トレントはリネーム操作中のオフセットルックアップ用にqBittorrentで`ab:{bangumi_id}`でタグ付けされます。これにより、データベースクエリなしで高速な番組識別が可能になります。
|
||
|
||
## 一般的な問題
|
||
|
||
### DetachedInstanceError
|
||
|
||
キャッシュされたオブジェクトを別のセッションからアクセスする場合:
|
||
|
||
```python
|
||
# 間違い:新しいセッションでキャッシュされたオブジェクトにアクセス
|
||
bangumis = db.bangumi.search_all() # キャッシュ済み
|
||
with Database() as new_db:
|
||
new_db.session.add(bangumis[0]) # エラー!
|
||
|
||
# 正しい:オブジェクトはexpungeされ、独立して動作
|
||
bangumis = db.bangumi.search_all()
|
||
bangumis[0].title_raw = "New Title" # OK、ただし永続化されない
|
||
```
|
||
|
||
### キャッシュの古さ
|
||
|
||
手動SQLアップデートがORMをバイパスする場合、キャッシュを無効化:
|
||
|
||
```python
|
||
from module.database.bangumi import _invalidate_bangumi_cache
|
||
|
||
with engine.connect() as conn:
|
||
conn.execute(text("UPDATE bangumi SET ..."))
|
||
conn.commit()
|
||
|
||
_invalidate_bangumi_cache() # 重要!
|
||
```
|