199 lines
7.9 KiB
Python
199 lines
7.9 KiB
Python
"""
|
||
简化的数据库和模型对比检查命令
|
||
直接使用 Django 的 inspectdb 逻辑来检查
|
||
"""
|
||
from django.core.management.base import BaseCommand
|
||
from django.db import connection
|
||
from django.apps import apps
|
||
from django.db import models
|
||
from django.core.management.sql import sql_flush
|
||
import sys
|
||
|
||
|
||
class Command(BaseCommand):
|
||
help = '检查数据库结构和模型类是否一致(简化版)'
|
||
|
||
def add_arguments(self, parser):
|
||
parser.add_argument(
|
||
'--app',
|
||
type=str,
|
||
help='指定要检查的应用名称',
|
||
)
|
||
parser.add_argument(
|
||
'--model',
|
||
type=str,
|
||
help='指定要检查的模型名称',
|
||
)
|
||
parser.add_argument(
|
||
'--detail',
|
||
action='store_true',
|
||
help='显示详细信息',
|
||
)
|
||
|
||
def handle(self, *args, **options):
|
||
self.stdout.write(self.style.SUCCESS('开始检查数据库结构和模型类...\n'))
|
||
|
||
app_name = options.get('app')
|
||
model_name = options.get('model')
|
||
detail = options.get('detail', False)
|
||
|
||
# 获取所有应用
|
||
if app_name:
|
||
apps_to_check = [apps.get_app_config(app_name)]
|
||
else:
|
||
apps_to_check = [
|
||
apps.get_app_config('User'),
|
||
apps.get_app_config('finance'),
|
||
apps.get_app_config('business')
|
||
]
|
||
|
||
all_issues = []
|
||
all_warnings = []
|
||
|
||
with connection.cursor() as cursor:
|
||
# 获取数据库名
|
||
cursor.execute("SELECT DATABASE()")
|
||
db_name = cursor.fetchone()[0]
|
||
self.stdout.write(f'数据库: {db_name}\n')
|
||
|
||
for app_config in apps_to_check:
|
||
self.stdout.write(self.style.WARNING(f'\n检查应用: {app_config.name}'))
|
||
self.stdout.write('=' * 80)
|
||
|
||
models_to_check = app_config.get_models()
|
||
if model_name:
|
||
models_to_check = [m for m in models_to_check if m.__name__ == model_name]
|
||
|
||
for model in models_to_check:
|
||
issues, warnings = self._check_model(cursor, db_name, model, detail)
|
||
all_issues.extend(issues)
|
||
all_warnings.extend(warnings)
|
||
|
||
# 输出总结
|
||
self.stdout.write('\n' + '=' * 80)
|
||
self.stdout.write(self.style.SUCCESS('\n检查完成!\n'))
|
||
|
||
if all_issues:
|
||
self.stdout.write(self.style.ERROR(f'发现 {len(all_issues)} 个问题:\n'))
|
||
for i, issue in enumerate(all_issues, 1):
|
||
self.stdout.write(f'{i}. [{issue["type"]}] {issue["message"]}')
|
||
if detail and 'details' in issue:
|
||
for detail_item in issue['details']:
|
||
self.stdout.write(f' - {detail_item}')
|
||
else:
|
||
self.stdout.write(self.style.SUCCESS('✅ 数据库结构和模型类完全一致!'))
|
||
|
||
if all_warnings:
|
||
self.stdout.write(self.style.WARNING(f'\n警告 ({len(all_warnings)} 个):\n'))
|
||
for i, warning in enumerate(all_warnings, 1):
|
||
self.stdout.write(f'{i}. {warning}')
|
||
|
||
return len(all_issues)
|
||
|
||
def _check_model(self, cursor, db_name, model, detail=False):
|
||
"""检查单个模型"""
|
||
issues = []
|
||
warnings = []
|
||
|
||
db_table = model._meta.db_table
|
||
self.stdout.write(f'\n检查模型: {model.__name__} (表: {db_table})')
|
||
self.stdout.write('-' * 80)
|
||
|
||
# 检查表是否存在
|
||
cursor.execute("""
|
||
SELECT COUNT(*)
|
||
FROM information_schema.tables
|
||
WHERE table_schema = %s AND table_name = %s
|
||
""", [db_name, db_table])
|
||
|
||
if cursor.fetchone()[0] == 0:
|
||
issues.append({
|
||
'type': 'missing_table',
|
||
'model': model.__name__,
|
||
'table': db_table,
|
||
'message': f'表 {db_table} 不存在于数据库中'
|
||
})
|
||
self.stdout.write(self.style.ERROR(f' ❌ 表 {db_table} 不存在'))
|
||
return issues, warnings
|
||
|
||
self.stdout.write(self.style.SUCCESS(f' ✅ 表 {db_table} 存在'))
|
||
|
||
# 获取数据库表的字段
|
||
cursor.execute("""
|
||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
||
ORDER BY ORDINAL_POSITION
|
||
""", [db_name, db_table])
|
||
|
||
db_columns = {row[0]: {'type': row[1], 'nullable': row[2] == 'YES', 'default': row[3]}
|
||
for row in cursor.fetchall()}
|
||
|
||
# 获取模型字段(只获取数据库字段,排除反向关系)
|
||
model_fields = {}
|
||
for field in model._meta.get_fields(include_parents=False):
|
||
# 跳过多对多关系
|
||
if isinstance(field, models.ManyToManyField):
|
||
continue
|
||
|
||
# 跳过反向关系(auto_created=True)
|
||
if getattr(field, 'auto_created', False):
|
||
continue
|
||
|
||
# 只处理有 column 属性的字段
|
||
if hasattr(field, 'column'):
|
||
model_fields[field.column] = {
|
||
'name': field.name,
|
||
'type': type(field).__name__,
|
||
'null': getattr(field, 'null', False),
|
||
}
|
||
|
||
# 检查模型字段是否在数据库中存在
|
||
for field_name, field_info in model_fields.items():
|
||
if field_name not in db_columns:
|
||
issues.append({
|
||
'type': 'missing_field',
|
||
'model': model.__name__,
|
||
'table': db_table,
|
||
'field': field_name,
|
||
'message': f'模型字段 {field_info["name"]} ({field_name}) 在数据库表 {db_table} 中不存在'
|
||
})
|
||
self.stdout.write(self.style.ERROR(f' ❌ 字段 {field_info["name"]} ({field_name}) 在数据库中不存在'))
|
||
else:
|
||
if detail:
|
||
self.stdout.write(self.style.SUCCESS(f' ✅ 字段 {field_info["name"]} ({field_name}) 存在'))
|
||
|
||
# 检查数据库中是否有模型中没有的字段(排除 id)
|
||
db_field_names = set(db_columns.keys())
|
||
model_field_names = set(model_fields.keys())
|
||
extra_fields = db_field_names - model_field_names - {'id'}
|
||
|
||
if extra_fields:
|
||
warnings.append(f'数据库表 {db_table} 中有模型中没有的字段: {", ".join(extra_fields)}')
|
||
self.stdout.write(self.style.WARNING(f' ⚠️ 数据库中有额外字段: {", ".join(extra_fields)}'))
|
||
|
||
# 检查多对多关系表
|
||
for field in model._meta.get_fields(include_parents=False):
|
||
if isinstance(field, models.ManyToManyField):
|
||
m2m_table = field.remote_field.through._meta.db_table
|
||
cursor.execute("""
|
||
SELECT COUNT(*)
|
||
FROM information_schema.tables
|
||
WHERE table_schema = %s AND table_name = %s
|
||
""", [db_name, m2m_table])
|
||
|
||
if cursor.fetchone()[0] == 0:
|
||
issues.append({
|
||
'type': 'missing_m2m_table',
|
||
'model': model.__name__,
|
||
'table': m2m_table,
|
||
'field': field.name,
|
||
'message': f'多对多关系表 {m2m_table} 不存在'
|
||
})
|
||
self.stdout.write(self.style.ERROR(f' ❌ 多对多关系表 {m2m_table} 不存在'))
|
||
else:
|
||
if detail:
|
||
self.stdout.write(self.style.SUCCESS(f' ✅ 多对多关系表 {m2m_table} 存在'))
|
||
|
||
return issues, warnings
|