Files
banban/talkingq-url/scripts/cleanup_db.py
2026-03-24 15:04:36 +08:00

386 lines
13 KiB
Python

import os
import sys
import asyncio
from pathlib import Path
import aiomysql
import datetime
import inspect
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import settings
from database.models import Base
from sqlalchemy import inspect as sqlalchemy_inspect
from utils.logger import session_logger
async def get_mysql_connection():
"""建立MySQL数据库连接"""
try:
conn = await aiomysql.connect(
host=settings.db_host,
port=settings.db_port,
user=settings.db_user,
password=settings.db_password,
db=settings.db_name,
autocommit=False
)
return conn
except Exception as e:
print(f"数据库连接失败: {e}")
return None
async def get_all_tables(conn):
"""获取数据库中所有表"""
async with conn.cursor() as cursor:
await cursor.execute("SHOW TABLES")
tables = await cursor.fetchall()
return [table[0] for table in tables]
async def get_model_tables():
"""获取模型定义的所有表"""
return [table.__tablename__ for table in Base.__subclasses__()]
async def get_table_columns(conn, table_name):
"""获取表的所有列"""
async with conn.cursor() as cursor:
await cursor.execute(f"SHOW COLUMNS FROM `{table_name}`")
columns = await cursor.fetchall()
return [column[0] for column in columns]
async def get_model_columns(model_class):
"""获取模型定义的所有列"""
inspector = sqlalchemy_inspect(model_class)
return [column.key for column in inspector.mapper.column_attrs]
async def identify_unused_tables(conn):
"""识别未使用的表"""
db_tables = await get_all_tables(conn)
model_tables = await get_model_tables()
unused_tables = [table for table in db_tables if table not in model_tables]
return unused_tables
async def identify_unused_columns(conn):
"""识别未使用的列"""
results = {}
model_dict = {model.__tablename__: model for model in Base.__subclasses__()}
for table_name, model_class in model_dict.items():
try:
db_columns = await get_table_columns(conn, table_name)
model_columns = await get_model_columns(model_class)
unused_columns = [col for col in db_columns if col not in model_columns]
if unused_columns:
results[table_name] = unused_columns
except Exception as e:
print(f"检查表 {table_name} 列时出错: {e}")
return results
async def get_table_foreign_keys(conn, table_name):
"""获取表的外键约束"""
async with conn.cursor() as cursor:
query = """
SELECT
CONSTRAINT_NAME,
TABLE_NAME,
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
REFERENCED_TABLE_NAME IS NOT NULL
AND TABLE_SCHEMA = %s
AND TABLE_NAME = %s
"""
await cursor.execute(query, (settings.db_name, table_name))
foreign_keys = await cursor.fetchall()
return foreign_keys
async def get_referenced_tables(conn, table_name):
"""获取引用指定表的所有表"""
async with conn.cursor() as cursor:
query = """
SELECT
TABLE_NAME,
CONSTRAINT_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
REFERENCED_TABLE_NAME = %s
AND TABLE_SCHEMA = %s
"""
await cursor.execute(query, (table_name, settings.db_name))
references = await cursor.fetchall()
if references:
print(f"{table_name} 被以下表引用:")
for ref in references:
print(f" - {ref[0]} (约束: {ref[1]})")
return references
async def disable_foreign_key_checks(conn):
"""临时禁用外键约束检查"""
async with conn.cursor() as cursor:
await cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
print("已临时禁用外键约束检查")
async def enable_foreign_key_checks(conn):
"""重新启用外键约束检查"""
async with conn.cursor() as cursor:
await cursor.execute("SET FOREIGN_KEY_CHECKS = 1")
print("已重新启用外键约束检查")
async def sort_tables_for_deletion(conn, tables):
"""对表进行排序,以确保先删除引用者,再删除被引用者"""
dependency_graph = {}
for table in tables:
references = await get_referenced_tables(conn, table)
dependency_graph[table] = [ref[0] for ref in references if ref[0] in tables]
result = []
visited = set()
temp_mark = set()
def visit(node):
if node in temp_mark:
return
if node not in visited:
temp_mark.add(node)
for dependent in dependency_graph.get(node, []):
visit(dependent)
temp_mark.remove(node)
visited.add(node)
result.append(node)
for table in tables:
if table not in visited:
visit(table)
return result[::-1]
async def drop_tables(conn, tables, dry_run=True):
"""删除未使用的表"""
if not tables:
return True
if not dry_run:
try:
print("禁用外键约束检查以确保安全删除所有表...")
await disable_foreign_key_checks(conn)
for table in tables:
try:
async with conn.cursor() as cursor:
await cursor.execute(f"DROP TABLE IF EXISTS `{table}`")
print(f"已删除表: {table}")
except Exception as e:
print(f"删除表 {table} 时出错: {e}")
await conn.rollback()
await enable_foreign_key_checks(conn)
return False
await conn.commit()
await enable_foreign_key_checks(conn)
return True
except Exception as e:
print(f"删除表时出错: {e}")
try:
await enable_foreign_key_checks(conn)
await conn.rollback()
except:
pass
return False
else:
for table in tables:
print(f"将删除表: {table}")
return True
async def drop_columns(conn, column_dict, dry_run=True):
"""删除未使用的列"""
for table, columns in column_dict.items():
try:
for column in columns:
if dry_run:
print(f"将从表 {table} 删除列: {column}")
else:
async with conn.cursor() as cursor:
await cursor.execute(f"ALTER TABLE `{table}` DROP COLUMN `{column}`")
print(f"已从表 {table} 删除列: {column}")
except Exception as e:
print(f"从表 {table} 删除列时出错: {e}")
if not dry_run:
await conn.rollback()
return False
if not dry_run:
await conn.commit()
return True
async def cleanup_database(dry_run=True):
"""清理数据库中未使用的表和列"""
conn = await get_mysql_connection()
if not conn:
return False
try:
unused_tables = await identify_unused_tables(conn)
unused_columns = await identify_unused_columns(conn)
print("\n=== 数据库清理报告 ===")
if unused_tables:
print(f"\n发现 {len(unused_tables)} 个未使用的表:")
for table in unused_tables:
print(f" - {table}")
if not dry_run:
success = await drop_tables(conn, unused_tables, dry_run=False)
if not success:
print("删除表失败,已回滚操作")
return False
else:
print("\n未发现任何未使用的表")
if unused_columns:
total_columns = sum(len(cols) for cols in unused_columns.values())
print(f"\n发现 {total_columns} 个未使用的列:")
for table, columns in unused_columns.items():
for column in columns:
print(f" - {table}.{column}")
if not dry_run:
success = await drop_columns(conn, unused_columns, dry_run=False)
if not success:
print("删除列失败,已回滚操作")
return False
else:
print("\n未发现任何未使用的列")
if dry_run:
print("\n这是一次模拟运行,未执行任何实际更改。要执行更改,请使用 --execute 选项。")
else:
print("\n数据库清理成功完成!")
return True
except Exception as e:
print(f"清理数据库时发生错误: {e}")
return False
finally:
conn.close()
async def remove_orphaned_records(dry_run=True):
"""删除孤立记录(有外键但对应的父记录不存在)"""
conn = await get_mysql_connection()
if not conn:
return False
try:
async with conn.cursor() as cursor:
await cursor.execute("""
SELECT COUNT(*) FROM conversation_messages
WHERE conversation_id NOT IN (SELECT id FROM conversation_histories)
""")
count = (await cursor.fetchone())[0]
if count > 0:
print(f"\n发现 {count} 条孤立的会话消息记录")
if not dry_run:
await cursor.execute("""
DELETE FROM conversation_messages
WHERE conversation_id NOT IN (SELECT id FROM conversation_histories)
""")
print(f"已删除 {count} 条孤立的会话消息记录")
else:
print("\n未发现孤立的会话消息记录")
async with conn.cursor() as cursor:
await cursor.execute("""
SELECT COUNT(*) FROM role_languages
WHERE role_id NOT IN (SELECT id FROM roles)
""")
count = (await cursor.fetchone())[0]
if count > 0:
print(f"\n发现 {count} 条孤立的角色语言记录")
if not dry_run:
await cursor.execute("""
DELETE FROM role_languages
WHERE role_id NOT IN (SELECT id FROM roles)
""")
print(f"已删除 {count} 条孤立的角色语言记录")
else:
print("\n未发现孤立的角色语言记录")
if not dry_run:
await conn.commit()
return True
except Exception as e:
print(f"清理孤立记录时发生错误: {e}")
if not dry_run:
await conn.rollback()
return False
finally:
conn.close()
async def drop_specific_tables(conn, tables_to_drop, dry_run=True):
"""删除指定的表,无论它们是否被使用"""
if dry_run:
print(f"\n将删除以下指定表:")
for table in tables_to_drop:
print(f" - {table}")
return True
try:
await disable_foreign_key_checks(conn)
for table in tables_to_drop:
try:
async with conn.cursor() as cursor:
await cursor.execute(f"DROP TABLE IF EXISTS `{table}`")
print(f"已删除表: {table}")
except Exception as e:
print(f"删除表 {table} 时出错: {e}")
await conn.commit()
print(f"指定表已成功删除")
return True
except Exception as e:
await conn.rollback()
print(f"删除指定表时出错: {e}")
return False
finally:
await enable_foreign_key_checks(conn)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="数据库清理工具")
parser.add_argument('--execute', action='store_true', help='执行实际更改(不使用此选项则只进行模拟运行)')
parser.add_argument('--drop-tables', type=str, help='指定要删除的表,用逗号分隔')
args = parser.parse_args()
dry_run = not args.execute
async def main():
if args.drop_tables:
conn = await get_mysql_connection()
if not conn:
return False
try:
tables_to_drop = [t.strip() for t in args.drop_tables.split(',')]
await drop_specific_tables(conn, tables_to_drop, dry_run)
finally:
conn.close()
else:
result = await cleanup_database(dry_run)
if result:
await remove_orphaned_records(dry_run)
asyncio.run(main())