1.4.1 调整Mysql字段同步机制

This commit is contained in:
t@123654
2025-06-10 16:02:52 +08:00
parent e00f65e72a
commit a55881a73b
7 changed files with 86 additions and 63 deletions
+75 -54
View File
@@ -1,4 +1,4 @@
from sqlalchemy import inspect, create_engine, text
from sqlalchemy import inspect, create_engine, text,String,Text
from sqlalchemy.engine import Engine
from typing import List, Type
from .models.base import Base
@@ -7,6 +7,10 @@ from .models.config_management import ConfigManagement
from .models.feed import Feed
from .models.message_task import MessageTask
from .models.user import User
def printf(*args):
if "SYNC" in str(args):
print(args)
pass
class ModelSync:
"""模型字段同步到数据库的工具类"""
@@ -43,10 +47,10 @@ class ModelSync:
"""同步所有模型到数据库
:param force_update: 是否强制更新字段类型,默认为False
"""
print("[SYNC] 开始同步所有模型到数据库...")
printf("[SYNC] 开始同步所有模型到数据库...")
for model in self.models:
self.sync_model(model, force_update)
print("[SYNC] 所有模型同步完成")
printf("[SYNC] 所有模型同步完成")
def sync_model(self, model: Type[Base], force_update: bool = False) -> None:
"""
@@ -59,11 +63,11 @@ class ModelSync:
table_name = model.__tablename__
db_type = self.get_database_type()
print(f"[SYNC] 开始同步表 {table_name} (数据库类型: {db_type})...")
printf(f"[SYNC] 开始同步表 {table_name} (数据库类型: {db_type})...")
# 检查表是否存在
if not inspector.has_table(table_name):
print(f"[SYNC] 表 {table_name} 不存在,创建新表")
printf(f"[SYNC] 表 {table_name} 不存在,创建新表")
with self.engine.begin() as conn:
# 自定义表创建逻辑以支持主外键约束
table = model.__table__
@@ -90,7 +94,7 @@ class ModelSync:
elif db_type == 'postgresql':
# PostgreSQL使用SERIAL会自动创建序列
column_def += " SERIAL"
print(f"[DEBUG] 为主键列 {column.name} 设置自动增长")
printf(f"[DEBUG] 为主键列 {column.name} 设置自动增长")
if default_value is not None:
column_def += f" DEFAULT '{default_value}'" if isinstance(default_value, str) else f" DEFAULT {default_value}"
@@ -125,10 +129,10 @@ class ModelSync:
create_sql += ", " + ", ".join(foreign_keys)
create_sql += ")"
print(f"[DEBUG] 执行CREATE TABLE语句: {create_sql}")
printf(f"[SQL] 执行表创建SQL:\n{create_sql}")
conn.execute(text(create_sql))
print(f"[SYNC] 表 {table_name} 创建成功,包含 {len(primary_keys)} 个主键和 {len(foreign_keys)} 个外键")
printf(f"[SYNC] 表 {table_name} 创建成功,包含 {len(primary_keys)} 个主键和 {len(foreign_keys)} 个外键")
return
# 获取数据库表列和模型列
@@ -139,9 +143,9 @@ class ModelSync:
for col_name, model_col in model_columns.items():
if col_name not in db_columns:
# 添加新列
print(f"[SYNC] 检测到新列 {col_name}({model_col.type})")
printf(f"[SYNC] 检测到新列 {col_name}({model_col.type})")
self._add_column(table_name, model_col)
print(f"[SYNC] 表 {table_name} 添加列 {col_name} 成功")
printf(f"[SYNC] 表 {table_name} 添加列 {col_name} 成功")
else:
# 检查类型是否一致
db_col = db_columns[col_name]
@@ -161,28 +165,28 @@ class ModelSync:
# 记录差异详情
if db_default != model_default:
print(f"[SYNC] 检测到列 {col_name} 默认值差异: 数据库({db_default}) vs 模型({model_default})")
printf(f"[SYNC] 检测到列 {col_name} 默认值差异: 数据库({db_default}) vs 模型({model_default})")
if db_col.get('primary_key', False) != model_col.primary_key:
print(f"[SYNC] 检测到列 {col_name} 主键差异: 数据库({db_col.get('primary_key', False)}) vs 模型({model_col.primary_key})")
printf(f"[SYNC] 检测到列 {col_name} 主键差异: 数据库({db_col.get('primary_key', False)}) vs 模型({model_col.primary_key})")
if ('foreign_keys' in db_col) != (hasattr(model_col, 'foreign_keys') and len(model_col.foreign_keys) > 0):
print(f"[SYNC] 检测到列 {col_name} 外键标记差异")
printf(f"[SYNC] 检测到列 {col_name} 外键标记差异")
print(f"[SYNC] 检测到列 {col_name} 类型差异: 数据库({db_type_str}) vs 模型({model_type_str})")
printf(f"[SYNC] 检测到列 {col_name} 类型差异: 数据库({db_type_str}) vs 模型({model_type_str})")
self._alter_column(table_name, model_col)
print(f"[SYNC] 表 {table_name} 更新列 {col_name} 成功")
printf(f"[SYNC] 表 {table_name} 更新列 {col_name} 成功")
else:
print(f"[SYNC] 表 {table_name}{col_name} 类型、默认值和约束一致({db_type_str})")
printf(f"[SYNC] 表 {table_name}{col_name} 类型、默认值和约束一致({db_type_str})")
print(f"[SYNC] 表 {table_name} 同步完成")
printf(f"[SYNC] 表 {table_name} 同步完成")
except Exception as e:
print(f"[ERROR] 同步表 {table_name} 失败: {str(e)}")
printf(f"[ERROR] 同步表 {table_name} 失败: {str(e)}")
raise
def get_database_type(self) -> str:
"""获取数据库类型,支持多种常见数据库"""
try:
dialect = self.engine.dialect.name
print(f"[DEBUG] 检测到数据库方言: {dialect}")
printf(f"[DEBUG] 检测到数据库方言: {dialect}")
# 支持更多数据库类型
if dialect == 'sqlite':
@@ -196,10 +200,10 @@ class ModelSync:
elif dialect == 'mssql':
return 'mssql'
else:
print(f"[WARN] 未知数据库方言: {dialect}, 默认使用mysql兼容模式")
printf(f"[WARN] 未知数据库方言: {dialect}, 默认使用mysql兼容模式")
return 'mysql'
except Exception as e:
print(f"[ERROR] 获取数据库类型失败: {str(e)}")
printf(f"[ERROR] 获取数据库类型失败: {str(e)}")
return 'mysql' # 默认回退
def _add_column(self, table_name: str, column) -> None:
@@ -230,7 +234,7 @@ class ModelSync:
add_sql += " AUTOINCREMENT"
elif db_type == 'postgresql':
add_sql += " SERIAL"
print(f"[DEBUG] 为主键列 {column.name} 添加自动增长属性")
printf(f"[DEBUG] 为主键列 {column.name} 添加自动增长属性")
# 添加默认值设置
if default_value is not None:
@@ -242,7 +246,7 @@ class ModelSync:
elif column.nullable:
add_sql += " DEFAULT NULL"
print(f"[DEBUG] 执行ADD COLUMN语句: {add_sql}")
printf(f"[SQL] 执行添加列SQL:\n{add_sql}")
conn.execute(text(add_sql))
# 处理外键约束
@@ -254,16 +258,16 @@ class ModelSync:
f"ALTER TABLE {table_name} ADD CONSTRAINT fk_{table_name}_{column.name} " +
f"FOREIGN KEY ({column.name}) REFERENCES {ref_table}({ref_column})"
)
print(f"[DEBUG] 执行外键约束语句: {fk_sql}")
printf(f"[DEBUG] 执行外键约束语句: {fk_sql}")
conn.execute(text(fk_sql))
except Exception as e:
print(f"[WARN] 添加列 {column.name} 失败: {str(e)}")
printf(f"[WARN] 添加列 {column.name} 失败: {str(e)}")
raise
def _alter_column_sqlite(self, table_name: str, column) -> None:
"""SQLite专用: 通过创建新表并复制数据来修改列,支持默认值"""
try:
print(f"[INFO] 开始修改SQLite表 {table_name} 的列 {column.name}")
printf(f"[INFO] 开始修改SQLite表 {table_name} 的列 {column.name}")
with self.engine.begin() as conn:
# 1. 获取原表结构
@@ -300,19 +304,24 @@ class ModelSync:
column_defs.append(col_def)
create_sql = f"CREATE TABLE {temp_table} ({', '.join(column_defs)})"
print(f"[DEBUG] SQLite创建表语句: {create_sql}")
printf(f"[SQL] 执行SQLite表重建SQL:\n{create_sql}")
conn.execute(text(create_sql))
# 3. 复制数据
conn.execute(text(f"INSERT INTO {temp_table} SELECT * FROM {table_name}"))
copy_sql = f"INSERT INTO {temp_table} SELECT * FROM {table_name}"
printf(f"[SQL] 执行数据复制SQL:\n{copy_sql}")
conn.execute(text(copy_sql))
# 4. 删除原表并重命名临时表
conn.execute(text(f"DROP TABLE {table_name}"))
conn.execute(text(f"ALTER TABLE {temp_table} RENAME TO {table_name}"))
drop_sql = f"DROP TABLE {table_name}"
rename_sql = f"ALTER TABLE {temp_table} RENAME TO {table_name}"
printf(f"[SQL] 执行表清理SQL:\n{drop_sql}\n{rename_sql}")
conn.execute(text(drop_sql))
conn.execute(text(rename_sql))
print(f"[INFO] 成功修改SQLite表 {table_name} 的列 {column.name}")
printf(f"[INFO] 成功修改SQLite表 {table_name} 的列 {column.name}")
except Exception as e:
print(f"[ERROR] 修改SQLite列 {column.name} 失败: {str(e)}")
printf(f"[ERROR] 修改SQLite列 {column.name} 失败: {str(e)}")
raise
def _alter_column(self, table_name: str, column) -> None:
@@ -339,15 +348,19 @@ class ModelSync:
# 1. 修改列定义
modify_sql = f"ALTER TABLE {table_name} MODIFY COLUMN {column.name} {column_type} {nullable}"
# 添加自动增长属性(仅对主键列)
if getattr(column, 'autoincrement', False) and getattr(column, 'primary_key', False):
# 添加自动增长属性(仅对整数类型的主键列)
if (getattr(column, 'autoincrement', False) and
getattr(column, 'primary_key', False) and
str(column.type).upper() in ('INT', 'INTEGER', 'BIGINT', 'SMALLINT')):
if db_type == 'mysql':
modify_sql += " AUTO_INCREMENT"
elif db_type == 'sqlite':
modify_sql += " AUTOINCREMENT"
elif db_type == 'postgresql':
modify_sql += " SERIAL"
print(f"[DEBUG] 修改主键列 {column.name} 的自动增长属性")
printf(f"[DEBUG] 修改整数主键列 {column.name} 的自动增长属性")
elif getattr(column, 'autoincrement', False) and getattr(column, 'primary_key', False):
printf(f"[WARN] 列 {column.name} 是主键但类型不是整数({str(column.type)}),跳过AUTO_INCREMENT设置")
# 添加默认值设置
if default_value is not None:
@@ -382,10 +395,10 @@ class ModelSync:
# 执行所有ALTER语句
for sql in alter_sqls:
print(f"[DEBUG] 执行ALTER语句: {sql}")
printf(f"[SQL] 执行修改表SQL:\n{sql}")
conn.execute(text(sql))
except Exception as e:
print(f"[WARN] 修改列 {column.name} 失败: {str(e)}")
printf(f"[WARN] 修改列 {column.name} 失败: {str(e)}")
raise
def _is_same_type(self, db_col, model_col) -> bool:
@@ -412,7 +425,7 @@ class ModelSync:
'INT': ['INT', 'INTEGER'],
'BIGINT': ['BIGINT'],
'VARCHAR': ['VARCHAR', 'CHAR', 'NVARCHAR'],
'TEXT': ['TEXT', 'LONGTEXT', 'MEDIUMTEXT', 'TINYTEXT'],
'TEXT': ['TEXT', 'LONGTEXT', 'MEDIUMTEXT', 'TINYTEXT', 'MEDIUMTEXT'],
'DATETIME': ['DATETIME', 'TIMESTAMP'],
'FLOAT': ['FLOAT', 'REAL'],
'DOUBLE': ['DOUBLE', 'DOUBLE PRECISION'],
@@ -426,17 +439,17 @@ class ModelSync:
# SQLite类型系统比较灵活,检查是否属于同一类型组
for group, types in type_mapping['sqlite'].items():
if db_base_type in types and model_base_type in types:
print(f"[DEBUG] 类型匹配: {db_type}{model_type} 都属于 {group}")
printf(f"[DEBUG] 类型匹配: {db_type}{model_type} 都属于 {group}")
type_match = True
break
if not type_match:
print(f"[DEBUG] 类型不匹配: {db_type}{model_type} 不属于同一组")
printf(f"[DEBUG] 类型不匹配: {db_type}{model_type} 不属于同一组")
return False
else:
# MySQL需要更精确的类型匹配
# 忽略长度差异
if db_base_type != model_base_type:
print(f"[DEBUG] 基础类型不匹配: {db_base_type} != {model_base_type}")
printf(f"[DEBUG] 基础类型不匹配: {db_base_type} != {model_base_type}")
return False
# 检查长度是否一致(如果有)
@@ -444,7 +457,7 @@ class ModelSync:
model_length = model_type.split('(')[1].split(')')[0] if '(' in model_type else None
if db_length and model_length and db_length != model_length:
print(f"[DEBUG] 类型长度不匹配: {db_length} != {model_length}")
printf(f"[DEBUG] 类型长度不匹配: {db_length} != {model_length}")
return False
# 2. 检查默认值是否匹配
@@ -457,14 +470,14 @@ class ModelSync:
# 比较默认值
if db_default != model_default:
print(f"[DEBUG] 默认值不匹配: 数据库({db_default}) != 模型({model_default})")
printf(f"[DEBUG] 默认值不匹配: 数据库({db_default}) != 模型({model_default})")
return False
# 3. 检查主键约束是否匹配
db_primary = db_col.get('primary_key', False)
model_primary = model_col.primary_key
if db_primary != model_primary:
print(f"[DEBUG] 主键约束不匹配: 数据库({db_primary}) != 模型({model_primary})")
printf(f"[DEBUG] 主键约束不匹配: 数据库({db_primary}) != 模型({model_primary})")
return False
# 4. 检查外键约束是否匹配
@@ -472,7 +485,7 @@ class ModelSync:
db_foreign = 'foreign_keys' in db_col
model_foreign = hasattr(model_col, 'foreign_keys') and len(model_col.foreign_keys) > 0
if db_foreign != model_foreign:
print(f"[DEBUG] 外键标记不匹配: 数据库({db_foreign}) != 模型({model_foreign})")
printf(f"[DEBUG] 外键标记不匹配: 数据库({db_foreign}) != 模型({model_foreign})")
return False
# 5. 检查自动增长属性是否匹配
@@ -481,12 +494,12 @@ class ModelSync:
# 只有当列是主键时才比较自动增长属性
if db_col.get('primary_key', False) or getattr(model_col, 'primary_key', False):
if db_autoinc != model_autoinc:
print(f"[DEBUG] 自动增长属性不匹配: 数据库({db_autoinc}) != 模型({model_autoinc})")
printf(f"[DEBUG] 自动增长属性不匹配: 数据库({db_autoinc}) != 模型({model_autoinc})")
return False
return True
except Exception as e:
print(f"[ERROR] 类型/默认值/约束比较失败: {str(e)}")
printf(f"[ERROR] 类型/默认值/约束比较失败: {str(e)}")
return False
def _get_column_type(self, column) -> str:
@@ -495,11 +508,11 @@ class ModelSync:
type_str = str(column.type)
# 调试日志
print(f"[DEBUG] 获取列类型: {column.name} - 原始类型: {type_str}")
printf(f"[DEBUG] 获取列类型: {column.name} - 原始类型: {type_str}")
if hasattr(column, 'autoincrement'):
print(f"[DEBUG] 自动增长属性: {column.autoincrement}")
printf(f"[DEBUG] 自动增长属性: {column.autoincrement}")
if hasattr(column, 'primary_key'):
print(f"[DEBUG] 主键属性: {column.primary_key}")
printf(f"[DEBUG] 主键属性: {column.primary_key}")
# 处理常见的类型差异
if db_type == 'sqlite':
@@ -508,10 +521,18 @@ class ModelSync:
return 'TEXT'
elif 'DATETIME' in type_str:
return 'TEXT' # SQLite没有专门的DATETIME类型
else:
# MySQL类型处理
if 'TEXT' in type_str and 'VARCHAR' not in type_str:
return type_str.replace('TEXT', 'LONGTEXT')
elif db_type in ('mysql', 'mariadb'):
# MySQL/MariaDB类型处理
if 'MEDIUMTEXT' in type_str:
return 'MEDIUMTEXT CHARACTER SET utf8mb4'
elif 'TEXT' in type_str and 'VARCHAR' not in type_str:
return type_str.replace('TEXT', 'LONGTEXT') + ' CHARACTER SET utf8mb4'
elif 'VARCHAR' in type_str:
# 对于主键或索引列,限制VARCHAR长度为191以避免超出MySQL的key长度限制
if getattr(column, 'primary_key', False) or getattr(column, 'index', False):
if 'VARCHAR(255)' in type_str:
return 'VARCHAR(191) CHARACTER SET utf8mb4'
return type_str + ' CHARACTER SET utf8mb4'
return type_str
+2 -2
View File
@@ -1,4 +1,4 @@
from .base import Base,Column,String,Integer,DateTime,Text
from .base import Base,Column,String,Integer,DateTime,Text,MEDIUMTEXT
class Article(Base):
__tablename__ = 'articles'
id = Column(String(255), primary_key=True)
@@ -6,7 +6,7 @@ class Article(Base):
title = Column(String(500))
pic_url = Column(String(500))
url=Column(String(500))
content = Column(Text)
content = Column(MEDIUMTEXT)
description=Column(String(800))
status = Column(Integer,default=1)
publish_time = Column(Integer)
+1
View File
@@ -1,6 +1,7 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine, Column, Integer, String, DateTime,Date,ForeignKey,Boolean,Text,Enum,Table
from sqlalchemy import inspect
from sqlalchemy.dialects.mysql import MEDIUMTEXT
from sqlalchemy.exc import SQLAlchemyError
+2 -2
View File
@@ -28,6 +28,6 @@ class MessageTask(Base):
# 定义任务状态字段,默认值为 pending
status = Column(Integer, default=0)
# 定义创建时间字段,默认值为当前 UTC 时间
created_at = Column(DateTime, default=datetime.utcnow)
created_at = Column(DateTime, onupdate=datetime.utcnow)
# 定义更新时间字段,默认值为当前 UTC 时间,更新时自动更新为当前时间
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_at = Column(DateTime,onupdate=datetime.utcnow)
+3 -3
View File
@@ -1,4 +1,4 @@
from .base import Base, Column, String, Integer, DateTime, Boolean
from .base import Base, Column, String, Integer, DateTime, Boolean,Text
from sqlalchemy import JSON
class User(Base):
@@ -7,8 +7,8 @@ class User(Base):
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True)
role = Column(String(20), default='user') # admin/editor/user
permissions = Column(JSON, default=list) # 权限列表
role = Column(String(20)) # admin/editor/user
permissions = Column(Text) # 权限列表
nickname = Column(String(50), default='') # 昵称
avatar = Column(String(255), default='/static/default-avatar.png') # 头像
email = Column(String(50), default='')
+2 -1
View File
@@ -26,7 +26,8 @@ def sync_models():
# 同步模型到表结构
from core.data_sync import ModelSync
DB.create_tables()
ModelSync(eng=DB.get_engine())
sync=ModelSync(eng=DB.get_engine())
sync.sync_all()
print_info("模型同步完成")
+1 -1
View File
@@ -1 +1 @@
python3 main.py -job true -init true
python3 main.py -job True -init True