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
"""
既存データ保護版移行プログラムLocation2025対応
既存のentry、team、memberデータを削除せずに移行データを追加する
Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する
GPS記録データマイグレーション (既存データ保護版)
gifurogeからrogdbへ12,665件のGPSチェックイン記録を移行
既存のアプリケーションデータ(188エントリ、226チーム、388メンバー)は保護
"""
import os
import sys
import psycopg2
from datetime import datetime, time, timedelta
from datetime import datetime, timezone
from typing import Optional, Dict, List, Tuple
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)
# 設定
GIFUROGE_DB = {
'host': 'postgres-db',
'database': 'gifuroge',
'user': 'rogadmin',
'password': 'rogpass',
'port': 5432
}
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時刻に変換"""
if not utc_timestamp:
if utc_time is None:
return None
utc_tz = pytz.UTC
jst_tz = pytz.timezone('Asia/Tokyo')
if isinstance(utc_time, str):
utc_time = datetime.fromisoformat(utc_time.replace('Z', '+00:00'))
# UTCタイムゾーン情報を付加
if utc_timestamp.tzinfo is None:
utc_timestamp = utc_tz.localize(utc_timestamp)
if utc_time.tzinfo is None:
utc_time = utc_time.replace(tzinfo=timezone.utc)
# JSTに変換
return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None)
jst = pytz.timezone('Asia/Tokyo')
return utc_time.astimezone(jst)
def parse_goal_time(goal_time_str, event_date_str):
"""goal_time文字列を適切なdatetimeに変換"""
if not goal_time_str:
return None
def get_event_date(event_name: str) -> Optional[datetime]:
"""イベント名から開催日を推定"""
event_dates = {
'岐阜県ロゲイニング大会': 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:
# goal_timeが時刻のみの場合例: "13:45:00"
goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time()
# gifuroge DB接続確認
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からイベント日付を解析
event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date()
# rogdb DB接続確認
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()
# 日付と時刻を結合
goal_datetime = datetime.combine(event_date, goal_time)
return True
# 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;")
except Exception as e:
print(f"❌ データベース接続エラー: {e}")
return False
def verify_location2025_compatibility(target_cursor):
"""Location2025テーブルとの互換性確認"""
"""rog_location2025テーブルとの互換性確認"""
print("\n=== Location2025互換性確認 ===")
try:
# Location2025テーブル存在確認
# テーブル存在確認
target_cursor.execute("""
SELECT COUNT(*) FROM information_schema.tables
WHERE table_name = 'rog_location2025'
SELECT EXISTS (
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("SELECT COUNT(*) FROM rog_location2025")
location2025_count = target_cursor.fetchone()[0]
print(f"✅ rog_location2025テーブル存在: {location2025_count}件のチェックポイント")
# カラム構造を動的に確認
target_cursor.execute("""
SELECT column_name FROM information_schema.columns
WHERE table_name = 'rog_location2025'
AND table_schema = 'public'
""")
columns = [row[0] for row in target_cursor.fetchall()]
print(f"検出されたカラム: {columns}")
# イベント別チェックポイント数確認
target_cursor.execute("""
SELECT e.event_code, COUNT(l.id) as checkpoint_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_codeまたはevent_nameカラムの存在確認
event_column = None
if 'event_code' in columns:
event_column = 'event_code'
elif 'event_name' in columns:
event_column = 'event_name'
event_checkpoints = target_cursor.fetchall()
if event_checkpoints:
print("イベント別チェックポイント数上位10件:")
for event_code, count in event_checkpoints:
print(f" {event_code}: {count}")
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データ:")
for event_id, count in location_data:
print(f" {event_column} {event_id}: {count}")
print("✅ Location2025互換性確認完了")
return True
else:
print("⚠️ rog_location2025テーブルが見つかりません")
print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます")
return False
print("⚠️ event_codeもevent_nameも見つかりません")
return True
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=== 既存データ保護確認 ===")
# 既存データ数を確認
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} 件 (保護対象)")
# 既存データ数を確認
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" 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が見つかりません。")
print(f"❌ 既存データ確認エラー: {e}")
# トランザクションエラーの場合はロールバック
try:
target_cursor.connection.rollback()
except:
pass
return False
def migrate_gps_data(source_cursor, target_cursor):
"""GPS記録データのみを移行写真記録データは除外"""
"""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
""")
try:
# 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)}")
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:
(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
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
# 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)
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'
))
# 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
migrated_count += 1
if migrated_count % 1000 == 0:
print(f"移行進捗: {migrated_count}/{len(gps_records)}")
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"移行エラー (record {serial_number}): {e}")
continue
except Exception as e:
error_count += 1
print(f" レコード移行エラー(serial_number={serial_number}): {e}")
if error_count > 100: # エラー上限
print("❌ エラー数が上限を超えました。移行を中止します。")
raise
print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー")
return migrated_count
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():
"""メイン移行処理既存データ保護版"""
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
"""メイン移行処理(既存データ保護版)"""
print("=" * 60)
print("GPS記録データ移行スクリプト (既存データ保護版)")
print("=" * 60)
print("移行対象: gifuroge.gps_information → rogdb.rog_gpscheckin")
print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025")
print("=" * 60)
try:
# データベース接続
print("データベースに接続中...")
source_conn = psycopg2.connect(**source_config)
target_conn = psycopg2.connect(**target_config)
# 1. データベース接続確認
if not check_database_connectivity():
return False
# 2. 実際の移行処理
source_conn = psycopg2.connect(**GIFUROGE_DB)
target_conn = psycopg2.connect(**ROGDB_DB)
source_cursor = source_conn.cursor()
target_cursor = target_conn.cursor()
# Location2025互換性確認
location2025_available = verify_location2025_compatibility(target_cursor)
# 既存データ保護確認
# 3. 既存データ確認
has_existing_data = backup_existing_data(target_cursor)
# 確認プロンプト
print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}")
print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}")
# 4. Location2025互換性確認
is_compatible = verify_location2025_compatibility(target_cursor)
if not is_compatible:
print("❌ Location2025互換性に問題があります。")
return False
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 '⚠️ 要確認'}")
# 5. 安全確認
if has_existing_data:
print("✅ 既存のentry、team、member、location2025データは保護されました")
else:
print("⚠️ 既存のcore application dataがありませんでした")
print(" 別途testdb/rogdb.sqlからの復元が必要です")
print("\n⚠️ 既存のアプリケーションデータが検出されました")
print("この移行操作は既存データを保護しながらGPS記録のみを移行します。")
confirm = input("続行しますか? (yes/no): ")
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:
print(f"移行エラー: {e}")
if target_conn:
target_conn.rollback()
sys.exit(1)
print(f"\n❌ 移行処理エラー: {e}")
return False
finally:
if source_conn:
try:
source_conn.close()
if target_conn:
target_conn.close()
except:
pass
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()