Fix migration

This commit is contained in:
2025-08-25 18:49:33 +09:00
parent 6886ba15c8
commit 8e3f7024a2
12 changed files with 2254 additions and 20 deletions

205
MIGRATE_OLD_ROGDB_README.md Normal file
View 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ライセンスの下で公開されています。

View 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/` ディレクトリに保存されます

View File

@ -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
View 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
View 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()

View 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()

View 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"

View 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()

View 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()

View File

@ -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)

View File

@ -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
View 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()