355 lines
14 KiB
Python
355 lines
14 KiB
Python
import logging
|
||
from django.core.management.base import BaseCommand
|
||
from django.db import connections, transaction, connection
|
||
from django.db.models import Q
|
||
from django.contrib.gis.geos import Point
|
||
from django.utils import timezone
|
||
from rog.models import Team, NewEvent2, Checkpoint, GpsCheckin, GpsLog, Entry
|
||
from datetime import datetime
|
||
import psycopg2
|
||
from psycopg2.extras import execute_values
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class Command(BaseCommand):
|
||
help = 'MobServerデータベースからDjangoモデルにデータを移行します'
|
||
|
||
def add_arguments(self, parser):
|
||
parser.add_argument(
|
||
'--dry-run',
|
||
action='store_true',
|
||
dest='dry_run',
|
||
help='実際の移行を行わず、処理内容のみを表示します',
|
||
)
|
||
parser.add_argument(
|
||
'--batch-size',
|
||
type=int,
|
||
default=100,
|
||
help='バッチサイズ(デフォルト: 100)'
|
||
)
|
||
|
||
def handle(self, *args, **options):
|
||
dry_run = options['dry_run']
|
||
batch_size = options['batch_size']
|
||
|
||
if dry_run:
|
||
self.stdout.write(self.style.WARNING('ドライランモードで実行中...'))
|
||
|
||
# MobServerデータベース接続を取得
|
||
mobserver_conn = connections['mobserver']
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
self.migrate_events(mobserver_conn, dry_run, batch_size)
|
||
self.migrate_teams(mobserver_conn, dry_run, batch_size)
|
||
self.migrate_checkpoints(mobserver_conn, dry_run, batch_size)
|
||
self.migrate_gps_logs(mobserver_conn, dry_run, batch_size)
|
||
|
||
if dry_run:
|
||
raise transaction.TransactionManagementError("ドライランのためロールバックします")
|
||
|
||
except transaction.TransactionManagementError:
|
||
if dry_run:
|
||
self.stdout.write(self.style.SUCCESS('ドライランが完了しました(変更は保存されていません)'))
|
||
else:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f'データ移行エラー: {e}')
|
||
self.stdout.write(self.style.ERROR(f'エラーが発生しました: {e}'))
|
||
raise
|
||
|
||
def migrate_events(self, conn, dry_run, batch_size):
|
||
"""イベント情報を移行"""
|
||
self.stdout.write('イベント情報を移行中...')
|
||
|
||
with conn.cursor() as cursor:
|
||
cursor.execute("SELECT * FROM event_table")
|
||
rows = cursor.fetchall()
|
||
|
||
events_migrated = 0
|
||
batch_data = []
|
||
|
||
for row in rows:
|
||
(event_code, event_name, start_time, event_day) = row
|
||
|
||
# start_timeのデータクリーニング
|
||
cleaned_start_time = start_time
|
||
if start_time and isinstance(start_time, str):
|
||
# セミコロンをコロンに置換
|
||
cleaned_start_time = start_time.replace(';', ':')
|
||
# タイムゾーン情報を含む場合は時間部分のみ抽出
|
||
if '+' in cleaned_start_time or 'T' in cleaned_start_time:
|
||
try:
|
||
from datetime import datetime
|
||
dt = datetime.fromisoformat(cleaned_start_time.replace('Z', '+00:00'))
|
||
cleaned_start_time = dt.strftime('%H:%M:%S')
|
||
except:
|
||
cleaned_start_time = None
|
||
|
||
if not dry_run:
|
||
batch_data.append(NewEvent2(
|
||
event_code=event_code,
|
||
event_name=event_name,
|
||
event_day=event_day,
|
||
start_time=cleaned_start_time,
|
||
))
|
||
|
||
events_migrated += 1
|
||
|
||
# バッチ処理
|
||
if len(batch_data) >= batch_size:
|
||
if not dry_run:
|
||
NewEvent2.objects.bulk_create(batch_data, ignore_conflicts=True)
|
||
batch_data = []
|
||
|
||
# 残りのデータを処理
|
||
if batch_data and not dry_run:
|
||
NewEvent2.objects.bulk_create(batch_data, ignore_conflicts=True)
|
||
|
||
self.stdout.write(f'{events_migrated}件のイベントを移行しました')
|
||
|
||
def migrate_teams(self, conn, dry_run, batch_size):
|
||
"""チーム情報を移行"""
|
||
self.stdout.write('チーム情報を移行中...')
|
||
|
||
with conn.cursor() as cursor:
|
||
cursor.execute("SELECT * FROM team_table")
|
||
rows = cursor.fetchall()
|
||
|
||
teams_migrated = 0
|
||
batch_data = []
|
||
|
||
for row in rows:
|
||
(zekken_number, event_code, team_name, class_name, password, trial) = row
|
||
|
||
# 対応するイベントを取得
|
||
try:
|
||
event = NewEvent2.objects.get(event_code=event_code)
|
||
except NewEvent2.DoesNotExist:
|
||
self.stdout.write(self.style.WARNING(f' 警告: イベント {event_code} が見つかりません。スキップします。'))
|
||
continue
|
||
|
||
if not dry_run:
|
||
batch_data.append(Team(
|
||
zekken_number=zekken_number,
|
||
team_name=team_name,
|
||
event=event,
|
||
class_name=class_name,
|
||
password=password,
|
||
trial=trial,
|
||
))
|
||
|
||
teams_migrated += 1
|
||
|
||
# バッチ処理
|
||
if len(batch_data) >= batch_size:
|
||
if not dry_run:
|
||
Team.objects.bulk_create(batch_data, ignore_conflicts=True)
|
||
batch_data = []
|
||
|
||
# 残りのデータを処理
|
||
if batch_data and not dry_run:
|
||
Team.objects.bulk_create(batch_data, ignore_conflicts=True)
|
||
|
||
self.stdout.write(f'{teams_migrated}件のチームを移行しました')
|
||
|
||
def migrate_checkpoints(self, conn, dry_run, batch_size):
|
||
"""チェックポイント情報を移行"""
|
||
self.stdout.write('チェックポイント情報を移行中...')
|
||
|
||
with conn.cursor() as cursor:
|
||
cursor.execute("SELECT * FROM checkpoint_table")
|
||
rows = cursor.fetchall()
|
||
|
||
checkpoints_migrated = 0
|
||
batch_data = []
|
||
|
||
for row in rows:
|
||
(cp_number, event_code, cp_name, latitude, longitude,
|
||
photo_point, buy_point, sample_photo, colabo_company_memo) = row
|
||
|
||
# 対応するイベントを取得
|
||
try:
|
||
event = NewEvent2.objects.get(event_code=event_code)
|
||
except NewEvent2.DoesNotExist:
|
||
continue
|
||
|
||
# 位置情報の処理
|
||
location = None
|
||
if latitude is not None and longitude is not None:
|
||
try:
|
||
location = Point(longitude, latitude) # Pointは(longitude, latitude)の順序
|
||
except (ValueError, AttributeError):
|
||
pass
|
||
|
||
if not dry_run:
|
||
batch_data.append(Checkpoint(
|
||
cp_number=cp_number,
|
||
event=event,
|
||
cp_name=cp_name,
|
||
location=location,
|
||
photo_point=photo_point or 0,
|
||
buy_point=buy_point or 0,
|
||
sample_photo=sample_photo,
|
||
colabo_company_memo=colabo_company_memo,
|
||
))
|
||
|
||
checkpoints_migrated += 1
|
||
|
||
# バッチ処理
|
||
if len(batch_data) >= batch_size:
|
||
if not dry_run:
|
||
Checkpoint.objects.bulk_create(batch_data, ignore_conflicts=True)
|
||
batch_data = []
|
||
|
||
# 残りのデータを処理
|
||
if batch_data and not dry_run:
|
||
Checkpoint.objects.bulk_create(batch_data, ignore_conflicts=True)
|
||
|
||
self.stdout.write(f'{checkpoints_migrated}件のチェックポイントを移行しました')
|
||
|
||
def migrate_gps_logs(self, conn, dry_run, batch_size):
|
||
"""GPS位置情報を移行"""
|
||
print('GPS位置情報を移行中...')
|
||
|
||
# チームとイベントのマッピングを作成
|
||
team_to_event_map = {}
|
||
for team in Team.objects.select_related('event'):
|
||
if team.event: # eventがNoneでないことを確認
|
||
team_to_event_map[team.zekken_number] = team.event.id
|
||
|
||
# チェックポイントのマッピングを作成
|
||
checkpoint_id_map = {}
|
||
for checkpoint in Checkpoint.objects.select_related('event'):
|
||
if checkpoint.event: # eventがNoneでないことを確認
|
||
key = (checkpoint.event.event_code, checkpoint.cp_number)
|
||
checkpoint_id_map[key] = checkpoint.id
|
||
|
||
with conn.cursor() as cursor:
|
||
cursor.execute("SELECT * FROM gps_information")
|
||
rows = cursor.fetchall()
|
||
|
||
logs_migrated = 0
|
||
batch_data = []
|
||
|
||
for row in rows:
|
||
(serial_number, zekken_number, event_code, cp_number,
|
||
image_address, goal_time, late_point, create_at, create_user,
|
||
update_at, update_user, buy_flag, minus_photo_flag, colabo_company_memo) = row
|
||
|
||
# 対応するチームを取得
|
||
try:
|
||
event = NewEvent2.objects.get(event_code=event_code)
|
||
team = Team.objects.get(zekken_number=zekken_number, event=event)
|
||
# teamが存在し、eventも存在することを確認
|
||
if not team or not team.event:
|
||
continue
|
||
except (NewEvent2.DoesNotExist, Team.DoesNotExist):
|
||
continue
|
||
|
||
# 対応するチェックポイントを取得(存在する場合)
|
||
checkpoint = None
|
||
if cp_number is not None and cp_number != -1:
|
||
try:
|
||
checkpoint = Checkpoint.objects.get(cp_number=cp_number, event=event)
|
||
except Checkpoint.DoesNotExist:
|
||
pass
|
||
|
||
# checkin_timeの設定(必須フィールド)
|
||
checkin_time = timezone.now() # デフォルト値
|
||
|
||
if goal_time:
|
||
try:
|
||
# goal_timeはHH:MM形式と仮定
|
||
from datetime import datetime, time
|
||
parsed_time = datetime.strptime(goal_time, '%H:%M').time()
|
||
if create_at:
|
||
checkin_time = timezone.make_aware(datetime.combine(create_at.date(), parsed_time))
|
||
else:
|
||
checkin_time = timezone.make_aware(datetime.combine(datetime.now().date(), parsed_time))
|
||
except:
|
||
checkin_time = timezone.make_aware(create_at) if create_at else timezone.now()
|
||
elif create_at:
|
||
checkin_time = timezone.make_aware(create_at) if timezone.is_naive(create_at) else create_at
|
||
|
||
if not dry_run:
|
||
# GpsCheckinテーブル用のデータ
|
||
batch_data.append({
|
||
'event_code': event_code,
|
||
'zekken': zekken_number,
|
||
'serial_number': serial_number,
|
||
'cp_number': cp_number or 0,
|
||
'lat': None, # 実際のMobServerデータベースから取得
|
||
'lng': None, # 実際のMobServerデータベースから取得
|
||
'checkin_time': checkin_time,
|
||
'record_time': timezone.make_aware(create_at) if create_at and timezone.is_naive(create_at) else (create_at or timezone.now()),
|
||
'location': "", # PostGISポイントは後で設定
|
||
'mobserver_id': serial_number,
|
||
'event_id': team_to_event_map.get(zekken_number),
|
||
'team_id': team.id,
|
||
'checkpoint_id': checkpoint.id if checkpoint else None
|
||
})
|
||
|
||
logs_migrated += 1
|
||
|
||
# バッチ処理
|
||
if len(batch_data) >= batch_size:
|
||
if not dry_run:
|
||
self.bulk_insert_gps_logs(batch_data)
|
||
batch_data = []
|
||
|
||
# 残りのデータを処理
|
||
if batch_data and not dry_run:
|
||
self.bulk_insert_gps_logs(batch_data)
|
||
|
||
print(f'{logs_migrated}件のGPS位置情報を移行しました')
|
||
|
||
def bulk_insert_gps_logs(self, batch_data):
|
||
"""
|
||
GpsCheckinテーブルに直接SQLを使って挿入
|
||
"""
|
||
if not batch_data:
|
||
return
|
||
|
||
with connection.cursor() as cursor:
|
||
# DjangoのGpsCheckinテーブルに挿入
|
||
insert_sql = """
|
||
INSERT INTO rog_gpscheckin (
|
||
event_code, zekken, serial_number, cp_number, lat, lng,
|
||
checkin_time, record_time, location, mobserver_id,
|
||
event_id, team_id, checkpoint_id
|
||
) VALUES %s
|
||
ON CONFLICT DO NOTHING
|
||
"""
|
||
|
||
# locationフィールドを除外してバリューを準備
|
||
clean_values = []
|
||
for data in batch_data:
|
||
# lat/lngがある場合はPostGISポイントを作成、ない場合はNULL
|
||
if data['lat'] is not None and data['lng'] is not None:
|
||
location_point = f"ST_GeomFromText('POINT({data['lng']} {data['lat']})', 4326)"
|
||
else:
|
||
location_point = None
|
||
|
||
clean_values.append((
|
||
data['event_code'],
|
||
data['zekken'],
|
||
data['serial_number'],
|
||
data['cp_number'],
|
||
data['lat'],
|
||
data['lng'],
|
||
data['checkin_time'],
|
||
data['record_time'],
|
||
location_point,
|
||
data['mobserver_id'],
|
||
data['event_id'],
|
||
data['team_id'],
|
||
data['checkpoint_id']
|
||
))
|
||
|
||
try:
|
||
execute_values(cursor, insert_sql, clean_values, template=None, page_size=100)
|
||
except Exception as e:
|
||
logger.error(f"データ移行エラー: {e}")
|
||
raise
|