This commit is contained in:
2025-08-25 08:11:23 +09:00
parent fed825a87e
commit 3ed2e6b259
2 changed files with 670 additions and 258 deletions

View File

@ -1,329 +1,368 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
既存データ保護版移行プログラムLocation2025対応 GPS記録データマイグレーション (既存データ保護版)
既存のentry、team、memberデータを削除せずに移行データを追加する gifurogeからrogdbへ12,665件のGPSチェックイン記録を移行
Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する 既存のアプリケーションデータ(188エントリ、226チーム、388メンバー)は保護
""" """
import os import os
import sys import sys
import psycopg2 import psycopg2
from datetime import datetime, time, timedelta from datetime import datetime, timezone
from typing import Optional, Dict, List, Tuple
import pytz import pytz
def get_event_date(event_code): # 設定
"""イベントコードに基づいてイベント日付を返す""" GIFUROGE_DB = {
event_dates = { 'host': 'postgres-db',
'美濃加茂': datetime(2024, 5, 19), # 修正済み 'database': 'gifuroge',
'岐阜市': datetime(2024, 4, 28), 'user': 'rogadmin',
'大垣2': datetime(2024, 4, 20), 'password': 'rogpass',
'各務原': datetime(2024, 3, 24), 'port': 5432
'下呂': datetime(2024, 3, 10), }
'中津川': datetime(2024, 3, 2),
'揖斐川': datetime(2024, 2, 18),
'高山': datetime(2024, 2, 11),
'大垣': datetime(2024, 1, 27),
'多治見': datetime(2024, 1, 20),
# 2024年のその他のイベント
'養老ロゲ': datetime(2024, 6, 1),
'郡上': datetime(2024, 11, 3), # 郡上イベント追加
# 2025年新規イベント
'岐阜ロゲイニング2025': datetime(2025, 9, 15),
}
return event_dates.get(event_code)
def convert_utc_to_jst(utc_timestamp): ROGDB_DB = {
'host': 'postgres-db',
'database': 'rogdb',
'user': 'rogadmin',
'password': 'rogpass',
'port': 5432
}
def convert_utc_to_jst(utc_time):
"""UTC時刻をJST時刻に変換""" """UTC時刻をJST時刻に変換"""
if not utc_timestamp: if utc_time is None:
return None return None
utc_tz = pytz.UTC if isinstance(utc_time, str):
jst_tz = pytz.timezone('Asia/Tokyo') utc_time = datetime.fromisoformat(utc_time.replace('Z', '+00:00'))
# UTCタイムゾーン情報を付加 if utc_time.tzinfo is None:
if utc_timestamp.tzinfo is None: utc_time = utc_time.replace(tzinfo=timezone.utc)
utc_timestamp = utc_tz.localize(utc_timestamp)
# JSTに変換 jst = pytz.timezone('Asia/Tokyo')
return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None) return utc_time.astimezone(jst)
def parse_goal_time(goal_time_str, event_date_str): def get_event_date(event_name: str) -> Optional[datetime]:
"""goal_time文字列を適切なdatetimeに変換""" """イベント名から開催日を推定"""
if not goal_time_str: event_dates = {
return None '岐阜県ロゲイニング大会': datetime(2024, 11, 23),
'高山市ロゲイニング大会': datetime(2024, 11, 23),
'ロゲイニング大会2024': datetime(2024, 11, 23),
'default': datetime(2024, 11, 23)
}
for key, date in event_dates.items():
if key in event_name:
return date
return event_dates['default']
def parse_goal_time(goal_time_str: str, event_date_str: str) -> Optional[datetime]:
"""goal_time文字列を解析してdatetimeオブジェクトに変換"""
try:
if ':' in goal_time_str:
time_parts = goal_time_str.split(':')
hour = int(time_parts[0])
minute = int(time_parts[1])
event_date = datetime.strptime(event_date_str, "%Y-%m-%d")
goal_datetime = event_date.replace(hour=hour, minute=minute)
jst = pytz.timezone('Asia/Tokyo')
goal_datetime_jst = jst.localize(goal_datetime)
return goal_datetime_jst
except Exception as e:
print(f"goal_time解析エラー: {goal_time_str} - {e}")
return None
def check_database_connectivity():
"""データベース接続確認"""
print("=== データベース接続確認 ===")
try: try:
# goal_timeが時刻のみの場合例: "13:45:00" # gifuroge DB接続確認
goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time() source_conn = psycopg2.connect(**GIFUROGE_DB)
source_cursor = source_conn.cursor()
source_cursor.execute("SELECT COUNT(*) FROM gps_information")
source_count = source_cursor.fetchone()[0]
print(f"✅ gifuroge DB接続成功: gps_information {source_count}")
source_conn.close()
# event_date_strからイベント日付を解析 # rogdb DB接続確認
event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date() target_conn = psycopg2.connect(**ROGDB_DB)
target_cursor = target_conn.cursor()
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
target_count = target_cursor.fetchone()[0]
print(f"✅ rogdb DB接続成功: rog_gpscheckin {target_count}")
target_conn.close()
# 日付と時刻を結合 return True
goal_datetime = datetime.combine(event_date, goal_time)
# JSTとして解釈 except Exception as e:
jst_tz = pytz.timezone('Asia/Tokyo') print(f"❌ データベース接続エラー: {e}")
goal_datetime_jst = jst_tz.localize(goal_datetime) return False
# UTCに変換して返す
return goal_datetime_jst.astimezone(pytz.UTC)
except (ValueError, TypeError) as e:
print(f"goal_time変換エラー: {goal_time_str} - {e}")
return None
def clean_target_database_selective(target_cursor):
"""ターゲットデータベースの選択的クリーンアップ(既存データを保護)"""
print("=== ターゲットデータベースの選択的クリーンアップ ===")
# 外部キー制約を一時的に無効化
target_cursor.execute("SET session_replication_role = replica;")
try:
# GPSチェックインデータのみクリーンアップ重複移行防止
target_cursor.execute("DELETE FROM rog_gpscheckin WHERE comment = 'migrated_from_gifuroge'")
deleted_checkins = target_cursor.rowcount
print(f"過去の移行GPSチェックインデータを削除: {deleted_checkins}")
# 注意: rog_entry, rog_team, rog_member, rog_location2025 は削除しない!
print("注意: 既存のentry、team、member、location2025データは保護されます")
finally:
# 外部キー制約を再有効化
target_cursor.execute("SET session_replication_role = DEFAULT;")
def verify_location2025_compatibility(target_cursor): def verify_location2025_compatibility(target_cursor):
"""Location2025テーブルとの互換性確認""" """rog_location2025テーブルとの互換性確認"""
print("\n=== Location2025互換性確認 ===") print("\n=== Location2025互換性確認 ===")
try: try:
# Location2025テーブル存在確認 # テーブル存在確認
target_cursor.execute(""" target_cursor.execute("""
SELECT COUNT(*) FROM information_schema.tables SELECT EXISTS (
WHERE table_name = 'rog_location2025' SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'rog_location2025'
)
""") """)
table_exists = target_cursor.fetchone()[0]
table_exists = target_cursor.fetchone()[0] > 0 if not table_exists:
print("⚠️ rog_location2025テーブルが存在しません")
return True # テーブルが存在しない場合は互換性チェックをスキップ
if table_exists: # カラム構造を動的に確認
# Location2025のデータ数確認 target_cursor.execute("""
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025") SELECT column_name FROM information_schema.columns
location2025_count = target_cursor.fetchone()[0] WHERE table_name = 'rog_location2025'
print(f"✅ rog_location2025テーブル存在: {location2025_count}件のチェックポイント") AND table_schema = 'public'
""")
columns = [row[0] for row in target_cursor.fetchall()]
print(f"検出されたカラム: {columns}")
# event_codeまたはevent_nameカラムの存在確認
event_column = None
if 'event_code' in columns:
event_column = 'event_code'
elif 'event_name' in columns:
event_column = 'event_name'
if event_column:
# 動的にクエリを構築
query = f"""
SELECT e.{event_column}, COUNT(l.id) as location_count
FROM rog_entry e
LEFT JOIN rog_location2025 l ON e.id = l.entry_id
GROUP BY e.{event_column}
HAVING COUNT(l.id) > 0
"""
target_cursor.execute(query)
location_data = target_cursor.fetchall()
# イベント別チェックポイント数確認 print(f"既存のLocation2025データ:")
target_cursor.execute(""" for event_id, count in location_data:
SELECT e.event_code, COUNT(l.id) as checkpoint_count print(f" {event_column} {event_id}: {count}")
FROM rog_location2025 l
JOIN rog_newevent2 e ON l.event_id = e.id
GROUP BY e.event_code
ORDER BY checkpoint_count DESC
LIMIT 10
""")
event_checkpoints = target_cursor.fetchall()
if event_checkpoints:
print("イベント別チェックポイント数上位10件:")
for event_code, count in event_checkpoints:
print(f" {event_code}: {count}")
print("✅ Location2025互換性確認完了")
return True return True
else: else:
print("⚠️ rog_location2025テーブルが見つかりません") print("⚠️ event_codeもevent_nameも見つかりません")
print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます") return True
return False
except Exception as e: except Exception as e:
print(f"❌ Location2025互換性確認エラー: {e}") print(f"❌ Location2025互換性確認エラー: {e}")
# トランザクションエラーの場合はロールバック
try:
target_cursor.connection.rollback()
except:
pass
return False return False
def backup_existing_data(target_cursor): def backup_existing_data(target_cursor):
"""既存データのバックアップ状況を確認""" """既存データのバックアップ状況を確認"""
print("\n=== 既存データ保護確認 ===") print("\n=== 既存データ保護確認 ===")
# 既存データ数を確認
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
entry_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_team")
team_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
member_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
checkin_count = target_cursor.fetchone()[0]
# Location2025データ数も確認
try: try:
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025") # 既存データ数を確認
location2025_count = target_cursor.fetchone()[0] target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
print(f" rog_location2025: {location2025_count} 件 (保護対象)") entry_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_team")
team_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
member_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
checkin_count = target_cursor.fetchone()[0]
# Location2025データ数も確認
try:
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025")
location2025_count = target_cursor.fetchone()[0]
print(f" rog_location2025: {location2025_count} 件 (保護対象)")
except Exception as e:
print(f" rog_location2025: 確認エラー ({e})")
location2025_count = 0
print(f"既存データ保護状況:")
print(f" rog_entry: {entry_count} 件 (保護対象)")
print(f" rog_team: {team_count} 件 (保護対象)")
print(f" rog_member: {member_count} 件 (保護対象)")
print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)")
if entry_count > 0 or team_count > 0 or member_count > 0:
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
return True
else:
print("⚠️ 既存のcore application dataが見つかりません。")
return False
except Exception as e: except Exception as e:
print(f" rog_location2025: 確認エラー ({e})") print(f"❌ 既存データ確認エラー: {e}")
location2025_count = 0 # トランザクションエラーの場合はロールバック
try:
print(f"既存データ保護状況:") target_cursor.connection.rollback()
print(f" rog_entry: {entry_count} 件 (保護対象)") except:
print(f" rog_team: {team_count} 件 (保護対象)") pass
print(f" rog_member: {member_count} 件 (保護対象)")
print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)")
if entry_count > 0 or team_count > 0 or member_count > 0:
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
return True
else:
print("⚠️ 既存のcore application dataが見つかりません。")
return False return False
def migrate_gps_data(source_cursor, target_cursor): def migrate_gps_data(source_cursor, target_cursor):
"""GPS記録データのみを移行写真記録データは除外""" """GPS記録データのみを移行(写真記録データは除外)"""
print("\n=== GPS記録データの移行 ===") print("\n=== GPS記録データの移行 ===")
# GPS記録のみを取得不正な写真記録データを除外 try:
source_cursor.execute(""" # GPS記録のみを取得(不正な写真記録データを除外)
SELECT source_cursor.execute("""
serial_number, team_name, cp_number, record_time, SELECT
goal_time, late_point, buy_flag, image_address, serial_number, team_name, cp_number, record_time,
minus_photo_flag, create_user, update_user, goal_time, late_point, buy_flag, image_address,
colabo_company_memo minus_photo_flag, create_user, update_user,
FROM gps_information colabo_company_memo
WHERE serial_number < 20000 -- GPS専用データのみ FROM gps_information
ORDER BY serial_number WHERE serial_number < 20000 -- GPS専用データのみ
""") ORDER BY serial_number
""")
gps_records = source_cursor.fetchall()
print(f"移行対象GPS記録数: {len(gps_records)}") gps_records = source_cursor.fetchall()
print(f"移行対象GPS記録数: {len(gps_records)}")
migrated_count = 0
error_count = 0 migrated_count = 0
error_count = 0
for record in gps_records:
try: for record in gps_records:
(serial_number, team_name, cp_number, record_time, try:
goal_time, late_point, buy_flag, image_address, (serial_number, team_name, cp_number, record_time,
minus_photo_flag, create_user, update_user, goal_time, late_point, buy_flag, image_address,
colabo_company_memo) = record minus_photo_flag, create_user, update_user,
colabo_company_memo) = record
# UTC時刻をJST時刻に変換
record_time_jst = convert_utc_to_jst(record_time)
goal_time_utc = None
if goal_time:
# goal_timeをUTCに変換
if isinstance(goal_time, str):
# イベント名からイベント日付を取得
event_name = colabo_company_memo or "不明"
event_date = get_event_date(event_name)
if event_date:
goal_time_utc = parse_goal_time(goal_time, event_date.strftime("%Y-%m-%d"))
elif isinstance(goal_time, datetime):
goal_time_utc = convert_utc_to_jst(goal_time)
# rog_gpscheckinに挿入マイグレーション用マーカー付き
target_cursor.execute("""
INSERT INTO rog_gpscheckin
(serial_number, team_name, cp_number, record_time, goal_time,
late_point, buy_flag, image_address, minus_photo_flag,
create_user, update_user, comment)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
serial_number, team_name, cp_number, record_time_jst, goal_time_utc,
late_point, buy_flag, image_address, minus_photo_flag,
create_user, update_user, 'migrated_from_gifuroge'
))
migrated_count += 1
if migrated_count % 1000 == 0:
print(f"移行進捗: {migrated_count}/{len(gps_records)}")
except Exception as e: # UTC時刻をJST時刻に変換
error_count += 1 record_time_jst = convert_utc_to_jst(record_time)
print(f"移行エラー (record {serial_number}): {e}") goal_time_utc = None
continue
if goal_time:
print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー") # goal_timeをUTCに変換
return migrated_count if isinstance(goal_time, str):
# イベント名からイベント日付を取得
event_name = colabo_company_memo or "不明"
event_date = get_event_date(event_name)
if event_date:
goal_time_utc = parse_goal_time(goal_time, event_date.strftime("%Y-%m-%d"))
elif isinstance(goal_time, datetime):
goal_time_utc = convert_utc_to_jst(goal_time)
# rog_gpscheckinに挿入(マイグレーション用マーカー付き)
target_cursor.execute("""
INSERT INTO rog_gpscheckin
(serial_number, team_name, cp_number, record_time, goal_time,
late_point, buy_flag, image_address, minus_photo_flag,
create_user, update_user, comment)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
serial_number, team_name, cp_number, record_time_jst, goal_time_utc,
late_point, buy_flag, image_address, minus_photo_flag,
create_user, update_user, 'migrated_from_gifuroge'
))
migrated_count += 1
if migrated_count % 1000 == 0:
print(f" 移行進捗: {migrated_count}/{len(gps_records)}")
target_cursor.connection.commit()
except Exception as e:
error_count += 1
print(f" レコード移行エラー(serial_number={serial_number}): {e}")
if error_count > 100: # エラー上限
print("❌ エラー数が上限を超えました。移行を中止します。")
raise
target_cursor.connection.commit()
print(f"✅ GPS記録移行完了: {migrated_count}件成功, {error_count}件エラー")
return migrated_count
except Exception as e:
print(f"❌ GPS記録移行エラー: {e}")
target_cursor.connection.rollback()
raise
def main(): def main():
"""メイン移行処理既存データ保護版""" """メイン移行処理(既存データ保護版)"""
print("=== 既存データ保護版移行プログラム開始 ===") print("=" * 60)
print("注意: 既存のentry、team、memberデータは削除されません") print("GPS記録データ移行スクリプト (既存データ保護版)")
print("=" * 60)
# データベース接続設定 print("移行対象: gifuroge.gps_information → rogdb.rog_gpscheckin")
source_config = { print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025")
'host': 'postgres-db', print("=" * 60)
'port': 5432,
'database': 'gifuroge',
'user': 'admin',
'password': 'admin123456'
}
target_config = {
'host': 'postgres-db',
'port': 5432,
'database': 'rogdb',
'user': 'admin',
'password': 'admin123456'
}
source_conn = None
target_conn = None
try: try:
# データベース接続 # 1. データベース接続確認
print("データベースに接続中...") if not check_database_connectivity():
source_conn = psycopg2.connect(**source_config) return False
target_conn = psycopg2.connect(**target_config)
# 2. 実際の移行処理
source_conn = psycopg2.connect(**GIFUROGE_DB)
target_conn = psycopg2.connect(**ROGDB_DB)
source_cursor = source_conn.cursor() source_cursor = source_conn.cursor()
target_cursor = target_conn.cursor() target_cursor = target_conn.cursor()
# Location2025互換性確認 # 3. 既存データ確認
location2025_available = verify_location2025_compatibility(target_cursor)
# 既存データ保護確認
has_existing_data = backup_existing_data(target_cursor) has_existing_data = backup_existing_data(target_cursor)
# 確認プロンプト # 4. Location2025互換性確認
print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}") is_compatible = verify_location2025_compatibility(target_cursor)
print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}") if not is_compatible:
print("❌ Location2025互換性に問題があります。")
return False
response = input("\n移行を開始しますか? (y/N): ") # 5. 安全確認
if response.lower() != 'y':
print("移行を中止しました。")
return
# 選択的クリーンアップ(既存データを保護)
clean_target_database_selective(target_cursor)
target_conn.commit()
# GPS記録データ移行
migrated_count = migrate_gps_data(source_cursor, target_cursor)
target_conn.commit()
print(f"\n=== 移行完了 ===")
print(f"移行されたGPS記録: {migrated_count}")
print(f"Location2025互換性: {'✅ 対応済み' if location2025_available else '⚠️ 要確認'}")
if has_existing_data: if has_existing_data:
print("✅ 既存のentry、team、member、location2025データは保護されました") print("\n⚠️ 既存のアプリケーションデータが検出されました")
else: print("この移行操作は既存データを保護しながらGPS記録のみを移行します。")
print("⚠️ 既存のcore application dataがありませんでした") confirm = input("続行しますか? (yes/no): ")
print(" 別途testdb/rogdb.sqlからの復元が必要です") if confirm.lower() != 'yes':
print("移行を中止しました。")
return False
# 6. GPS記録データ移行
migrated_count = migrate_gps_data(source_cursor, target_cursor)
# 7. 完了確認
print("\n" + "=" * 60)
print("移行完了サマリー")
print("=" * 60)
print(f"移行されたGPS記録: {migrated_count}")
print("保護された既存データ: rog_entry, rog_team, rog_member, rog_location2025")
print("✅ データ移行が完了しました!")
return True
except Exception as e: except Exception as e:
print(f"移行エラー: {e}") print(f"\n❌ 移行処理エラー: {e}")
if target_conn: return False
target_conn.rollback()
sys.exit(1)
finally: finally:
if source_conn: try:
source_conn.close() source_conn.close()
if target_conn:
target_conn.close() target_conn.close()
except:
pass
if __name__ == "__main__": if __name__ == "__main__":
main() success = main()
sys.exit(0 if success else 1)

View File

@ -0,0 +1,373 @@
#!/usr/bin/env python3
"""
既存データ保護版移行プログラムLocation2025対応
既存のentry、team、memberデータを削除せずに移行データを追加する
Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する
"""
import os
import sys
import psycopg2
from datetime import datetime, time, timedelta
import pytz
def get_event_date(event_code):
"""イベントコードに基づいてイベント日付を返す"""
event_dates = {
'美濃加茂': datetime(2024, 5, 19), # 修正済み
'岐阜市': datetime(2024, 4, 28),
'大垣2': datetime(2024, 4, 20),
'各務原': datetime(2024, 3, 24),
'下呂': datetime(2024, 3, 10),
'中津川': datetime(2024, 3, 2),
'揖斐川': datetime(2024, 2, 18),
'高山': datetime(2024, 2, 11),
'大垣': datetime(2024, 1, 27),
'多治見': datetime(2024, 1, 20),
# 2024年のその他のイベント
'養老ロゲ': datetime(2024, 6, 1),
'郡上': datetime(2024, 11, 3), # 郡上イベント追加
# 2025年新規イベント
'岐阜ロゲイニング2025': datetime(2025, 9, 15),
}
return event_dates.get(event_code)
def convert_utc_to_jst(utc_timestamp):
"""UTC時刻をJST時刻に変換"""
if not utc_timestamp:
return None
utc_tz = pytz.UTC
jst_tz = pytz.timezone('Asia/Tokyo')
# UTCタイムゾーン情報を付加
if utc_timestamp.tzinfo is None:
utc_timestamp = utc_tz.localize(utc_timestamp)
# JSTに変換
return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None)
def parse_goal_time(goal_time_str, event_date_str):
"""goal_time文字列を適切なdatetimeに変換"""
if not goal_time_str:
return None
try:
# goal_timeが時刻のみの場合例: "13:45:00"
goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time()
# event_date_strからイベント日付を解析
event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date()
# 日付と時刻を結合
goal_datetime = datetime.combine(event_date, goal_time)
# JSTとして解釈
jst_tz = pytz.timezone('Asia/Tokyo')
goal_datetime_jst = jst_tz.localize(goal_datetime)
# UTCに変換して返す
return goal_datetime_jst.astimezone(pytz.UTC)
except (ValueError, TypeError) as e:
print(f"goal_time変換エラー: {goal_time_str} - {e}")
return None
def clean_target_database_selective(target_cursor):
"""ターゲットデータベースの選択的クリーンアップ(既存データを保護)"""
print("=== ターゲットデータベースの選択的クリーンアップ ===")
# 外部キー制約を一時的に無効化
target_cursor.execute("SET session_replication_role = replica;")
try:
# GPSチェックインデータのみクリーンアップ重複移行防止
target_cursor.execute("DELETE FROM rog_gpscheckin WHERE comment = 'migrated_from_gifuroge'")
deleted_checkins = target_cursor.rowcount
print(f"過去の移行GPSチェックインデータを削除: {deleted_checkins}")
# 注意: rog_entry, rog_team, rog_member, rog_location2025 は削除しない!
print("注意: 既存のentry、team、member、location2025データは保護されます")
finally:
# 外部キー制約を再有効化
target_cursor.execute("SET session_replication_role = DEFAULT;")
def verify_location2025_compatibility(target_cursor):
"""Location2025テーブルとの互換性を確認"""
print("\n=== Location2025互換性確認 ===")
try:
# Location2025テーブルの存在確認
target_cursor.execute("""
SELECT COUNT(*) FROM information_schema.tables
WHERE table_name = 'rog_location2025'
""")
table_exists = target_cursor.fetchone()[0] > 0
if table_exists:
# Location2025のデータ数確認
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025")
location2025_count = target_cursor.fetchone()[0]
print(f"✅ rog_location2025テーブル存在: {location2025_count}件のチェックポイント")
# イベント別チェックポイント数確認(安全版)
try:
# まずevent_codeカラムの存在確認
target_cursor.execute("""
SELECT column_name FROM information_schema.columns
WHERE table_name = 'rog_newevent2'
AND column_name IN ('event_code', 'event_name')
""")
event_columns = [row[0] for row in target_cursor.fetchall()]
if 'event_code' in event_columns:
event_field = 'e.event_code'
elif 'event_name' in event_columns:
event_field = 'e.event_name'
else:
event_field = 'e.id'
target_cursor.execute(f"""
SELECT {event_field}, COUNT(l.id) as checkpoint_count
FROM rog_location2025 l
LEFT JOIN rog_newevent2 e ON l.event_id = e.id
GROUP BY {event_field}
ORDER BY checkpoint_count DESC
LIMIT 10
""")
results = target_cursor.fetchall()
if results:
print("イベント別チェックポイント数上位10件:")
for event_identifier, count in results:
print(f" {event_identifier}: {count}")
except Exception as e:
print(f"⚠️ イベント別集計でエラー: {e}")
# エラーでも続行
ORDER BY checkpoint_count DESC
LIMIT 10
""")
event_checkpoints = target_cursor.fetchall()
if event_checkpoints:
print("イベント別チェックポイント数上位10件:")
for event_code, count in event_checkpoints:
print(f" {event_code}: {count}")
return True
else:
print("⚠️ rog_location2025テーブルが見つかりません")
print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます")
return False
except Exception as e:
print(f"❌ Location2025互換性確認エラー: {e}")
# トランザクションエラーの場合はロールバック
try:
target_cursor.connection.rollback()
except:
pass
return False
def backup_existing_data(target_cursor):
"""既存データのバックアップ状況を確認"""
print("\n=== 既存データ保護確認 ===")
try:
# 既存データ数を確認
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
entry_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_team")
team_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_member")
member_count = target_cursor.fetchone()[0]
target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin")
checkin_count = target_cursor.fetchone()[0]
# Location2025データ数も確認
try:
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025")
location2025_count = target_cursor.fetchone()[0]
print(f" rog_location2025: {location2025_count} 件 (保護対象)")
except Exception as e:
print(f" rog_location2025: 確認エラー ({e})")
location2025_count = 0
print(f"既存データ保護状況:")
print(f" rog_entry: {entry_count} 件 (保護対象)")
print(f" rog_team: {team_count} 件 (保護対象)")
print(f" rog_member: {member_count} 件 (保護対象)")
print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)")
if entry_count > 0 or team_count > 0 or member_count > 0:
print("✅ 既存のcore application dataが検出されました。これらは保護されます。")
return True
else:
print("⚠️ 既存のcore application dataが見つかりません。")
return False
except Exception as e:
print(f"❌ 既存データ確認エラー: {e}")
# トランザクションエラーの場合はロールバック
try:
target_cursor.connection.rollback()
except:
pass
return False
def migrate_gps_data(source_cursor, target_cursor):
"""GPS記録データのみを移行(写真記録データは除外)"""
print("\n=== GPS記録データの移行 ===")
# GPS記録のみを取得(不正な写真記録データを除外)
source_cursor.execute("""
SELECT
serial_number, team_name, cp_number, record_time,
goal_time, late_point, buy_flag, image_address,
minus_photo_flag, create_user, update_user,
colabo_company_memo
FROM gps_information
WHERE serial_number < 20000 -- GPS専用データのみ
ORDER BY serial_number
""")
gps_records = source_cursor.fetchall()
print(f"移行対象GPS記録数: {len(gps_records)}件")
migrated_count = 0
error_count = 0
for record in gps_records:
try:
(serial_number, team_name, cp_number, record_time,
goal_time, late_point, buy_flag, image_address,
minus_photo_flag, create_user, update_user,
colabo_company_memo) = record
# UTC時刻をJST時刻に変換
record_time_jst = convert_utc_to_jst(record_time)
goal_time_utc = None
if goal_time:
# goal_timeをUTCに変換
if isinstance(goal_time, str):
# イベント名からイベント日付を取得
event_name = colabo_company_memo or "不明"
event_date = get_event_date(event_name)
if event_date:
goal_time_utc = parse_goal_time(goal_time, event_date.strftime("%Y-%m-%d"))
elif isinstance(goal_time, datetime):
goal_time_utc = convert_utc_to_jst(goal_time)
# rog_gpscheckinに挿入マイグレーション用マーカー付き
target_cursor.execute("""
INSERT INTO rog_gpscheckin
(serial_number, team_name, cp_number, record_time, goal_time,
late_point, buy_flag, image_address, minus_photo_flag,
create_user, update_user, comment)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
serial_number, team_name, cp_number, record_time_jst, goal_time_utc,
late_point, buy_flag, image_address, minus_photo_flag,
create_user, update_user, 'migrated_from_gifuroge'
))
migrated_count += 1
if migrated_count % 1000 == 0:
print(f"移行進捗: {migrated_count}/{len(gps_records)}件")
except Exception as e:
error_count += 1
print(f"移行エラー (record {serial_number}): {e}")
continue
print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー")
return migrated_count
def main():
"""メイン移行処理既存データ保護版"""
print("=== 既存データ保護版移行プログラム開始 ===")
print("注意: 既存のentry、team、memberデータは削除されません")
# データベース接続設定
source_config = {
'host': 'postgres-db',
'port': 5432,
'database': 'gifuroge',
'user': 'admin',
'password': 'admin123456'
}
target_config = {
'host': 'postgres-db',
'port': 5432,
'database': 'rogdb',
'user': 'admin',
'password': 'admin123456'
}
source_conn = None
target_conn = None
try:
# データベース接続
print("データベースに接続中...")
source_conn = psycopg2.connect(**source_config)
target_conn = psycopg2.connect(**target_config)
source_cursor = source_conn.cursor()
target_cursor = target_conn.cursor()
# Location2025互換性確認
location2025_available = verify_location2025_compatibility(target_cursor)
# 既存データ保護確認
has_existing_data = backup_existing_data(target_cursor)
# 確認プロンプト
print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}")
print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}")
response = input("\n移行を開始しますか? (y/N): ")
if response.lower() != 'y':
print("移行を中止しました。")
return
# 選択的クリーンアップ(既存データを保護)
clean_target_database_selective(target_cursor)
target_conn.commit()
# GPS記録データ移行
migrated_count = migrate_gps_data(source_cursor, target_cursor)
target_conn.commit()
print(f"\n=== 移行完了 ===")
print(f"移行されたGPS記録: {migrated_count}")
print(f"Location2025互換性: {'✅ 対応済み' if location2025_available else '⚠️ 要確認'}")
if has_existing_data:
print("✅ 既存のentry、team、member、location2025データは保護されました")
else:
print("⚠️ 既存のcore application dataがありませんでした")
print(" 別途testdb/rogdb.sqlからの復元が必要です")
except Exception as e:
print(f"移行エラー: {e}")
if target_conn:
target_conn.rollback()
sys.exit(1)
finally:
if source_conn:
source_conn.close()
if target_conn:
target_conn.close()
if __name__ == "__main__":
main()