Fix migration
This commit is contained in:
205
MIGRATE_OLD_ROGDB_README.md
Normal file
205
MIGRATE_OLD_ROGDB_README.md
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
# Old RogDB → RogDB データ移行ガイド (エラー修正版)
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
old_rogdb データベースの `rog_*` テーブルから rogdb データベースの `rog_*` テーブルへデータを移行するスクリプトです。
|
||||||
|
|
||||||
|
## 修正点 (v3)
|
||||||
|
- PostgreSQL予約語(`like`など)のカラム名をクォートで囲む対応
|
||||||
|
- **キャメルケースカラム名**(`hasGoaled`, `deadlineDateTime`など)の自動クォート対応
|
||||||
|
- **NULL値の自動処理**:NOT NULL制約違反を防ぐデフォルト値設定
|
||||||
|
- トランザクションエラー時の自動ロールバック機能強化
|
||||||
|
- データベース接続のautocommit設定でトランザクション問題を回避
|
||||||
|
- より堅牢なエラーハンドリング
|
||||||
|
- カラム名事前チェック機能の追加
|
||||||
|
- NULL値事前チェック機能の追加
|
||||||
|
|
||||||
|
### NULL値デフォルト設定
|
||||||
|
以下のカラムで自動的にデフォルト値を設定:
|
||||||
|
- `trial`, `is_trial`: `False`
|
||||||
|
- `is_active`: `True`
|
||||||
|
- `hasGoaled`, `hasParticipated`: `False`
|
||||||
|
- `public`, `class_*`: `True`
|
||||||
|
- その他のBoolean型: 一般的なデフォルト値
|
||||||
|
|
||||||
|
## 機能
|
||||||
|
- 自動テーブル構造比較
|
||||||
|
- UPSERT操作(存在する場合は更新、しない場合は挿入)
|
||||||
|
- 主キーベースの重複チェック
|
||||||
|
- 詳細な移行統計レポート
|
||||||
|
- 予約語カラムの自動クォート処理
|
||||||
|
- エラーハンドリングとロールバック
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. Docker Compose での実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本実行
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# 環境変数を使用した実行
|
||||||
|
docker compose exec -e OLD_ROGDB_HOST=old-postgres app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# 特定テーブルを除外
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Makefileタスクの使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本移行
|
||||||
|
make migrate-old-rogdb
|
||||||
|
|
||||||
|
# カラム名チェックのみ
|
||||||
|
make check-columns
|
||||||
|
|
||||||
|
# NULL値チェックのみ
|
||||||
|
make check-null-values
|
||||||
|
|
||||||
|
# 完全な移行前チェック(カラム名 + NULL値)
|
||||||
|
make pre-migration-check
|
||||||
|
|
||||||
|
# 安全な移行(カラム名チェック + 移行実行)
|
||||||
|
make migrate-old-rogdb-safe
|
||||||
|
|
||||||
|
# 統計情報のみ表示
|
||||||
|
make migrate-rogdb-stats
|
||||||
|
|
||||||
|
# ドライラン(テーブル一覧のみ表示)
|
||||||
|
make migrate-rogdb-dryrun
|
||||||
|
```
|
||||||
|
|
||||||
|
## 環境変数
|
||||||
|
|
||||||
|
### Old RogDB 接続設定
|
||||||
|
```bash
|
||||||
|
OLD_ROGDB_HOST=postgres-db # デフォルト: postgres-db
|
||||||
|
OLD_ROGDB_NAME=old_rogdb # デフォルト: old_rogdb
|
||||||
|
OLD_ROGDB_USER=admin # デフォルト: admin
|
||||||
|
OLD_ROGDB_PASSWORD=admin123456 # デフォルト: admin123456
|
||||||
|
OLD_ROGDB_PORT=5432 # デフォルト: 5432
|
||||||
|
```
|
||||||
|
|
||||||
|
### RogDB 接続設定
|
||||||
|
```bash
|
||||||
|
ROGDB_HOST=postgres-db # デフォルト: postgres-db
|
||||||
|
ROGDB_NAME=rogdb # デフォルト: rogdb
|
||||||
|
ROGDB_USER=admin # デフォルト: admin
|
||||||
|
ROGDB_PASSWORD=admin123456 # デフォルト: admin123456
|
||||||
|
ROGDB_PORT=5432 # デフォルト: 5432
|
||||||
|
```
|
||||||
|
|
||||||
|
### その他の設定
|
||||||
|
```bash
|
||||||
|
EXCLUDE_TABLES=table1,table2 # 除外するテーブル(カンマ区切り)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 移行対象テーブル
|
||||||
|
|
||||||
|
スクリプトは `rog_` で始まる全てのテーブルを自動検出し、以下の処理を行います:
|
||||||
|
|
||||||
|
### 主要テーブル(例)
|
||||||
|
- `rog_customuser` - ユーザー情報
|
||||||
|
- `rog_newevent2` - イベント情報
|
||||||
|
- `rog_team` - チーム情報
|
||||||
|
- `rog_member` - メンバー情報
|
||||||
|
- `rog_entry` - エントリー情報
|
||||||
|
- `rog_location2025` - チェックポイント情報
|
||||||
|
- `rog_checkpoint` - チェックポイント記録
|
||||||
|
- その他 `rog_*` テーブル
|
||||||
|
|
||||||
|
### 移行ロジック
|
||||||
|
1. **テーブル構造比較**: 共通カラムのみを移行対象とする
|
||||||
|
2. **主キーチェック**: 既存レコードの有無を確認
|
||||||
|
3. **UPSERT操作**:
|
||||||
|
- 存在する場合: UPDATE(主キー以外のカラムを更新)
|
||||||
|
- 存在しない場合: INSERT(新規追加)
|
||||||
|
|
||||||
|
## 出力例
|
||||||
|
|
||||||
|
```
|
||||||
|
================================================================================
|
||||||
|
Old RogDB → RogDB データ移行開始
|
||||||
|
================================================================================
|
||||||
|
データベースに接続中...
|
||||||
|
✅ データベース接続成功
|
||||||
|
old_rogdb rog_テーブル: 15個
|
||||||
|
rogdb rog_テーブル: 15個
|
||||||
|
共通 rog_テーブル: 15個
|
||||||
|
移行対象テーブル (15個): ['rog_customuser', 'rog_newevent2', ...]
|
||||||
|
|
||||||
|
=== rog_customuser データ移行開始 ===
|
||||||
|
共通カラム (12個): ['date_joined', 'email', 'first_name', ...]
|
||||||
|
主キー: ['id']
|
||||||
|
移行対象レコード数: 50件
|
||||||
|
進捗: 50/50 件処理完了
|
||||||
|
✅ rog_customuser 移行完了:
|
||||||
|
挿入: 25件
|
||||||
|
更新: 25件
|
||||||
|
エラー: 0件
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
移行完了サマリー
|
||||||
|
================================================================================
|
||||||
|
処理対象テーブル: 15個
|
||||||
|
総挿入件数: 1250件
|
||||||
|
総更新件数: 750件
|
||||||
|
総エラー件数: 0件
|
||||||
|
|
||||||
|
--- テーブル別詳細 ---
|
||||||
|
rog_customuser: 挿入25, 更新25, エラー0
|
||||||
|
rog_newevent2: 挿入10, 更新5, エラー0
|
||||||
|
...
|
||||||
|
✅ 全ての移行が正常に完了しました!
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
1. **バックアップ推奨**: 移行前にrogdbのバックアップを取得してください
|
||||||
|
2. **権限確認**: 両データベースへの読み書き権限が必要です
|
||||||
|
3. **外部キー制約**: 移行順序によっては外部キー制約エラーが発生する可能性があります
|
||||||
|
4. **大量データ**: 大量データの場合は時間がかかる場合があります
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### よくあるエラー
|
||||||
|
|
||||||
|
#### 1. 接続エラー
|
||||||
|
```
|
||||||
|
❌ データベース接続エラー: connection refused
|
||||||
|
```
|
||||||
|
**対処法**: データベースサービスが起動していることを確認
|
||||||
|
|
||||||
|
#### 2. 権限エラー
|
||||||
|
```
|
||||||
|
❌ テーブル移行エラー: permission denied
|
||||||
|
```
|
||||||
|
**対処法**: データベースユーザーの権限を確認
|
||||||
|
|
||||||
|
#### 3. 外部キー制約エラー
|
||||||
|
```
|
||||||
|
❌ レコード処理エラー: foreign key constraint
|
||||||
|
```
|
||||||
|
**対処法**: 依存関係のあるテーブルから先に移行
|
||||||
|
|
||||||
|
### デバッグ方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ログレベルを上げて詳細情報を表示
|
||||||
|
docker compose exec app python -c "
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
exec(open('migrate_old_rogdb_to_rogdb.py').read())
|
||||||
|
"
|
||||||
|
|
||||||
|
# 特定テーブルのみテスト
|
||||||
|
docker compose exec app python -c "
|
||||||
|
from migrate_old_rogdb_to_rogdb import RogTableMigrator
|
||||||
|
migrator = RogTableMigrator()
|
||||||
|
migrator.connect_databases()
|
||||||
|
migrator.migrate_table_data('rog_customuser')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
このスクリプトはMITライセンスの下で公開されています。
|
||||||
148
MIGRATION_STATISTICS_README.md
Normal file
148
MIGRATION_STATISTICS_README.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# 移行結果統計情報表示スクリプト
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
|
||||||
|
移行処理の結果を詳細な統計情報として表示するスクリプトです。Docker Compose環境で実行可能で、移行データの品質チェックや分析に役立ちます。
|
||||||
|
|
||||||
|
## 実行方法
|
||||||
|
|
||||||
|
### 1. Docker Composeで実行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 統計情報を表示
|
||||||
|
docker compose exec app python migration_statistics.py
|
||||||
|
|
||||||
|
# または Makeタスクを使用
|
||||||
|
make migration-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 他の移行関連コマンド
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 移行実行
|
||||||
|
make migration-run
|
||||||
|
|
||||||
|
# Location2025移行
|
||||||
|
make migration-location2025
|
||||||
|
|
||||||
|
# データ保護移行
|
||||||
|
make migration-data-protection
|
||||||
|
|
||||||
|
# データベースシェル
|
||||||
|
make db-shell
|
||||||
|
|
||||||
|
# アプリケーションログ確認
|
||||||
|
make app-logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 表示される統計情報
|
||||||
|
|
||||||
|
### 📊 基本統計情報
|
||||||
|
- 各テーブルのレコード数
|
||||||
|
- 全体のデータ量概要
|
||||||
|
|
||||||
|
### 🎯 イベント別統計
|
||||||
|
- 登録イベント一覧
|
||||||
|
- イベント別参加チーム数、メンバー数、エントリー数
|
||||||
|
|
||||||
|
### 📍 GPSチェックイン統計
|
||||||
|
- 総チェックイン数、参加チーム数
|
||||||
|
- 時間帯別チェックイン分布
|
||||||
|
- CP利用ランキング(上位10位)
|
||||||
|
|
||||||
|
### 👥 チーム統計
|
||||||
|
- 総チーム数、クラス数
|
||||||
|
- クラス別チーム分布
|
||||||
|
- 平均メンバー数
|
||||||
|
|
||||||
|
### 🔍 データ品質チェック
|
||||||
|
- 重複データチェック
|
||||||
|
- 異常時刻データチェック
|
||||||
|
- データ整合性チェック
|
||||||
|
|
||||||
|
### 📄 JSON出力
|
||||||
|
- 統計情報をJSONファイルで出力
|
||||||
|
- 外部システムでの利用や保存に便利
|
||||||
|
|
||||||
|
## 出力例
|
||||||
|
|
||||||
|
```
|
||||||
|
================================================================================
|
||||||
|
📊 移行データ基本統計情報
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
📋 テーブル別レコード数:
|
||||||
|
テーブル名 日本語名 レコード数
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
rog_newevent2 イベント 12件
|
||||||
|
rog_team チーム 450件
|
||||||
|
rog_member メンバー 1,200件
|
||||||
|
rog_entry エントリー 450件
|
||||||
|
rog_gpscheckin GPSチェックイン 8,500件
|
||||||
|
rog_checkpoint チェックポイント 800件
|
||||||
|
rog_location2025 ロケーション2025 50件
|
||||||
|
rog_customuser ユーザー 25件
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
合計 11,487件
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
🎯 イベント別統計情報
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
📅 登録イベント数: 12件
|
||||||
|
|
||||||
|
イベント詳細:
|
||||||
|
ID イベント名 開催日 登録日時
|
||||||
|
------------------------------------------------------------
|
||||||
|
1 美濃加茂 2024-05-19 2024-08-25 10:30
|
||||||
|
2 岐阜市 2024-04-28 2024-08-25 10:30
|
||||||
|
3 大垣2 2024-04-20 2024-08-25 10:30
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### データベース接続エラー
|
||||||
|
```bash
|
||||||
|
# コンテナの状態確認
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# PostgreSQLログ確認
|
||||||
|
make db-logs
|
||||||
|
|
||||||
|
# アプリケーションログ確認
|
||||||
|
make app-logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 環境変数の確認
|
||||||
|
```bash
|
||||||
|
# 環境変数が正しく設定されているか確認
|
||||||
|
docker compose exec app env | grep POSTGRES
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手動でのデータベース接続テスト
|
||||||
|
```bash
|
||||||
|
# PostgreSQLコンテナに直接接続
|
||||||
|
docker compose exec postgres-db psql -U admin -d rogdb
|
||||||
|
|
||||||
|
# テーブル確認
|
||||||
|
\dt
|
||||||
|
|
||||||
|
# 基本的なクエリテスト
|
||||||
|
SELECT COUNT(*) FROM rog_gpscheckin;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 関連ファイル
|
||||||
|
|
||||||
|
- `migration_statistics.py` - 統計表示メインスクリプト
|
||||||
|
- `migration_final_simple.py` - GPS記録移行スクリプト
|
||||||
|
- `migration_location2025_support.py` - Location2025移行スクリプト
|
||||||
|
- `migration_data_protection.py` - データ保護移行スクリプト
|
||||||
|
- `Makefile` - 実行用タスク定義
|
||||||
|
|
||||||
|
## 注意事項
|
||||||
|
|
||||||
|
- Docker Composeが正常に起動していることを確認してください
|
||||||
|
- PostgreSQLコンテナが稼働していることを確認してください
|
||||||
|
- 統計情報は実行時点のデータベース状態を反映します
|
||||||
|
- JSON出力ファイルは `/app/` ディレクトリに保存されます
|
||||||
62
Makefile
62
Makefile
@ -31,3 +31,65 @@ volume:
|
|||||||
shell:
|
shell:
|
||||||
docker-compose exec api python3 manage.py shell
|
docker-compose exec api python3 manage.py shell
|
||||||
|
|
||||||
|
# 移行関連タスク
|
||||||
|
migration-stats:
|
||||||
|
docker compose exec app python migration_statistics.py
|
||||||
|
|
||||||
|
migration-run:
|
||||||
|
docker compose exec app python migration_final_simple.py
|
||||||
|
|
||||||
|
migration-location2025:
|
||||||
|
docker compose exec app python migration_location2025_support.py
|
||||||
|
|
||||||
|
migration-data-protection:
|
||||||
|
docker compose exec app python migration_data_protection.py
|
||||||
|
|
||||||
|
# Old RogDB → RogDB 移行
|
||||||
|
migrate-old-rogdb:
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# カラム名チェック
|
||||||
|
check-columns:
|
||||||
|
docker compose exec app python check_column_names.py
|
||||||
|
|
||||||
|
# NULL値チェック
|
||||||
|
check-null-values:
|
||||||
|
docker compose exec app python check_null_values.py
|
||||||
|
|
||||||
|
# 完全な移行前チェック
|
||||||
|
pre-migration-check:
|
||||||
|
@echo "=== カラム名チェック ==="
|
||||||
|
docker compose exec app python check_column_names.py
|
||||||
|
@echo "=== NULL値チェック ==="
|
||||||
|
docker compose exec app python check_null_values.py
|
||||||
|
|
||||||
|
# 移行前準備(カラム名チェック + 移行実行)
|
||||||
|
migrate-old-rogdb-safe:
|
||||||
|
@echo "=== カラム名チェック実行 ==="
|
||||||
|
docker compose exec app python check_column_names.py
|
||||||
|
@echo "=== 移行実行 ==="
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
migrate-old-rogdb-stats:
|
||||||
|
docker compose exec app python -c "from migrate_old_rogdb_to_rogdb import RogTableMigrator; m = RogTableMigrator(); m.connect_databases(); m.get_rog_tables()"
|
||||||
|
|
||||||
|
migrate-old-rogdb-dryrun:
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=all app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
migrate-old-rogdb-exclude-users:
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
|
||||||
|
# データベース関連
|
||||||
|
db-shell:
|
||||||
|
docker compose exec postgres-db psql -U $(POSTGRES_USER) -d $(POSTGRES_DBNAME)
|
||||||
|
|
||||||
|
db-backup:
|
||||||
|
docker compose exec postgres-db pg_dump -U $(POSTGRES_USER) $(POSTGRES_DBNAME) > backup_$(shell date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# ログ確認
|
||||||
|
app-logs:
|
||||||
|
docker compose logs app --tail=100 -f
|
||||||
|
|
||||||
|
db-logs:
|
||||||
|
docker compose logs postgres-db --tail=50 -f
|
||||||
|
|
||||||
|
|||||||
175
check_column_names.py
Normal file
175
check_column_names.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
カラム名検証スクリプト
|
||||||
|
PostgreSQLで問題となるカラム名を事前にチェック
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL予約語
|
||||||
|
RESERVED_KEYWORDS = {
|
||||||
|
'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert',
|
||||||
|
'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint',
|
||||||
|
'default', 'check', 'unique', 'primary', 'foreign', 'key', 'references'
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_column_names():
|
||||||
|
"""全rog_テーブルのカラム名をチェック"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# rog_テーブル一覧取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
tables = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
logger.info(f"チェック対象テーブル: {len(tables)}個")
|
||||||
|
|
||||||
|
problematic_columns = {}
|
||||||
|
|
||||||
|
for table_name in tables:
|
||||||
|
# テーブルのカラム一覧取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
problem_cols = []
|
||||||
|
|
||||||
|
for col in columns:
|
||||||
|
# 予約語チェック
|
||||||
|
if col.lower() in RESERVED_KEYWORDS:
|
||||||
|
problem_cols.append((col, '予約語'))
|
||||||
|
|
||||||
|
# キャメルケース/大文字チェック
|
||||||
|
elif any(c.isupper() for c in col) or col != col.lower():
|
||||||
|
problem_cols.append((col, 'キャメルケース/大文字'))
|
||||||
|
|
||||||
|
if problem_cols:
|
||||||
|
problematic_columns[table_name] = problem_cols
|
||||||
|
|
||||||
|
# 結果出力
|
||||||
|
if problematic_columns:
|
||||||
|
logger.warning("⚠️ 問題のあるカラム名が見つかりました:")
|
||||||
|
for table, cols in problematic_columns.items():
|
||||||
|
logger.warning(f" {table}:")
|
||||||
|
for col, reason in cols:
|
||||||
|
logger.warning(f" - {col} ({reason})")
|
||||||
|
else:
|
||||||
|
logger.info("✅ 全てのカラム名は問題ありません")
|
||||||
|
|
||||||
|
# クォートが必要なカラムのリスト生成
|
||||||
|
need_quotes = set()
|
||||||
|
for table, cols in problematic_columns.items():
|
||||||
|
for col, reason in cols:
|
||||||
|
need_quotes.add(col)
|
||||||
|
|
||||||
|
if need_quotes:
|
||||||
|
logger.info("📋 クォートが必要なカラム一覧:")
|
||||||
|
for col in sorted(need_quotes):
|
||||||
|
logger.info(f" '{col}' -> '\"{col}\"'")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return problematic_columns
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ カラム名チェックエラー: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def test_quoted_query():
|
||||||
|
"""クォート付きクエリのテスト"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 問題のあるテーブルでテストクエリ実行
|
||||||
|
test_tables = ['rog_entry', 'rog_newevent2']
|
||||||
|
|
||||||
|
for table_name in test_tables:
|
||||||
|
logger.info(f"=== {table_name} クエリテスト ===")
|
||||||
|
|
||||||
|
# カラム一覧取得
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# クォート付きカラム名生成
|
||||||
|
def quote_column_if_needed(column_name):
|
||||||
|
if column_name.lower() in RESERVED_KEYWORDS:
|
||||||
|
return f'"{column_name}"'
|
||||||
|
if any(c.isupper() for c in column_name) or column_name != column_name.lower():
|
||||||
|
return f'"{column_name}"'
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
quoted_columns = [quote_column_if_needed(col) for col in columns]
|
||||||
|
columns_str = ', '.join(quoted_columns)
|
||||||
|
|
||||||
|
# テストクエリ実行
|
||||||
|
try:
|
||||||
|
test_query = f"SELECT {columns_str} FROM {table_name} LIMIT 1"
|
||||||
|
logger.info(f"テストクエリ: {test_query[:100]}...")
|
||||||
|
cursor.execute(test_query)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
logger.info(f"✅ {table_name}: クエリ成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name}: クエリエラー: {e}")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ クエリテストエラー: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("PostgreSQL カラム名検証スクリプト")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
# カラム名チェック
|
||||||
|
problematic_columns = check_column_names()
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# クエリテスト
|
||||||
|
test_quoted_query()
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("検証完了")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
179
check_null_values.py
Normal file
179
check_null_values.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
NULL値チェック・デフォルト値テストスクリプト
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_null_values():
|
||||||
|
"""NULL値の問題を事前チェック"""
|
||||||
|
try:
|
||||||
|
old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
old_conn.autocommit = True
|
||||||
|
new_conn.autocommit = True
|
||||||
|
|
||||||
|
old_cursor = old_conn.cursor()
|
||||||
|
new_cursor = new_conn.cursor()
|
||||||
|
|
||||||
|
# 共通テーブル取得
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT table_name FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name LIKE 'rog_%'
|
||||||
|
""")
|
||||||
|
old_tables = [row[0] for row in old_cursor.fetchall()]
|
||||||
|
|
||||||
|
new_cursor.execute("""
|
||||||
|
SELECT table_name FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_name LIKE 'rog_%'
|
||||||
|
""")
|
||||||
|
new_tables = [row[0] for row in new_cursor.fetchall()]
|
||||||
|
|
||||||
|
common_tables = list(set(old_tables) & set(new_tables))
|
||||||
|
|
||||||
|
logger.info(f"チェック対象テーブル: {len(common_tables)}個")
|
||||||
|
|
||||||
|
null_issues = {}
|
||||||
|
|
||||||
|
for table_name in common_tables:
|
||||||
|
logger.info(f"=== {table_name} NULL値チェック ===")
|
||||||
|
|
||||||
|
# 新しいDBのNOT NULL制約確認
|
||||||
|
new_cursor.execute("""
|
||||||
|
SELECT column_name, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s AND table_schema = 'public'
|
||||||
|
AND is_nullable = 'NO'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
not_null_columns = new_cursor.fetchall()
|
||||||
|
|
||||||
|
if not not_null_columns:
|
||||||
|
logger.info(f" NOT NULL制約なし")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f" NOT NULL制約カラム: {[col[0] for col in not_null_columns]}")
|
||||||
|
|
||||||
|
# 古いDBのNULL値チェック
|
||||||
|
for col_name, is_nullable, default_val in not_null_columns:
|
||||||
|
try:
|
||||||
|
# PostgreSQL予約語とcamelCaseカラムのクォート処理
|
||||||
|
reserved_words = ['group', 'like', 'order', 'user', 'table', 'index', 'where', 'from', 'select']
|
||||||
|
quoted_col = f'"{col_name}"' if (col_name.lower() in reserved_words or any(c.isupper() for c in col_name)) else col_name
|
||||||
|
|
||||||
|
# カラム存在チェック
|
||||||
|
old_cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = %s AND column_name = %s AND table_schema = 'public'
|
||||||
|
""", (table_name, col_name))
|
||||||
|
|
||||||
|
if old_cursor.fetchone()[0] == 0:
|
||||||
|
logger.warning(f" ⚠️ {col_name}: 古いDBに存在しないカラム")
|
||||||
|
continue
|
||||||
|
|
||||||
|
old_cursor.execute(f"""
|
||||||
|
SELECT COUNT(*) FROM {table_name}
|
||||||
|
WHERE {quoted_col} IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
|
null_count = old_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if null_count > 0:
|
||||||
|
logger.warning(f" ⚠️ {col_name}: {null_count}件のNULL値あり (デフォルト: {default_val})")
|
||||||
|
|
||||||
|
if table_name not in null_issues:
|
||||||
|
null_issues[table_name] = []
|
||||||
|
null_issues[table_name].append((col_name, null_count, default_val))
|
||||||
|
else:
|
||||||
|
logger.info(f" ✅ {col_name}: NULL値なし")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" ❌ {col_name}: チェックエラー: {e}")
|
||||||
|
|
||||||
|
# サマリー
|
||||||
|
if null_issues:
|
||||||
|
logger.warning("=" * 60)
|
||||||
|
logger.warning("NULL値問題のあるテーブル:")
|
||||||
|
for table, issues in null_issues.items():
|
||||||
|
logger.warning(f" {table}:")
|
||||||
|
for col, count, default in issues:
|
||||||
|
logger.warning(f" - {col}: {count}件 (デフォルト: {default})")
|
||||||
|
else:
|
||||||
|
logger.info("✅ NULL値の問題はありません")
|
||||||
|
|
||||||
|
old_cursor.close()
|
||||||
|
new_cursor.close()
|
||||||
|
old_conn.close()
|
||||||
|
new_conn.close()
|
||||||
|
|
||||||
|
return null_issues
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ NULL値チェックエラー: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def suggest_default_values(null_issues):
|
||||||
|
"""デフォルト値の提案"""
|
||||||
|
if not null_issues:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("推奨デフォルト値設定:")
|
||||||
|
|
||||||
|
for table_name, issues in null_issues.items():
|
||||||
|
logger.info(f" '{table_name}': {{")
|
||||||
|
for col_name, count, default in issues:
|
||||||
|
# データ型に基づくデフォルト値推測
|
||||||
|
if 'trial' in col_name.lower() or 'is_' in col_name.lower():
|
||||||
|
suggested = 'False'
|
||||||
|
elif 'public' in col_name.lower():
|
||||||
|
suggested = 'True'
|
||||||
|
elif 'name' in col_name.lower() or 'description' in col_name.lower():
|
||||||
|
suggested = "''"
|
||||||
|
elif 'order' in col_name.lower() or 'sort' in col_name.lower():
|
||||||
|
suggested = '0'
|
||||||
|
else:
|
||||||
|
suggested = 'None # 要確認'
|
||||||
|
|
||||||
|
logger.info(f" '{col_name}': {suggested}, # {count}件のNULL")
|
||||||
|
logger.info(" },")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("NULL値チェック・デフォルト値提案スクリプト")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
null_issues = check_null_values()
|
||||||
|
suggest_default_values(null_issues)
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("チェック完了")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
59
external_db_connection_test.py
Normal file
59
external_db_connection_test.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
外部スクリプトからDBコンテナに接続するサンプル
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import DictCursor
|
||||||
|
|
||||||
|
# 環境変数から接続情報を取得
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': os.getenv('PG_HOST', 'localhost'),
|
||||||
|
'port': os.getenv('PG_PORT', '5432'),
|
||||||
|
'database': os.getenv('POSTGRES_DBNAME', 'rogdb'),
|
||||||
|
'user': os.getenv('POSTGRES_USER', 'admin'),
|
||||||
|
'password': os.getenv('POSTGRES_PASS', 'admin123456')
|
||||||
|
}
|
||||||
|
|
||||||
|
def connect_to_db():
|
||||||
|
"""データベースに接続"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(**DB_CONFIG)
|
||||||
|
print(f"✅ データベースに接続成功: {DB_CONFIG['host']}:{DB_CONFIG['port']}")
|
||||||
|
return conn
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
print(f"❌ データベース接続エラー: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_connection():
|
||||||
|
"""接続テスト"""
|
||||||
|
conn = connect_to_db()
|
||||||
|
if conn:
|
||||||
|
try:
|
||||||
|
with conn.cursor(cursor_factory=DictCursor) as cur:
|
||||||
|
cur.execute("SELECT version();")
|
||||||
|
version = cur.fetchone()
|
||||||
|
print(f"PostgreSQL バージョン: {version[0]}")
|
||||||
|
|
||||||
|
# テーブル一覧を取得
|
||||||
|
cur.execute("""
|
||||||
|
SELECT tablename FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
ORDER BY tablename;
|
||||||
|
""")
|
||||||
|
tables = cur.fetchall()
|
||||||
|
print(f"テーブル数: {len(tables)}")
|
||||||
|
for table in tables[:5]: # 最初の5個を表示
|
||||||
|
print(f" - {table[0]}")
|
||||||
|
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
print(f"❌ クエリ実行エラー: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== データベース接続テスト ===")
|
||||||
|
print(f"接続先: {DB_CONFIG['host']}:{DB_CONFIG['port']}")
|
||||||
|
print(f"データベース: {DB_CONFIG['database']}")
|
||||||
|
print(f"ユーザー: {DB_CONFIG['user']}")
|
||||||
|
test_connection()
|
||||||
93
migrate_old_rogdb_quickstart.sh
Normal file
93
migrate_old_rogdb_quickstart.sh
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Old RogDB → RogDB 移行クイックスタート
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Old RogDB → RogDB データ移行クイックスタート"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
# 実行前チェック
|
||||||
|
echo "🔍 実行前チェック..."
|
||||||
|
|
||||||
|
# Docker Composeサービス確認
|
||||||
|
if ! docker compose ps | grep -q "Up"; then
|
||||||
|
echo "❌ Docker Composeサービスが起動していません"
|
||||||
|
echo "次のコマンドでサービスを起動してください:"
|
||||||
|
echo "docker compose up -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Docker Composeサービス確認完了"
|
||||||
|
|
||||||
|
# データベース接続確認
|
||||||
|
echo "🔍 データベース接続確認..."
|
||||||
|
if ! docker compose exec app python -c "
|
||||||
|
import psycopg2
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(host='postgres-db', database='rogdb', user='admin', password='admin123456')
|
||||||
|
print('✅ rogdb接続成功')
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ rogdb接続エラー: {e}')
|
||||||
|
exit(1)
|
||||||
|
"; then
|
||||||
|
echo "❌ データベース接続に失敗しました"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# メニュー表示
|
||||||
|
echo ""
|
||||||
|
echo "📋 移行オプションを選択してください:"
|
||||||
|
echo "1) 完全移行(全rog_*テーブル)"
|
||||||
|
echo "2) 安全移行(ユーザー関連テーブル除外)"
|
||||||
|
echo "3) 統計情報のみ表示"
|
||||||
|
echo "4) テーブル一覧のみ表示"
|
||||||
|
echo "5) カスタム移行(除外テーブル指定)"
|
||||||
|
echo "0) キャンセル"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "選択 (0-5): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
echo "🚀 完全移行を開始します..."
|
||||||
|
docker compose exec app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo "🛡️ 安全移行を開始します(ユーザー関連テーブル除外)..."
|
||||||
|
docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo "📊 統計情報を表示します..."
|
||||||
|
make migrate-old-rogdb-stats
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo "📋 テーブル一覧を表示します..."
|
||||||
|
make migrate-old-rogdb-dryrun
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo "除外するテーブル名をカンマ区切りで入力してください(例: rog_customuser,rog_session):"
|
||||||
|
read -p "除外テーブル: " exclude_tables
|
||||||
|
echo "🔧 カスタム移行を開始します..."
|
||||||
|
docker compose exec -e EXCLUDE_TABLES="$exclude_tables" app python migrate_old_rogdb_to_rogdb.py
|
||||||
|
;;
|
||||||
|
0)
|
||||||
|
echo "移行をキャンセルしました"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ 無効な選択です"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "移行処理が完了しました"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "📋 移行後の確認方法:"
|
||||||
|
echo " - ログ確認: make app-logs"
|
||||||
|
echo " - DB接続: make db-shell"
|
||||||
|
echo " - 統計再表示: make migrate-old-rogdb-stats"
|
||||||
|
echo ""
|
||||||
|
echo "📖 詳細情報: MIGRATE_OLD_ROGDB_README.md"
|
||||||
493
migrate_old_rogdb_to_rogdb.py
Normal file
493
migrate_old_rogdb_to_rogdb.py
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Old RogDB データ移行スクリプト (エラー修正版)
|
||||||
|
old_rogdb の rog_* テーブルから rogdb の rog_* テーブルへデータを更新・挿入
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL予約語をクォートで囲む必要があるカラム
|
||||||
|
RESERVED_KEYWORDS = {
|
||||||
|
'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert',
|
||||||
|
'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint'
|
||||||
|
}
|
||||||
|
|
||||||
|
class RogTableMigrator:
|
||||||
|
"""Rogテーブル移行クラス (エラー修正版)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.old_conn = None
|
||||||
|
self.new_conn = None
|
||||||
|
self.old_cursor = None
|
||||||
|
self.new_cursor = None
|
||||||
|
self.migration_stats = {}
|
||||||
|
|
||||||
|
def quote_column_if_needed(self, column_name):
|
||||||
|
"""予約語やキャメルケースの場合はダブルクォートで囲む"""
|
||||||
|
# 予約語チェック
|
||||||
|
if column_name.lower() in RESERVED_KEYWORDS:
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
# キャメルケースや大文字が含まれる場合もクォート
|
||||||
|
if any(c.isupper() for c in column_name) or column_name != column_name.lower():
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
def handle_null_values(self, table_name, column_name, value):
|
||||||
|
"""NULL値の処理とデフォルト値設定"""
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# テーブル・カラム別のデフォルト値設定
|
||||||
|
null_defaults = {
|
||||||
|
'rog_team': {
|
||||||
|
'trial': False, # Boolean型のデフォルト
|
||||||
|
'is_active': True,
|
||||||
|
'is_trial': False,
|
||||||
|
},
|
||||||
|
'rog_entry': {
|
||||||
|
'trial': False,
|
||||||
|
'is_active': True,
|
||||||
|
'is_trial': False,
|
||||||
|
'hasGoaled': False,
|
||||||
|
'hasParticipated': False,
|
||||||
|
},
|
||||||
|
'rog_member': {
|
||||||
|
'is_active': True,
|
||||||
|
'is_captain': False,
|
||||||
|
},
|
||||||
|
'rog_newevent2': {
|
||||||
|
'public': True,
|
||||||
|
'class_general': True,
|
||||||
|
'class_family': True,
|
||||||
|
'class_solo_male': True,
|
||||||
|
'class_solo_female': True,
|
||||||
|
'hour_3': True,
|
||||||
|
'hour_5': True,
|
||||||
|
'self_rogaining': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# テーブル固有のデフォルト値を取得
|
||||||
|
if table_name in null_defaults and column_name in null_defaults[table_name]:
|
||||||
|
default_value = null_defaults[table_name][column_name]
|
||||||
|
logger.debug(f"NULL値をデフォルト値に変換: {table_name}.{column_name} = {default_value}")
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
# 一般的なデフォルト値
|
||||||
|
common_defaults = {
|
||||||
|
# Boolean型
|
||||||
|
'is_active': True,
|
||||||
|
'is_trial': False,
|
||||||
|
'public': True,
|
||||||
|
'trial': False,
|
||||||
|
# 文字列型
|
||||||
|
'description': '',
|
||||||
|
'note': '',
|
||||||
|
'comment': '',
|
||||||
|
# 数値型
|
||||||
|
'sort_order': 0,
|
||||||
|
'order': 0,
|
||||||
|
# PostgreSQL予約語
|
||||||
|
'group': '',
|
||||||
|
'like': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if column_name in common_defaults:
|
||||||
|
default_value = common_defaults[column_name]
|
||||||
|
logger.debug(f"NULL値を共通デフォルト値に変換: {column_name} = {default_value}")
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
# デフォルト値が見つからない場合はNULLを返す
|
||||||
|
logger.warning(f"デフォルト値が設定されていません: {table_name}.{column_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def connect_databases(self):
|
||||||
|
"""データベース接続"""
|
||||||
|
try:
|
||||||
|
logger.info("データベースに接続中...")
|
||||||
|
self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
self.new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
# autocommitを有効にしてトランザクション問題を回避
|
||||||
|
self.old_conn.autocommit = True
|
||||||
|
self.new_conn.autocommit = False # 新しい方は手動コミット
|
||||||
|
|
||||||
|
self.old_cursor = self.old_conn.cursor()
|
||||||
|
self.new_cursor = self.new_conn.cursor()
|
||||||
|
|
||||||
|
logger.info("✅ データベース接続成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
"""データベース接続クローズ"""
|
||||||
|
try:
|
||||||
|
if self.old_cursor:
|
||||||
|
self.old_cursor.close()
|
||||||
|
if self.new_cursor:
|
||||||
|
self.new_cursor.close()
|
||||||
|
if self.old_conn:
|
||||||
|
self.old_conn.close()
|
||||||
|
if self.new_conn:
|
||||||
|
self.new_conn.close()
|
||||||
|
logger.info("データベース接続をクローズしました")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"接続クローズ時の警告: {e}")
|
||||||
|
|
||||||
|
def get_rog_tables(self):
|
||||||
|
"""rog_で始まるテーブル一覧を取得"""
|
||||||
|
try:
|
||||||
|
# old_rogdbのrog_テーブル一覧
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
old_tables = [row[0] for row in self.old_cursor.fetchall()]
|
||||||
|
|
||||||
|
# rogdbのrog_テーブル一覧
|
||||||
|
self.new_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
new_tables = [row[0] for row in self.new_cursor.fetchall()]
|
||||||
|
|
||||||
|
# 共通テーブル
|
||||||
|
common_tables = list(set(old_tables) & set(new_tables))
|
||||||
|
|
||||||
|
logger.info(f"old_rogdb rog_テーブル: {len(old_tables)}個")
|
||||||
|
logger.info(f"rogdb rog_テーブル: {len(new_tables)}個")
|
||||||
|
logger.info(f"共通 rog_テーブル: {len(common_tables)}個")
|
||||||
|
|
||||||
|
return old_tables, new_tables, common_tables
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル一覧取得エラー: {e}")
|
||||||
|
return [], [], []
|
||||||
|
|
||||||
|
def get_table_structure(self, table_name, cursor):
|
||||||
|
"""テーブル構造を取得 (エラーハンドリング強化)"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
return {
|
||||||
|
'columns': [col[0] for col in columns],
|
||||||
|
'details': columns
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル構造取得エラー ({table_name}): {e}")
|
||||||
|
# トランザクションエラーを回避
|
||||||
|
try:
|
||||||
|
cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'columns': [], 'details': []}
|
||||||
|
|
||||||
|
def get_primary_key(self, table_name, cursor):
|
||||||
|
"""主キーカラムを取得"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT a.attname
|
||||||
|
FROM pg_index i
|
||||||
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||||
|
WHERE i.indrelid = %s::regclass AND i.indisprimary
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
pk_columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
return pk_columns if pk_columns else ['id']
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"主キー取得エラー ({table_name}): {e}")
|
||||||
|
return ['id'] # デフォルトでidを使用
|
||||||
|
|
||||||
|
def migrate_table_data(self, table_name):
|
||||||
|
"""個別テーブルのデータ移行 (エラー修正版)"""
|
||||||
|
logger.info(f"\n=== {table_name} データ移行開始 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# テーブル構造比較
|
||||||
|
old_structure = self.get_table_structure(table_name, self.old_cursor)
|
||||||
|
new_structure = self.get_table_structure(table_name, self.new_cursor)
|
||||||
|
|
||||||
|
old_columns = set(old_structure['columns'])
|
||||||
|
new_columns = set(new_structure['columns'])
|
||||||
|
common_columns = old_columns & new_columns
|
||||||
|
|
||||||
|
if not common_columns:
|
||||||
|
logger.warning(f"⚠️ {table_name}: 共通カラムがありません")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"共通カラム ({len(common_columns)}個): {sorted(common_columns)}")
|
||||||
|
|
||||||
|
# 主キー取得
|
||||||
|
pk_columns = self.get_primary_key(table_name, self.new_cursor)
|
||||||
|
logger.info(f"主キー: {pk_columns}")
|
||||||
|
|
||||||
|
# カラム名を予約語対応でクォート
|
||||||
|
quoted_columns = [self.quote_column_if_needed(col) for col in common_columns]
|
||||||
|
columns_str = ', '.join(quoted_columns)
|
||||||
|
|
||||||
|
# old_rogdbからデータ取得
|
||||||
|
try:
|
||||||
|
self.old_cursor.execute(f"SELECT {columns_str} FROM {table_name}")
|
||||||
|
old_records = self.old_cursor.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} データ取得エラー: {e}")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
if not old_records:
|
||||||
|
logger.info(f"✅ {table_name}: 移行対象データなし")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"移行対象レコード数: {len(old_records)}件")
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
# レコード移行処理
|
||||||
|
for i, record in enumerate(old_records):
|
||||||
|
try:
|
||||||
|
# レコードデータを辞書形式に変換(NULL値処理込み)
|
||||||
|
record_dict = {}
|
||||||
|
for j, col in enumerate(common_columns):
|
||||||
|
original_value = record[j]
|
||||||
|
processed_value = self.handle_null_values(table_name, col, original_value)
|
||||||
|
record_dict[col] = processed_value
|
||||||
|
|
||||||
|
# 主キー値を取得
|
||||||
|
pk_values = []
|
||||||
|
pk_conditions = []
|
||||||
|
for pk_col in pk_columns:
|
||||||
|
if pk_col in record_dict:
|
||||||
|
pk_values.append(record_dict[pk_col])
|
||||||
|
quoted_pk_col = self.quote_column_if_needed(pk_col)
|
||||||
|
pk_conditions.append(f"{quoted_pk_col} = %s")
|
||||||
|
|
||||||
|
if not pk_values:
|
||||||
|
error_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 既存レコード確認
|
||||||
|
check_query = f"SELECT COUNT(*) FROM {table_name} WHERE {' AND '.join(pk_conditions)}"
|
||||||
|
self.new_cursor.execute(check_query, pk_values)
|
||||||
|
exists = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# UPDATE処理
|
||||||
|
set_clauses = []
|
||||||
|
update_values = []
|
||||||
|
|
||||||
|
for col in common_columns:
|
||||||
|
if col not in pk_columns: # 主キー以外を更新
|
||||||
|
quoted_col = self.quote_column_if_needed(col)
|
||||||
|
set_clauses.append(f"{quoted_col} = %s")
|
||||||
|
update_values.append(record_dict[col])
|
||||||
|
|
||||||
|
if set_clauses:
|
||||||
|
update_query = f"""
|
||||||
|
UPDATE {table_name}
|
||||||
|
SET {', '.join(set_clauses)}
|
||||||
|
WHERE {' AND '.join(pk_conditions)}
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(update_query, update_values + pk_values)
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# INSERT処理
|
||||||
|
insert_columns = list(common_columns)
|
||||||
|
insert_values = [record_dict[col] for col in insert_columns]
|
||||||
|
quoted_insert_columns = [self.quote_column_if_needed(col) for col in insert_columns]
|
||||||
|
placeholders = ', '.join(['%s'] * len(insert_columns))
|
||||||
|
|
||||||
|
insert_query = f"""
|
||||||
|
INSERT INTO {table_name} ({', '.join(quoted_insert_columns)})
|
||||||
|
VALUES ({placeholders})
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(insert_query, insert_values)
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
# 定期的なコミット
|
||||||
|
if (i + 1) % 100 == 0:
|
||||||
|
self.new_conn.commit()
|
||||||
|
logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
logger.error(f" レコード処理エラー (行{i+1}): {e}")
|
||||||
|
# トランザクションロールバック
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if error_count > 10: # エラー上限
|
||||||
|
logger.error(f"❌ {table_name}: エラー数が上限を超えました")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 最終コミット
|
||||||
|
try:
|
||||||
|
self.new_conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"コミットエラー: {e}")
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
stats = {
|
||||||
|
'inserted': inserted_count,
|
||||||
|
'updated': updated_count,
|
||||||
|
'errors': error_count,
|
||||||
|
'total': len(old_records)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"✅ {table_name} 移行完了:")
|
||||||
|
logger.info(f" 挿入: {inserted_count}件")
|
||||||
|
logger.info(f" 更新: {updated_count}件")
|
||||||
|
logger.info(f" エラー: {error_count}件")
|
||||||
|
|
||||||
|
self.migration_stats[table_name] = stats
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} 移行エラー: {e}")
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
def run_migration(self, exclude_tables=None):
|
||||||
|
"""全体移行実行"""
|
||||||
|
if exclude_tables is None:
|
||||||
|
exclude_tables = []
|
||||||
|
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("Old RogDB → RogDB データ移行開始")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続
|
||||||
|
if not self.connect_databases():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# テーブル一覧取得
|
||||||
|
old_tables, new_tables, common_tables = self.get_rog_tables()
|
||||||
|
|
||||||
|
if not common_tables:
|
||||||
|
logger.error("❌ 移行対象の共通テーブルがありません")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 除外テーブルを除去
|
||||||
|
migration_tables = [t for t in common_tables if t not in exclude_tables]
|
||||||
|
|
||||||
|
if exclude_tables:
|
||||||
|
logger.info(f"除外テーブル: {exclude_tables}")
|
||||||
|
|
||||||
|
logger.info(f"移行対象テーブル ({len(migration_tables)}個): {migration_tables}")
|
||||||
|
|
||||||
|
# テーブル別移行実行
|
||||||
|
total_inserted = 0
|
||||||
|
total_updated = 0
|
||||||
|
total_errors = 0
|
||||||
|
|
||||||
|
for table_name in migration_tables:
|
||||||
|
stats = self.migrate_table_data(table_name)
|
||||||
|
total_inserted += stats['inserted']
|
||||||
|
total_updated += stats['updated']
|
||||||
|
total_errors += stats['errors']
|
||||||
|
|
||||||
|
# 最終結果
|
||||||
|
logger.info("\n" + "=" * 80)
|
||||||
|
logger.info("移行完了サマリー")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info(f"処理対象テーブル: {len(migration_tables)}個")
|
||||||
|
logger.info(f"総挿入件数: {total_inserted}件")
|
||||||
|
logger.info(f"総更新件数: {total_updated}件")
|
||||||
|
logger.info(f"総エラー件数: {total_errors}件")
|
||||||
|
|
||||||
|
# テーブル別詳細
|
||||||
|
logger.info("\n--- テーブル別詳細 ---")
|
||||||
|
for table_name, stats in self.migration_stats.items():
|
||||||
|
logger.info(f"{table_name}: 挿入{stats['inserted']}, 更新{stats['updated']}, エラー{stats['errors']}")
|
||||||
|
|
||||||
|
if total_errors == 0:
|
||||||
|
logger.info("✅ 全ての移行が正常に完了しました!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ {total_errors}件のエラーがありました")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 移行処理エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.close_connections()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
logger.info("Old RogDB → RogDB データ移行スクリプト")
|
||||||
|
|
||||||
|
# 除外テーブルの設定(必要に応じて)
|
||||||
|
exclude_tables = [
|
||||||
|
# 'rog_customuser', # ユーザーデータは慎重に扱う
|
||||||
|
# 'rog_session', # セッションデータは移行不要
|
||||||
|
# 'django_migrations', # Django管理テーブル
|
||||||
|
]
|
||||||
|
|
||||||
|
# 環境変数による除外テーブル指定
|
||||||
|
env_exclude = os.getenv('EXCLUDE_TABLES', '')
|
||||||
|
if env_exclude:
|
||||||
|
exclude_tables.extend(env_exclude.split(','))
|
||||||
|
|
||||||
|
# 移行実行
|
||||||
|
migrator = RogTableMigrator()
|
||||||
|
success = migrator.run_migration(exclude_tables=exclude_tables)
|
||||||
|
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
414
migrate_old_rogdb_to_rogdb_fixed.py
Normal file
414
migrate_old_rogdb_to_rogdb_fixed.py
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Old RogDB データ移行スクリプト (エラー修正版)
|
||||||
|
old_rogdb の rog_* テーブルから rogdb の rog_* テーブルへデータを更新・挿入
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ログ設定
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# データベース設定
|
||||||
|
OLD_ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'),
|
||||||
|
'user': os.getenv('OLD_ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('OLD_ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
ROGDB_CONFIG = {
|
||||||
|
'host': os.getenv('ROGDB_HOST', 'postgres-db'),
|
||||||
|
'database': os.getenv('ROGDB_NAME', 'rogdb'),
|
||||||
|
'user': os.getenv('ROGDB_USER', 'admin'),
|
||||||
|
'password': os.getenv('ROGDB_PASSWORD', 'admin123456'),
|
||||||
|
'port': int(os.getenv('ROGDB_PORT', 5432))
|
||||||
|
}
|
||||||
|
|
||||||
|
# PostgreSQL予約語をクォートで囲む必要があるカラム
|
||||||
|
RESERVED_KEYWORDS = {
|
||||||
|
'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert',
|
||||||
|
'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint'
|
||||||
|
}
|
||||||
|
|
||||||
|
class RogTableMigrator:
|
||||||
|
"""Rogテーブル移行クラス (エラー修正版)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.old_conn = None
|
||||||
|
self.new_conn = None
|
||||||
|
self.old_cursor = None
|
||||||
|
self.new_cursor = None
|
||||||
|
self.migration_stats = {}
|
||||||
|
|
||||||
|
def quote_column_if_needed(self, column_name):
|
||||||
|
"""予約語やキャメルケースの場合はダブルクォートで囲む"""
|
||||||
|
# 予約語チェック
|
||||||
|
if column_name.lower() in RESERVED_KEYWORDS:
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
# キャメルケースや大文字が含まれる場合もクォート
|
||||||
|
if any(c.isupper() for c in column_name) or column_name != column_name.lower():
|
||||||
|
return f'"{column_name}"'
|
||||||
|
|
||||||
|
return column_name
|
||||||
|
|
||||||
|
def connect_databases(self):
|
||||||
|
"""データベース接続"""
|
||||||
|
try:
|
||||||
|
logger.info("データベースに接続中...")
|
||||||
|
self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG)
|
||||||
|
self.new_conn = psycopg2.connect(**ROGDB_CONFIG)
|
||||||
|
|
||||||
|
# autocommitを有効にしてトランザクション問題を回避
|
||||||
|
self.old_conn.autocommit = True
|
||||||
|
self.new_conn.autocommit = False # 新しい方は手動コミット
|
||||||
|
|
||||||
|
self.old_cursor = self.old_conn.cursor()
|
||||||
|
self.new_cursor = self.new_conn.cursor()
|
||||||
|
|
||||||
|
logger.info("✅ データベース接続成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ データベース接続エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_connections(self):
|
||||||
|
"""データベース接続クローズ"""
|
||||||
|
try:
|
||||||
|
if self.old_cursor:
|
||||||
|
self.old_cursor.close()
|
||||||
|
if self.new_cursor:
|
||||||
|
self.new_cursor.close()
|
||||||
|
if self.old_conn:
|
||||||
|
self.old_conn.close()
|
||||||
|
if self.new_conn:
|
||||||
|
self.new_conn.close()
|
||||||
|
logger.info("データベース接続をクローズしました")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"接続クローズ時の警告: {e}")
|
||||||
|
|
||||||
|
def get_rog_tables(self):
|
||||||
|
"""rog_で始まるテーブル一覧を取得"""
|
||||||
|
try:
|
||||||
|
# old_rogdbのrog_テーブル一覧
|
||||||
|
self.old_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
old_tables = [row[0] for row in self.old_cursor.fetchall()]
|
||||||
|
|
||||||
|
# rogdbのrog_テーブル一覧
|
||||||
|
self.new_cursor.execute("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name LIKE 'rog_%'
|
||||||
|
ORDER BY table_name
|
||||||
|
""")
|
||||||
|
new_tables = [row[0] for row in self.new_cursor.fetchall()]
|
||||||
|
|
||||||
|
# 共通テーブル
|
||||||
|
common_tables = list(set(old_tables) & set(new_tables))
|
||||||
|
|
||||||
|
logger.info(f"old_rogdb rog_テーブル: {len(old_tables)}個")
|
||||||
|
logger.info(f"rogdb rog_テーブル: {len(new_tables)}個")
|
||||||
|
logger.info(f"共通 rog_テーブル: {len(common_tables)}個")
|
||||||
|
|
||||||
|
return old_tables, new_tables, common_tables
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル一覧取得エラー: {e}")
|
||||||
|
return [], [], []
|
||||||
|
|
||||||
|
def get_table_structure(self, table_name, cursor):
|
||||||
|
"""テーブル構造を取得 (エラーハンドリング強化)"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = %s
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
return {
|
||||||
|
'columns': [col[0] for col in columns],
|
||||||
|
'details': columns
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"テーブル構造取得エラー ({table_name}): {e}")
|
||||||
|
# トランザクションエラーを回避
|
||||||
|
try:
|
||||||
|
cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'columns': [], 'details': []}
|
||||||
|
|
||||||
|
def get_primary_key(self, table_name, cursor):
|
||||||
|
"""主キーカラムを取得"""
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT a.attname
|
||||||
|
FROM pg_index i
|
||||||
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||||
|
WHERE i.indrelid = %s::regclass AND i.indisprimary
|
||||||
|
""", (table_name,))
|
||||||
|
|
||||||
|
pk_columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
return pk_columns if pk_columns else ['id']
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"主キー取得エラー ({table_name}): {e}")
|
||||||
|
return ['id'] # デフォルトでidを使用
|
||||||
|
|
||||||
|
def migrate_table_data(self, table_name):
|
||||||
|
"""個別テーブルのデータ移行 (エラー修正版)"""
|
||||||
|
logger.info(f"\n=== {table_name} データ移行開始 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# テーブル構造比較
|
||||||
|
old_structure = self.get_table_structure(table_name, self.old_cursor)
|
||||||
|
new_structure = self.get_table_structure(table_name, self.new_cursor)
|
||||||
|
|
||||||
|
old_columns = set(old_structure['columns'])
|
||||||
|
new_columns = set(new_structure['columns'])
|
||||||
|
common_columns = old_columns & new_columns
|
||||||
|
|
||||||
|
if not common_columns:
|
||||||
|
logger.warning(f"⚠️ {table_name}: 共通カラムがありません")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"共通カラム ({len(common_columns)}個): {sorted(common_columns)}")
|
||||||
|
|
||||||
|
# 主キー取得
|
||||||
|
pk_columns = self.get_primary_key(table_name, self.new_cursor)
|
||||||
|
logger.info(f"主キー: {pk_columns}")
|
||||||
|
|
||||||
|
# カラム名を予約語対応でクォート
|
||||||
|
quoted_columns = [self.quote_column_if_needed(col) for col in common_columns]
|
||||||
|
columns_str = ', '.join(quoted_columns)
|
||||||
|
|
||||||
|
# old_rogdbからデータ取得
|
||||||
|
try:
|
||||||
|
self.old_cursor.execute(f"SELECT {columns_str} FROM {table_name}")
|
||||||
|
old_records = self.old_cursor.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} データ取得エラー: {e}")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
if not old_records:
|
||||||
|
logger.info(f"✅ {table_name}: 移行対象データなし")
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 0}
|
||||||
|
|
||||||
|
logger.info(f"移行対象レコード数: {len(old_records)}件")
|
||||||
|
|
||||||
|
# 統計情報
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
# レコード移行処理
|
||||||
|
for i, record in enumerate(old_records):
|
||||||
|
try:
|
||||||
|
# レコードデータを辞書形式に変換
|
||||||
|
record_dict = dict(zip(common_columns, record))
|
||||||
|
|
||||||
|
# 主キー値を取得
|
||||||
|
pk_values = []
|
||||||
|
pk_conditions = []
|
||||||
|
for pk_col in pk_columns:
|
||||||
|
if pk_col in record_dict:
|
||||||
|
pk_values.append(record_dict[pk_col])
|
||||||
|
quoted_pk_col = self.quote_column_if_needed(pk_col)
|
||||||
|
pk_conditions.append(f"{quoted_pk_col} = %s")
|
||||||
|
|
||||||
|
if not pk_values:
|
||||||
|
error_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 既存レコード確認
|
||||||
|
check_query = f"SELECT COUNT(*) FROM {table_name} WHERE {' AND '.join(pk_conditions)}"
|
||||||
|
self.new_cursor.execute(check_query, pk_values)
|
||||||
|
exists = self.new_cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
# UPDATE処理
|
||||||
|
set_clauses = []
|
||||||
|
update_values = []
|
||||||
|
|
||||||
|
for col in common_columns:
|
||||||
|
if col not in pk_columns: # 主キー以外を更新
|
||||||
|
quoted_col = self.quote_column_if_needed(col)
|
||||||
|
set_clauses.append(f"{quoted_col} = %s")
|
||||||
|
update_values.append(record_dict[col])
|
||||||
|
|
||||||
|
if set_clauses:
|
||||||
|
update_query = f"""
|
||||||
|
UPDATE {table_name}
|
||||||
|
SET {', '.join(set_clauses)}
|
||||||
|
WHERE {' AND '.join(pk_conditions)}
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(update_query, update_values + pk_values)
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
# INSERT処理
|
||||||
|
insert_columns = list(common_columns)
|
||||||
|
insert_values = [record_dict[col] for col in insert_columns]
|
||||||
|
quoted_insert_columns = [self.quote_column_if_needed(col) for col in insert_columns]
|
||||||
|
placeholders = ', '.join(['%s'] * len(insert_columns))
|
||||||
|
|
||||||
|
insert_query = f"""
|
||||||
|
INSERT INTO {table_name} ({', '.join(quoted_insert_columns)})
|
||||||
|
VALUES ({placeholders})
|
||||||
|
"""
|
||||||
|
self.new_cursor.execute(insert_query, insert_values)
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
# 定期的なコミット
|
||||||
|
if (i + 1) % 100 == 0:
|
||||||
|
self.new_conn.commit()
|
||||||
|
logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
logger.error(f" レコード処理エラー (行{i+1}): {e}")
|
||||||
|
# トランザクションロールバック
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if error_count > 10: # エラー上限
|
||||||
|
logger.error(f"❌ {table_name}: エラー数が上限を超えました")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 最終コミット
|
||||||
|
try:
|
||||||
|
self.new_conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"コミットエラー: {e}")
|
||||||
|
|
||||||
|
# 結果サマリー
|
||||||
|
stats = {
|
||||||
|
'inserted': inserted_count,
|
||||||
|
'updated': updated_count,
|
||||||
|
'errors': error_count,
|
||||||
|
'total': len(old_records)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"✅ {table_name} 移行完了:")
|
||||||
|
logger.info(f" 挿入: {inserted_count}件")
|
||||||
|
logger.info(f" 更新: {updated_count}件")
|
||||||
|
logger.info(f" エラー: {error_count}件")
|
||||||
|
|
||||||
|
self.migration_stats[table_name] = stats
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ {table_name} 移行エラー: {e}")
|
||||||
|
try:
|
||||||
|
self.new_conn.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return {'inserted': 0, 'updated': 0, 'errors': 1}
|
||||||
|
|
||||||
|
def run_migration(self, exclude_tables=None):
|
||||||
|
"""全体移行実行"""
|
||||||
|
if exclude_tables is None:
|
||||||
|
exclude_tables = []
|
||||||
|
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("Old RogDB → RogDB データ移行開始")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# データベース接続
|
||||||
|
if not self.connect_databases():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# テーブル一覧取得
|
||||||
|
old_tables, new_tables, common_tables = self.get_rog_tables()
|
||||||
|
|
||||||
|
if not common_tables:
|
||||||
|
logger.error("❌ 移行対象テーブルがありません")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 除外テーブルをフィルタリング
|
||||||
|
migration_tables = [t for t in common_tables if t not in exclude_tables]
|
||||||
|
logger.info(f"移行対象テーブル ({len(migration_tables)}個): {migration_tables}")
|
||||||
|
|
||||||
|
# 各テーブルを移行
|
||||||
|
total_inserted = 0
|
||||||
|
total_updated = 0
|
||||||
|
total_errors = 0
|
||||||
|
|
||||||
|
for table_name in migration_tables:
|
||||||
|
stats = self.migrate_table_data(table_name)
|
||||||
|
total_inserted += stats['inserted']
|
||||||
|
total_updated += stats['updated']
|
||||||
|
total_errors += stats['errors']
|
||||||
|
|
||||||
|
# 全体サマリー出力
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("移行完了サマリー")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info(f"処理対象テーブル: {len(migration_tables)}個")
|
||||||
|
logger.info(f"総挿入件数: {total_inserted}件")
|
||||||
|
logger.info(f"総更新件数: {total_updated}件")
|
||||||
|
logger.info(f"総エラー件数: {total_errors}件")
|
||||||
|
|
||||||
|
logger.info("\n--- テーブル別詳細 ---")
|
||||||
|
for table_name, stats in self.migration_stats.items():
|
||||||
|
logger.info(f"{table_name}: 挿入{stats['inserted']}, 更新{stats['updated']}, エラー{stats['errors']}")
|
||||||
|
|
||||||
|
if total_errors > 0:
|
||||||
|
logger.warning(f"⚠️ {total_errors}件のエラーがありました")
|
||||||
|
else:
|
||||||
|
logger.info("✅ 全ての移行が正常に完了しました!")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 移行処理エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.close_connections()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
logger.info("Old RogDB → RogDB データ移行スクリプト")
|
||||||
|
|
||||||
|
# 除外テーブル設定
|
||||||
|
exclude_tables = []
|
||||||
|
if os.getenv('EXCLUDE_TABLES'):
|
||||||
|
exclude_tables = [t.strip() for t in os.getenv('EXCLUDE_TABLES').split(',')]
|
||||||
|
logger.info(f"除外テーブル: {exclude_tables}")
|
||||||
|
|
||||||
|
# 移行実行
|
||||||
|
migrator = RogTableMigrator()
|
||||||
|
success = migrator.run_migration(exclude_tables=exclude_tables)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info("移行処理が完了しました")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
logger.error("移行処理が失敗しました")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -107,19 +107,19 @@ def check_database_connectivity():
|
|||||||
# rogdb DB接続確認
|
# rogdb DB接続確認
|
||||||
target_conn = psycopg2.connect(**ROGDB_DB)
|
target_conn = psycopg2.connect(**ROGDB_DB)
|
||||||
target_cursor = target_conn.cursor()
|
target_cursor = target_conn.cursor()
|
||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
target_cursor.execute("SELECT COUNT(*) FROM gps_checkins")
|
||||||
target_count = target_cursor.fetchone()[0]
|
target_count = target_cursor.fetchone()[0]
|
||||||
print(f"✅ rogdb DB接続成功: rog_gpscheckin {target_count}件")
|
print(f"✅ rogdb DB接続成功: gps_checkins {target_count}件")
|
||||||
|
|
||||||
# 移行先テーブル構造確認
|
# 移行先テーブル構造確認
|
||||||
target_cursor.execute("""
|
target_cursor.execute("""
|
||||||
SELECT column_name, data_type
|
SELECT column_name, data_type
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_name = 'rog_gpscheckin'
|
WHERE table_name = 'gps_checkins'
|
||||||
ORDER BY ordinal_position
|
ORDER BY ordinal_position
|
||||||
""")
|
""")
|
||||||
target_columns = target_cursor.fetchall()
|
target_columns = target_cursor.fetchall()
|
||||||
print("📋 rog_gpscheckinテーブル構造:")
|
print("📋 gps_checkinsテーブル構造:")
|
||||||
for col_name, col_type in target_columns:
|
for col_name, col_type in target_columns:
|
||||||
print(f" {col_name}: {col_type}")
|
print(f" {col_name}: {col_type}")
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ def backup_existing_data(target_cursor):
|
|||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
|
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
|
||||||
member_count = target_cursor.fetchone()[0]
|
member_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
target_cursor.execute("SELECT COUNT(*) FROM gps_checkins")
|
||||||
checkin_count = target_cursor.fetchone()[0]
|
checkin_count = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
# Location2025データ数も確認
|
# Location2025データ数も確認
|
||||||
@ -228,7 +228,7 @@ def backup_existing_data(target_cursor):
|
|||||||
print(f" rog_entry: {entry_count} 件 (保護対象)")
|
print(f" rog_entry: {entry_count} 件 (保護対象)")
|
||||||
print(f" rog_team: {team_count} 件 (保護対象)")
|
print(f" rog_team: {team_count} 件 (保護対象)")
|
||||||
print(f" rog_member: {member_count} 件 (保護対象)")
|
print(f" rog_member: {member_count} 件 (保護対象)")
|
||||||
print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)")
|
print(f" gps_checkins: {checkin_count} 件 (移行対象)")
|
||||||
|
|
||||||
if entry_count > 0 or team_count > 0 or member_count > 0:
|
if entry_count > 0 or team_count > 0 or member_count > 0:
|
||||||
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
|
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
|
||||||
@ -265,7 +265,7 @@ def migrate_gps_data(source_cursor, target_cursor):
|
|||||||
target_cursor.execute("""
|
target_cursor.execute("""
|
||||||
SELECT column_name
|
SELECT column_name
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_name = 'rog_gpscheckin'
|
WHERE table_name = 'gps_checkins'
|
||||||
ORDER BY ordinal_position
|
ORDER BY ordinal_position
|
||||||
""")
|
""")
|
||||||
target_columns = [row[0] for row in target_cursor.fetchall()]
|
target_columns = [row[0] for row in target_cursor.fetchall()]
|
||||||
@ -370,7 +370,7 @@ def migrate_gps_data(source_cursor, target_cursor):
|
|||||||
columns_str = ', '.join(final_columns)
|
columns_str = ', '.join(final_columns)
|
||||||
|
|
||||||
target_cursor.execute(f"""
|
target_cursor.execute(f"""
|
||||||
INSERT INTO rog_gpscheckin ({columns_str})
|
INSERT INTO gps_checkins ({columns_str})
|
||||||
VALUES ({placeholders})
|
VALUES ({placeholders})
|
||||||
""", final_values)
|
""", final_values)
|
||||||
|
|
||||||
@ -406,7 +406,7 @@ def main():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("GPS記録データ移行スクリプト (既存データ保護版)")
|
print("GPS記録データ移行スクリプト (既存データ保護版)")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("移行対象: gifuroge.gps_information → rogdb.rog_gpscheckin")
|
print("移行対象: gifuroge.gps_information → rogdb.gps_checkins")
|
||||||
print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025")
|
print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
|
|||||||
@ -354,21 +354,21 @@ def main():
|
|||||||
print("=== Location2025対応版移行プログラム開始 ===")
|
print("=== Location2025対応版移行プログラム開始 ===")
|
||||||
print("注意: 既存のentry、team、member、location2025データは削除されません")
|
print("注意: 既存のentry、team、member、location2025データは削除されません")
|
||||||
|
|
||||||
# データベース接続設定
|
# データベース接続設定(環境変数から取得、デフォルト値あり)
|
||||||
source_config = {
|
source_config = {
|
||||||
'host': 'localhost',
|
'host': os.getenv('SOURCE_DB_HOST', 'postgres-db'),
|
||||||
'port': '5433',
|
'port': os.getenv('SOURCE_DB_PORT', '5432'),
|
||||||
'database': 'gifuroge',
|
'database': os.getenv('SOURCE_DB_NAME', 'gifuroge'),
|
||||||
'user': 'postgres',
|
'user': os.getenv('SOURCE_DB_USER', 'admin'),
|
||||||
'password': 'postgres'
|
'password': os.getenv('SOURCE_DB_PASSWORD', 'admin123456')
|
||||||
}
|
}
|
||||||
|
|
||||||
target_config = {
|
target_config = {
|
||||||
'host': 'localhost',
|
'host': os.getenv('TARGET_DB_HOST', 'postgres-db'),
|
||||||
'port': '5432',
|
'port': os.getenv('TARGET_DB_PORT', '5432'),
|
||||||
'database': 'rogdb',
|
'database': os.getenv('TARGET_DB_NAME', 'rogdb'),
|
||||||
'user': 'postgres',
|
'user': os.getenv('TARGET_DB_USER', 'admin'),
|
||||||
'password': 'postgres'
|
'password': os.getenv('TARGET_DB_PASSWORD', 'admin123456')
|
||||||
}
|
}
|
||||||
|
|
||||||
source_conn = None
|
source_conn = None
|
||||||
|
|||||||
406
migration_statistics.py
Normal file
406
migration_statistics.py
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
移行結果統計情報表示スクリプト
|
||||||
|
Docker Compose環境で実行可能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pytz
|
||||||
|
from collections import defaultdict
|
||||||
|
import json
|
||||||
|
|
||||||
|
def connect_database():
|
||||||
|
"""データベースに接続"""
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=os.environ.get('DB_HOST', 'postgres-db'),
|
||||||
|
port=os.environ.get('DB_PORT', '5432'),
|
||||||
|
database=os.environ.get('POSTGRES_DBNAME', 'rogdb'),
|
||||||
|
user=os.environ.get('POSTGRES_USER', 'admin'),
|
||||||
|
password=os.environ.get('POSTGRES_PASS', 'admin123456')
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ データベース接続エラー: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_basic_statistics(cursor):
|
||||||
|
"""基本統計情報を取得"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 移行データ基本統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# テーブル別レコード数
|
||||||
|
tables = [
|
||||||
|
('rog_newevent2', 'イベント'),
|
||||||
|
('rog_team', 'チーム'),
|
||||||
|
('rog_member', 'メンバー'),
|
||||||
|
('rog_entry', 'エントリー'),
|
||||||
|
('rog_gpscheckin', 'GPSチェックイン'),
|
||||||
|
('rog_checkpoint', 'チェックポイント'),
|
||||||
|
('rog_location2025', 'ロケーション2025'),
|
||||||
|
('rog_customuser', 'ユーザー')
|
||||||
|
]
|
||||||
|
|
||||||
|
print("\n📋 テーブル別レコード数:")
|
||||||
|
print("テーブル名 日本語名 レコード数")
|
||||||
|
print("-" * 65)
|
||||||
|
|
||||||
|
total_records = 0
|
||||||
|
for table_name, japanese_name in tables:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
total_records += count
|
||||||
|
print(f"{table_name:<25} {japanese_name:<15} {count:>10,}件")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{table_name:<25} {japanese_name:<15} {'エラー':>10}")
|
||||||
|
|
||||||
|
print("-" * 65)
|
||||||
|
print(f"{'合計':<41} {total_records:>10,}件")
|
||||||
|
|
||||||
|
def get_event_statistics(cursor):
|
||||||
|
"""イベント別統計情報"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🎯 イベント別統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# イベント一覧と基本情報
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, event_name, event_date, created_at
|
||||||
|
FROM rog_newevent2
|
||||||
|
ORDER BY event_date DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
events = cursor.fetchall()
|
||||||
|
if not events:
|
||||||
|
print("イベントデータがありません")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n📅 登録イベント数: {len(events)}件")
|
||||||
|
print("\nイベント詳細:")
|
||||||
|
print("ID イベント名 開催日 登録日時")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
event_id, event_name, event_date, created_at = event
|
||||||
|
event_date_str = event_date.strftime("%Y-%m-%d") if event_date else "未設定"
|
||||||
|
created_str = created_at.strftime("%Y-%m-%d %H:%M") if created_at else "未設定"
|
||||||
|
print(f"{event_id:>2} {event_name:<15} {event_date_str} {created_str}")
|
||||||
|
|
||||||
|
# イベント別参加統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
e.event_name,
|
||||||
|
COUNT(DISTINCT t.id) as team_count,
|
||||||
|
COUNT(DISTINCT m.id) as member_count,
|
||||||
|
COUNT(DISTINCT en.id) as entry_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_team t ON e.id = t.event_id
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
LEFT JOIN rog_entry en ON t.id = en.team_id
|
||||||
|
GROUP BY e.id, e.event_name
|
||||||
|
ORDER BY team_count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
participation_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
print("\n👥 イベント別参加統計:")
|
||||||
|
print("イベント名 チーム数 メンバー数 エントリー数")
|
||||||
|
print("-" * 55)
|
||||||
|
|
||||||
|
total_teams = 0
|
||||||
|
total_members = 0
|
||||||
|
total_entries = 0
|
||||||
|
|
||||||
|
for stat in participation_stats:
|
||||||
|
event_name, team_count, member_count, entry_count = stat
|
||||||
|
total_teams += team_count
|
||||||
|
total_members += member_count
|
||||||
|
total_entries += entry_count
|
||||||
|
print(f"{event_name:<15} {team_count:>6}件 {member_count:>8}件 {entry_count:>9}件")
|
||||||
|
|
||||||
|
print("-" * 55)
|
||||||
|
print(f"{'合計':<15} {total_teams:>6}件 {total_members:>8}件 {total_entries:>9}件")
|
||||||
|
|
||||||
|
def get_gps_checkin_statistics(cursor):
|
||||||
|
"""GPSチェックイン統計"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📍 GPSチェックイン統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 基本統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_checkins,
|
||||||
|
COUNT(DISTINCT zekken) as unique_teams,
|
||||||
|
COUNT(DISTINCT cp_number) as unique_checkpoints,
|
||||||
|
MIN(checkin_time) as earliest_checkin,
|
||||||
|
MAX(checkin_time) as latest_checkin
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
""")
|
||||||
|
|
||||||
|
basic_stats = cursor.fetchone()
|
||||||
|
if not basic_stats or basic_stats[0] == 0:
|
||||||
|
print("GPSチェックインデータがありません")
|
||||||
|
return
|
||||||
|
|
||||||
|
total_checkins, unique_teams, unique_checkpoints, earliest, latest = basic_stats
|
||||||
|
|
||||||
|
print(f"\n📊 基本統計:")
|
||||||
|
print(f"総チェックイン数: {total_checkins:,}件")
|
||||||
|
print(f"参加チーム数: {unique_teams:,}チーム")
|
||||||
|
print(f"利用CP数: {unique_checkpoints:,}箇所")
|
||||||
|
|
||||||
|
if earliest and latest:
|
||||||
|
print(f"最早チェックイン: {earliest.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"最終チェックイン: {latest.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# 時間帯別分析
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
EXTRACT(hour FROM checkin_time) as hour,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
GROUP BY EXTRACT(hour FROM checkin_time)
|
||||||
|
ORDER BY hour
|
||||||
|
""")
|
||||||
|
|
||||||
|
hourly_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"\n⏰ 時間帯別チェックイン分布:")
|
||||||
|
print("時間 チェックイン数 グラフ")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
max_count = max([count for _, count in hourly_stats]) if hourly_stats else 1
|
||||||
|
|
||||||
|
for hour, count in hourly_stats:
|
||||||
|
bar_length = int((count / max_count) * 20)
|
||||||
|
bar = "█" * bar_length
|
||||||
|
print(f"{int(hour):>2}時 {count:>8}件 {bar}")
|
||||||
|
|
||||||
|
# CP別利用統計(上位10位)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
cp_number,
|
||||||
|
COUNT(*) as checkin_count,
|
||||||
|
COUNT(DISTINCT zekken) as team_count
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
GROUP BY cp_number
|
||||||
|
ORDER BY checkin_count DESC
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
|
||||||
|
cp_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"\n🏅 CP利用ランキング(上位10位):")
|
||||||
|
print("順位 CP番号 チェックイン数 利用チーム数")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
for i, (cp_number, checkin_count, team_count) in enumerate(cp_stats, 1):
|
||||||
|
print(f"{i:>2}位 CP{cp_number:>3} {checkin_count:>8}件 {team_count:>7}チーム")
|
||||||
|
|
||||||
|
def get_team_statistics(cursor):
|
||||||
|
"""チーム統計"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("👥 チーム統計情報")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# チーム基本統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_teams,
|
||||||
|
COUNT(DISTINCT class_name) as unique_classes,
|
||||||
|
AVG(CASE WHEN member_count > 0 THEN member_count END) as avg_members
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.class_name,
|
||||||
|
COUNT(m.id) as member_count
|
||||||
|
FROM rog_team t
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
GROUP BY t.id, t.class_name
|
||||||
|
) team_stats
|
||||||
|
""")
|
||||||
|
|
||||||
|
team_basic = cursor.fetchone()
|
||||||
|
total_teams, unique_classes, avg_members = team_basic
|
||||||
|
|
||||||
|
print(f"\n📊 基本統計:")
|
||||||
|
print(f"総チーム数: {total_teams:,}チーム")
|
||||||
|
print(f"クラス数: {unique_classes or 0}種類")
|
||||||
|
print(f"平均メンバー数: {avg_members:.1f}人/チーム" if avg_members else "平均メンバー数: データなし")
|
||||||
|
|
||||||
|
# クラス別統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COALESCE(class_name, '未分類') as class_name,
|
||||||
|
COUNT(*) as team_count,
|
||||||
|
COUNT(CASE WHEN member_count > 0 THEN 1 END) as active_teams
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
t.class_name,
|
||||||
|
COUNT(m.id) as member_count
|
||||||
|
FROM rog_team t
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
GROUP BY t.id, t.class_name
|
||||||
|
) team_stats
|
||||||
|
GROUP BY class_name
|
||||||
|
ORDER BY team_count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
class_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
if class_stats:
|
||||||
|
print(f"\n🏆 クラス別チーム数:")
|
||||||
|
print("クラス名 チーム数 アクティブ")
|
||||||
|
print("-" * 35)
|
||||||
|
|
||||||
|
for class_name, team_count, active_teams in class_stats:
|
||||||
|
print(f"{class_name:<15} {team_count:>6}件 {active_teams:>7}件")
|
||||||
|
|
||||||
|
def get_data_quality_check(cursor):
|
||||||
|
"""データ品質チェック"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🔍 データ品質チェック")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
checks = []
|
||||||
|
|
||||||
|
# 1. 重複チェック
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM (
|
||||||
|
SELECT zekken, cp_number, checkin_time, COUNT(*)
|
||||||
|
FROM rog_gpscheckin
|
||||||
|
GROUP BY zekken, cp_number, checkin_time
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) duplicates
|
||||||
|
""")
|
||||||
|
duplicate_count = cursor.fetchone()[0]
|
||||||
|
checks.append(("重複チェックイン", duplicate_count, "件"))
|
||||||
|
|
||||||
|
# 2. 異常時刻チェック(0時台)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_gpscheckin
|
||||||
|
WHERE EXTRACT(hour FROM checkin_time) = 0
|
||||||
|
""")
|
||||||
|
zero_hour_count = cursor.fetchone()[0]
|
||||||
|
checks.append(("0時台チェックイン", zero_hour_count, "件"))
|
||||||
|
|
||||||
|
# 3. 未来日時チェック
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_gpscheckin
|
||||||
|
WHERE checkin_time > NOW()
|
||||||
|
""")
|
||||||
|
future_count = cursor.fetchone()[0]
|
||||||
|
checks.append(("未来日時チェックイン", future_count, "件"))
|
||||||
|
|
||||||
|
# 4. メンバー不在チーム
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_team t
|
||||||
|
LEFT JOIN rog_member m ON t.id = m.team_id
|
||||||
|
WHERE m.id IS NULL
|
||||||
|
""")
|
||||||
|
no_member_teams = cursor.fetchone()[0]
|
||||||
|
checks.append(("メンバー不在チーム", no_member_teams, "チーム"))
|
||||||
|
|
||||||
|
# 5. エントリー不在チーム
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*) FROM rog_team t
|
||||||
|
LEFT JOIN rog_entry e ON t.id = e.team_id
|
||||||
|
WHERE e.id IS NULL
|
||||||
|
""")
|
||||||
|
no_entry_teams = cursor.fetchone()[0]
|
||||||
|
checks.append(("エントリー不在チーム", no_entry_teams, "チーム"))
|
||||||
|
|
||||||
|
print("\n🧪 品質チェック結果:")
|
||||||
|
print("チェック項目 件数 状態")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
for check_name, count, unit in checks:
|
||||||
|
status = "✅ 正常" if count == 0 else "⚠️ 要確認"
|
||||||
|
print(f"{check_name:<15} {count:>6}{unit} {status}")
|
||||||
|
|
||||||
|
def export_statistics_json(cursor):
|
||||||
|
"""統計情報をJSONで出力"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📄 統計情報JSON出力")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
statistics = {}
|
||||||
|
|
||||||
|
# 基本統計
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
|
||||||
|
statistics['total_checkins'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(DISTINCT zekken) FROM rog_gpscheckin")
|
||||||
|
statistics['unique_teams'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM rog_newevent2")
|
||||||
|
statistics['total_events'] = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# イベント別統計
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT event_name, COUNT(*) as checkin_count
|
||||||
|
FROM rog_newevent2 e
|
||||||
|
LEFT JOIN rog_team t ON e.id = t.event_id
|
||||||
|
LEFT JOIN rog_gpscheckin g ON t.zekken = g.zekken
|
||||||
|
GROUP BY e.id, event_name
|
||||||
|
ORDER BY checkin_count DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
event_stats = {}
|
||||||
|
for event_name, count in cursor.fetchall():
|
||||||
|
event_stats[event_name] = count
|
||||||
|
|
||||||
|
statistics['event_checkins'] = event_stats
|
||||||
|
statistics['generated_at'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# ファイル出力
|
||||||
|
output_file = f"/app/migration_statistics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(statistics, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"✅ 統計情報をJSONで出力しました: {output_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ JSON出力エラー: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""メイン処理"""
|
||||||
|
print("🚀 移行結果統計情報表示スクリプト開始")
|
||||||
|
print(f"実行時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# データベース接続
|
||||||
|
conn = connect_database()
|
||||||
|
if not conn:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 各統計情報を表示
|
||||||
|
get_basic_statistics(cursor)
|
||||||
|
get_event_statistics(cursor)
|
||||||
|
get_gps_checkin_statistics(cursor)
|
||||||
|
get_team_statistics(cursor)
|
||||||
|
get_data_quality_check(cursor)
|
||||||
|
export_statistics_json(cursor)
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✅ 統計情報表示完了")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 統計処理中にエラーが発生しました: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user