mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
refactor(database): 重构数据库配置和CRUD逻辑
移除SQLITE_DB_NAME配置,统一使用DATABASE_NAME作为数据库名称 重构CRUD基类,增加out_schema参数用于序列化输出 优化数据库表查询逻辑,支持多种数据库类型 修复序列化工具类的方法签名和实现
This commit is contained in:
@@ -39,6 +39,3 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
|
||||
async def delete_crud(self, ids: List[int]) -> None:
|
||||
"""批量删除"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
|
||||
mcp_crud: McpCRUD = McpCRUD(auth=AuthSchema())
|
||||
@@ -148,8 +148,8 @@ async def get_gen_db_table_list_controller(
|
||||
search: GenTableQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["generator:dblist:query"]))
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await GenTableService.get_gen_db_table_list_service(auth=auth, query_object=search, is_page=False)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list["items"], page_no=page.page_no, page_size=page.page_size)
|
||||
result_dict_list = await GenTableService.get_gen_db_table_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
logger.info('获取数据库表列表成功')
|
||||
return SuccessResponse(data=result_dict, msg="获取数据库表列表成功")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from datetime import datetime, time
|
||||
import json
|
||||
from sqlalchemy.engine.row import Row
|
||||
from sqlalchemy import delete, func, select, text, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -143,75 +144,125 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateS
|
||||
"has_next": False
|
||||
}
|
||||
|
||||
async def get_gen_db_table_list(self, db: AsyncSession, query_object: GenTableQueryParam, is_page: bool = False):
|
||||
async def get_gen_db_table_list(self, search: GenTableQueryParam, order_by: Optional[List[Dict[str, str]]] = None) -> list[Any]:
|
||||
"""
|
||||
根据查询参数获取数据库列表信息
|
||||
|
||||
:param db: orm对象
|
||||
:param query_object: 查询参数对象
|
||||
:param is_page: 是否开启分页
|
||||
:param search: 查询参数对象
|
||||
:param order_by: 排序字段
|
||||
:return: 数据库列表信息对象
|
||||
"""
|
||||
query_sql = """
|
||||
SELECT table_name as table_name,
|
||||
table_comment as table_comment,
|
||||
create_time as create_time,
|
||||
update_time as update_time
|
||||
from
|
||||
information_schema.tables
|
||||
where
|
||||
table_schema = (select database())
|
||||
and table_name not like 'apscheduler\_%'
|
||||
and table_name not like 'gen\_%'
|
||||
and table_name not in (select table_name from gen_table)
|
||||
"""
|
||||
# 根据param.py中的定义,table_name是元组形式("like", value)
|
||||
if getattr(query_object, 'table_name', None) and query_object.table_name[1]:
|
||||
query_sql += """and lower(table_name) like lower(concat('%', :table_name, '%'))"""
|
||||
# pg数据库
|
||||
# {
|
||||
# "table_catalog": "fastapiadmin",
|
||||
# "table_schema": "pg_catalog",
|
||||
# "table_name": "pg_foreign_table",
|
||||
# "table_type": "BASE TABLE",
|
||||
# "self_referencing_column_name": null,
|
||||
# "reference_generation": null,
|
||||
# "user_defined_type_catalog": null,
|
||||
# "user_defined_type_schema": null,
|
||||
# "user_defined_type_name": null,
|
||||
# "is_insertable_into": "YES",
|
||||
# "is_typed": "NO",
|
||||
# "commit_action": null
|
||||
# }
|
||||
|
||||
# mysql
|
||||
# {
|
||||
# "TABLE_CATALOG": "def",
|
||||
# "TABLE_SCHEMA": "fastapiadmin",
|
||||
# "TABLE_NAME": "ai_mcp",
|
||||
# "TABLE_TYPE": "BASE TABLE",
|
||||
# "ENGINE": "InnoDB",
|
||||
# "VERSION": 10,
|
||||
# "ROW_FORMAT": "Dynamic",
|
||||
# "TABLE_ROWS": 0,
|
||||
# "AVG_ROW_LENGTH": 0,
|
||||
# "DATA_LENGTH": 16384,
|
||||
# "MAX_DATA_LENGTH": 0,
|
||||
# "INDEX_LENGTH": 16384,
|
||||
# "DATA_FREE": 0,
|
||||
# "AUTO_INCREMENT": null,
|
||||
# "CREATE_TIME": "2025-10-02T03:43:02",
|
||||
# "UPDATE_TIME": null,
|
||||
# "CHECK_TIME": null,
|
||||
# "TABLE_COLLATION": "utf8mb4_0900_ai_ci",
|
||||
# "CHECKSUM": null,
|
||||
# "CREATE_OPTIONS": "",
|
||||
# "TABLE_COMMENT": "MCP 服务器表"
|
||||
# },
|
||||
|
||||
# sqlite
|
||||
# {
|
||||
# "type": "table",
|
||||
# "name": "system_users",
|
||||
# "tbl_name": "system_users",
|
||||
# "rootpage": 47,
|
||||
# "sql": "CREATE TABLE system_users (\n\tusername VARCHAR(32) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tname VARCHAR(32) NOT NULL, \n\tstatus BOOLEAN NOT NULL, \n\tmobile VARCHAR(20), \n\temail VARCHAR(64), \n\tgender VARCHAR(1), \n\tavatar VARCHAR(500), \n\tis_superuser BOOLEAN NOT NULL, \n\tlast_login DATETIME, \n\tdept_id INTEGER, \n\tcreator_id INTEGER, \n\tid INTEGER NOT NULL, \n\tdescription TEXT, \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tPRIMARY KEY (id), \n\tUNIQUE (username), \n\tUNIQUE (mobile), \n\tUNIQUE (email), \n\tFOREIGN KEY(dept_id) REFERENCES system_dept (id) ON DELETE SET NULL ON UPDATE CASCADE\n)"
|
||||
# },
|
||||
|
||||
# 使用更健壮的方式检测数据库方言
|
||||
if settings.DATABASE_TYPE == 'postgresql':
|
||||
query_sql = """
|
||||
SELECT
|
||||
table_catalog as database_name,
|
||||
table_name as table_name,
|
||||
table_type as table_type,
|
||||
table_schema as table_comment
|
||||
from
|
||||
information_schema.tables
|
||||
where
|
||||
table_catalog = (select current_database())
|
||||
and is_insertable_into = 'YES'
|
||||
and table_schema = 'public'
|
||||
"""
|
||||
elif settings.DATABASE_TYPE == 'mysql':
|
||||
query_sql = """
|
||||
SELECT
|
||||
table_schema as database_name,
|
||||
table_name as table_name,
|
||||
table_type as table_type,
|
||||
table_comment as table_comment
|
||||
from
|
||||
information_schema.tables
|
||||
where
|
||||
table_schema = (select database())
|
||||
"""
|
||||
else:
|
||||
query_sql = f"""
|
||||
SELECT
|
||||
'{settings.DATABASE_NAME}' as database_name,
|
||||
name as table_name,
|
||||
type as table_type,
|
||||
tbl_name as table_comment
|
||||
from
|
||||
sqlite_master
|
||||
where
|
||||
type = 'table'
|
||||
"""
|
||||
|
||||
# 处理table_comment字段(如果有)
|
||||
if getattr(query_object, 'table_comment', None):
|
||||
query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))"""
|
||||
|
||||
# 构建查询参数
|
||||
query_params = {}
|
||||
|
||||
# 添加table_name查询参数
|
||||
if getattr(query_object, 'table_name', None) and query_object.table_name[1]:
|
||||
query_params['table_name'] = query_object.table_name[1]
|
||||
|
||||
# 添加table_comment查询参数
|
||||
if getattr(query_object, 'table_comment', None):
|
||||
query_params['table_comment'] = query_object.table_comment
|
||||
|
||||
query_sql += """order by create_time desc"""
|
||||
query = text(query_sql).bindparams(**query_params)
|
||||
# 直接执行文本SQL查询,避免SQLAlchemy自动添加额外的SELECT )
|
||||
query = text(query_sql).bindparams()
|
||||
|
||||
# 执行查询
|
||||
result = await db.execute(query)
|
||||
all_data = list(result.fetchall())
|
||||
|
||||
# 使用PaginationService.paginate进行分页
|
||||
# 注意:这里假设query_object有page_no和page_size属性,如果没有需要从其他地方获取
|
||||
page_no = getattr(query_object, 'page_no', None)
|
||||
page_size = getattr(query_object, 'page_size', None)
|
||||
if is_page and page_no is not None and page_size is not None:
|
||||
paginated_result = await PaginationService.paginate(
|
||||
data_list=all_data,
|
||||
page_no=page_no,
|
||||
page_size=page_size
|
||||
)
|
||||
return paginated_result
|
||||
else:
|
||||
return {
|
||||
"items": all_data,
|
||||
"total": len(all_data),
|
||||
"page_no": None,
|
||||
"page_size": None,
|
||||
"has_next": False
|
||||
}
|
||||
result = await self.db.execute(query)
|
||||
all_data = result.fetchall()
|
||||
|
||||
async def get_gen_db_table_list_by_names(self, db: AsyncSession, table_names: List[str]):
|
||||
# 将Row对象转换为字典列表,解决JSON序列化问题
|
||||
dict_data = []
|
||||
for row in all_data:
|
||||
# 检查row是否为Row对象
|
||||
if isinstance(row, Row):
|
||||
# 使用._mapping获取字典
|
||||
dict_row = dict(row._mapping)
|
||||
dict_data.append(dict_row)
|
||||
else:
|
||||
dict_data.append(row)
|
||||
return dict_data
|
||||
|
||||
async def get_gen_db_table_list_by_names(self, table_names: List[str]):
|
||||
"""
|
||||
根据业务表名称组获取数据库列表信息
|
||||
|
||||
@@ -219,20 +270,51 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableCreateSchema, GenTableUpdateS
|
||||
:param table_names: 业务表名称组
|
||||
:return: 数据库列表信息对象
|
||||
"""
|
||||
query_sql = """
|
||||
select
|
||||
table_name as table_name,
|
||||
table_comment as table_comment,
|
||||
create_time as create_time,
|
||||
update_time as update_time
|
||||
from
|
||||
information_schema.tables
|
||||
where
|
||||
table_schema = (select database())
|
||||
and table_name in :table_names
|
||||
"""
|
||||
# 使用更健壮的方式检测数据库方言
|
||||
if settings.DATABASE_TYPE == 'postgresql':
|
||||
query_sql = """
|
||||
SELECT
|
||||
table_catalog as database_name,
|
||||
table_name as table_name,
|
||||
table_type as table_type,
|
||||
table_schema as table_comment
|
||||
from
|
||||
information_schema.tables
|
||||
where
|
||||
table_catalog = (select current_database())
|
||||
and is_insertable_into = 'YES'
|
||||
and table_schema = 'public'
|
||||
and table_name in :table_names
|
||||
"""
|
||||
elif settings.DATABASE_TYPE == 'mysql':
|
||||
query_sql = """
|
||||
SELECT
|
||||
table_schema as database_name,
|
||||
table_name as table_name,
|
||||
table_type as table_type,
|
||||
table_comment as table_comment
|
||||
from
|
||||
information_schema.tables
|
||||
where
|
||||
table_schema = (select database())
|
||||
and table_name in :table_names
|
||||
"""
|
||||
else:
|
||||
query_sql = f"""
|
||||
SELECT
|
||||
'{settings.DATABASE_NAME}' as database_name,
|
||||
name as table_name,
|
||||
type as table_type,
|
||||
tbl_name as table_comment
|
||||
from
|
||||
sqlite_master
|
||||
where
|
||||
type = 'table'
|
||||
and table_name in :table_names
|
||||
"""
|
||||
|
||||
query = text(query_sql).bindparams(table_names=tuple(table_names))
|
||||
gen_db_table_list = (await db.execute(query)).fetchall()
|
||||
gen_db_table_list = (await self.db.execute(query)).fetchall()
|
||||
|
||||
return gen_db_table_list
|
||||
|
||||
@@ -276,33 +358,60 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnCreateSchem
|
||||
:param table_name: 业务表名称
|
||||
:return: 业务表字段列表信息对象
|
||||
"""
|
||||
query_sql = """
|
||||
select
|
||||
column_name as column_name,
|
||||
case
|
||||
when is_nullable = 'no' and column_key != 'PRI' then '1'
|
||||
else '0'
|
||||
end as is_required,
|
||||
case
|
||||
when column_key = 'PRI' then '1'
|
||||
else '0'
|
||||
end as is_pk,
|
||||
ordinal_position as sort,
|
||||
column_comment as column_comment,
|
||||
case
|
||||
when extra = 'auto_increment' then '1'
|
||||
else '0'
|
||||
end as is_increment,
|
||||
column_type as column_type
|
||||
from
|
||||
information_schema.columns
|
||||
where
|
||||
table_schema = (select database())
|
||||
and table_name = :table_name
|
||||
order by
|
||||
ordinal_position
|
||||
"""
|
||||
query = text(query_sql).bindparams(table_name=table_name)
|
||||
gen_db_table_columns = (await db.execute(query)).fetchall()
|
||||
# 兼容SQLite和MySQL/PostgreSQL
|
||||
if str(db.bind.dialect) == 'sqlite':
|
||||
query_sql = """
|
||||
pragma table_info(:table_name)
|
||||
"""
|
||||
query = text(query_sql).bindparams(table_name=table_name)
|
||||
gen_db_table_columns_raw = (await db.execute(query)).fetchall()
|
||||
|
||||
# 转换SQLite的pragma结果为与information_schema.columns兼容的格式
|
||||
gen_db_table_columns = []
|
||||
for col in gen_db_table_columns_raw:
|
||||
# col格式: (cid, name, type, notnull, dflt_value, pk)
|
||||
is_required = '1' if col[3] == 1 and col[5] == 0 else '0'
|
||||
is_pk = '1' if col[5] == 1 else '0'
|
||||
is_increment = '0' # SQLite没有auto_increment标记,需要额外判断
|
||||
|
||||
# 构建兼容的结果行
|
||||
gen_db_table_columns.append({
|
||||
'column_name': col[1],
|
||||
'is_required': is_required,
|
||||
'is_pk': is_pk,
|
||||
'sort': col[0], # 使用cid作为排序
|
||||
'column_comment': '', # SQLite不存储列注释
|
||||
'is_increment': is_increment,
|
||||
'column_type': col[2]
|
||||
})
|
||||
else:
|
||||
query_sql = """
|
||||
select
|
||||
column_name as column_name,
|
||||
case
|
||||
when is_nullable = 'no' and column_key != 'PRI' then '1'
|
||||
else '0'
|
||||
end as is_required,
|
||||
case
|
||||
when column_key = 'PRI' then '1'
|
||||
else '0'
|
||||
end as is_pk,
|
||||
ordinal_position as sort,
|
||||
column_comment as column_comment,
|
||||
case
|
||||
when extra = 'auto_increment' then '1'
|
||||
else '0'
|
||||
end as is_increment,
|
||||
column_type as column_type
|
||||
from
|
||||
information_schema.columns
|
||||
where
|
||||
table_schema = (select database())
|
||||
and table_name = :table_name
|
||||
order by
|
||||
ordinal_position
|
||||
"""
|
||||
query = text(query_sql).bindparams(table_name=table_name)
|
||||
gen_db_table_columns = (await db.execute(query)).fetchall()
|
||||
|
||||
return gen_db_table_columns
|
||||
@@ -50,35 +50,20 @@ class GenTableService:
|
||||
if not isinstance(db, AsyncSession):
|
||||
raise CustomException(msg='数据库连接类型不正确')
|
||||
gen_table_dao = GenTableCRUD(auth=auth)
|
||||
gen_table_list_result = await gen_table_dao.get_gen_table_list(db, query_object, is_page)
|
||||
gen_table_list_result = await gen_table_dao.get_gen_table_list(query_object, is_page)
|
||||
return gen_table_list_result
|
||||
|
||||
@classmethod
|
||||
async def get_gen_db_table_list_service(
|
||||
cls, auth: AuthSchema, query_object: GenTableQueryParam, is_page: bool = False
|
||||
) -> Dict:
|
||||
async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam, order_by: Optional[List[Dict[str, str]]] = None) -> list[Any]:
|
||||
"""获取数据库列表信息"""
|
||||
if not auth.db:
|
||||
raise CustomException(msg='数据库连接不存在')
|
||||
# 确保db是AsyncSession类型
|
||||
db = auth.db
|
||||
if not isinstance(db, AsyncSession):
|
||||
raise CustomException(msg='数据库连接类型不正确')
|
||||
gen_table_dao = GenTableCRUD(auth=auth)
|
||||
gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list(db, query_object, is_page)
|
||||
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_gen_db_table_list(search, order_by)
|
||||
return gen_db_table_list_result
|
||||
|
||||
@classmethod
|
||||
async def get_gen_db_table_list_by_name_service(cls, auth: AuthSchema, table_names: List[str]) -> List[GenTableOutSchema]:
|
||||
"""根据表名称组获取数据库列表信息"""
|
||||
if not auth.db:
|
||||
raise CustomException(msg='数据库连接不存在')
|
||||
# 确保db是AsyncSession类型
|
||||
db = auth.db
|
||||
if not isinstance(db, AsyncSession):
|
||||
raise CustomException(msg='数据库连接类型不正确')
|
||||
gen_table_dao = GenTableCRUD(auth=auth)
|
||||
gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list_by_names(db, table_names)
|
||||
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_gen_db_table_list_by_names(table_names)
|
||||
return [GenTableOutSchema(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -8,7 +8,6 @@ from .model import JobModel, JobLogModel
|
||||
from .schema import JobCreateSchema,JobUpdateSchema,JobLogCreateSchema,JobLogUpdateSchema
|
||||
|
||||
|
||||
|
||||
class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
"""定时任务数据层"""
|
||||
|
||||
@@ -62,10 +61,6 @@ class JobLogCRUD(CRUDBase[JobLogModel, JobLogCreateSchema, JobLogUpdateSchema]):
|
||||
"""获取定时任务日志列表"""
|
||||
return await self.list(search=search, order_by=order_by)
|
||||
|
||||
async def create_obj_log_crud(self, data: JobLogCreateSchema) -> Optional[JobLogModel]:
|
||||
"""创建定时任务日志"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def delete_obj_log_crud(self, ids: List[int]) -> None:
|
||||
"""删除定时任务日志"""
|
||||
return await self.delete(ids=ids)
|
||||
@@ -35,13 +35,15 @@ class JobLogQueryParam:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job_name: Optional[str] = Query(None, description="任务名称"),
|
||||
status: Optional[bool] = Query(None, description="状态: 正常,失败"),
|
||||
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2025-01-01 00:00:00"),
|
||||
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2025-12-31 23:59:59"),
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
self.job_name = ("like", job_name)
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
|
||||
# 时间范围查询
|
||||
if start_time and end_time:
|
||||
self.create_time = ("between", (start_time, end_time))
|
||||
@@ -168,9 +168,9 @@ class JobLogService:
|
||||
'job_kwargs': '关键字参数',
|
||||
'job_trigger': '任务触发器',
|
||||
'job_message': '日志信息',
|
||||
'status': '执行状态',
|
||||
'exception_info': '异常信息',
|
||||
'created_at': '创建时间',
|
||||
'status': '执行状态',
|
||||
'create_time': '创建时间',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
|
||||
@@ -15,7 +15,7 @@ class AuthSchema(BaseModel):
|
||||
|
||||
user: Optional[UserOutSchema] = Field(default=None, description='用户信息')
|
||||
check_data_scope: bool = Field(default=True, description='是否检查数据权限')
|
||||
db: AsyncSession | Session | None = Field(default=None, description='数据库会话')
|
||||
db: AsyncSession = Field(description='数据库会话')
|
||||
|
||||
|
||||
class JWTPayloadSchema(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user