Python实现一个类似MybatisPlus的简易SQL注解
文章目录
- 前言
- 实现思路
- 定义一个类
- 然后开始手撸这个微型框架
- 根据字符串获取到所定义的`DTO`类
- 构建返回结果
- 装饰器
- 解析字符串,获得变量
- SQL字符串拼接
- 使用装饰器
前言
在实际开发中,根据业务拼接SQL
所需要考虑的内容太多了。于是,有没有一种办法,可以像MyBatisPlus
一样通过配置注解实现SQL
注入呢?
就像是:
@mybatis.select("select * from user where id = #{id}")
def get_user(id): ...
那可就降低了好多工作量。
P.S.:本文并不希望完全复现
MyBatisPlus
的所有功能,能够基本配置SQL
注解就基本能够完成大部分工作了。
实现思路
那我们这么考虑:
- 首先,我们需要定义一个类,类中给一个或者多个装饰器;
- 我们先在类内定义一个字符串,这个字符串能够配置到指定的
DTO
类,用于存储结果; - 我们针对装饰器中的
SQL
字符串进行解析,解析到其中的变量个数与名称; - 我们针对被装饰的函数进行解析,与
SQL
变量进行匹配; - 替换变量;
- 执行
SQL
;
听起来并不难。我们一步步来。
定义一个类
首先定义:
# dto/student.py
class Student:def __init__(self, name, age):self.name = nameself.age = age
为了简化操作,这个类就不放在任意位置了,直接放在dto
文件夹下,后续导入这个类也就直接从dto
文件夹中引入,就不考虑做这个包名定位的接口了。
当然,为了更方便后续的操作,我们需要在dto
文件夹中定义一个__init__.py
文件,用于对外暴露这个类:
# dto/__init__.py
from dto.student import Student
__all__ = ["Student"]
最后呢,我们为了方便这个类的序列化,让他能够变成dict
类型,加一些魔法函数:
# dto/student.py
class Student:def __init__(self, name, age):self.name = nameself.age = agedef __iter__(self):for key, value in self.__dict__.items():yield key, valuedef __getitem__(self, key):return getattr(self, key)def keys(self):return self.__dict__.keys()
当然,一个项目里面肯定不止这一个返回结果,所以各位也可以这么操作:
# dto/common.py
class CommonResult:def __init__(self): ...def __iter__(self):for key, value in self.__dict__.items():yield key, valuedef __getitem__(self, key):return getattr(self, key)def keys(self):return self.__dict__.keys()
# dto/student.py
from dto.common import CommonResult
class Student(CommonResult):def __init__(self, name, age):self.name = nameself.age = age
至于实际业务中还有很多复杂的联立等操作需要新的类,受限于篇幅,就不展开了。如果能够把本篇看懂的话,相信各位也没什么其他的困难了。
然后开始手撸这个微型框架
# db/common.py
from pydantic import BaseModel, Fieldclass DBManager(BaseModel):base_type: str = Field(..., description="数据库表名")link: str = Field(..., description="数据库连接地址")local_generator: Any = Field(..., description="实体类实例化解析生成器")def search(query_template): ...
在这里呢,我们定义了一个DBManager
作为父类,要求后面的子类必须有:
str
类型的base_type
,表示返回结果类的名称;str
类型的link
,表示数据库连接地址;Any
类型的local_generator
,表示实体类实例化解析生成器,- 任意返回值的query
方法,用于执行SQL
。
为什么一定要用
BaseModel
定义?直接定义self.xxx
不好吗?
因为这样会看起来代码量很大(逃)
看着差不多。
根据字符串获取到所定义的DTO
类
考虑到实际上我们所有的方法都需要特定到具体的位置,所以这个方法还是直接写到DBManager
类中,这样子类就不需要再重写了。
# db/common.py
from pydantic import BaseModel, Fieldclass DBManager(BaseModel):base_type: str = Field(..., description="数据库表名")link: str = Field(..., description="数据库连接地址")local_generator: Any = Field(..., description="实体类实例化解析生成器")def search(query_template): ...def import_class_from_package(self, package_name, class_name):# 根据包名获得`DTO`包_package = importlib.import_module(package_name)# 检测是不是有这么个类if class_name not in _package.__all__:raise ImportError(f"{class_name} not found in {package_name}")# 有就拿着cls = getattr(_package, class_name)# 返回这个类if cls is not None:return clselse:raise ImportError(f"{class_name} not found in {package_name}")
这样子类就可以调用这个方法获得所需的类了。
构建返回结果
既然都已经能够动态导入类了,那我把返回结果导入到Student
中,没问题吧?
其中需要注意的是,我这边采用的数据库驱动是sqlalchemy
,所以构造返回结果所需要的参数是sqlalchemy
的Row
类型。
同样的,为了减少子类重写的代码量,直接在父类给出来:
# db/common.py
from pydantic import BaseModel, Field
from sqlalchemy.engine.row import Rowclass DBManager(BaseModel):base_type: str = Field(..., description="数据库表名")link: str = Field(..., description="数据库连接地址")local_generator: Any = Field(..., description="实体类实例化解析生成器")def search(query_template): ...# 为了方便看,省略掉细节def import_class_from_package(self, package_name, class_name): ...def build_obj(self, row: Row):return self.local_generator(**row._asdict()) if self.local_generator else None
装饰器
那么接下来就是重头戏了,怎么定义这个装饰器。
我们先构建一个子类:
# db/student.py
class StudentDBManager(DBManager):base_type: ClassVar[str] = "Student"link: ClassVar[str] = 'sqlite:///school.db'local_generator: ClassVar[Any] = None"""自定义PyMyBatis"""def __init__(self):StudentDBManager.local_generator = self.import_class_from_package("dto", self.base_type)
在这里,首先需要注意的是,需要用ClassVar
修饰,将变量名定义为类内成员变量,否则无法使用self.xxx
访问。
其次,我们利用base_type
指定返回值对应的DTO
类、link
指定数据库连接地址,local_generator
指定实体类实例化解析生成器。
在这个类实例化的过程中,我们还需要进一步构建local_generator
,也就是动态执行from xxx import xxx
。
然后定义一个装饰器:
def query(query_template: str):def decorator(func):@wraps(func)def wrapper(*args, **kwargs):return func(*args, **kwargs)return wrapperreturn decorator
这可以算得上是比较基础的模板了。至于之后怎么改,管他呢,先套公式。
在这里,我们首先定义的装饰器是decorator
,没有参数;其次再用query
装饰器包装,从而给无参的装饰器给一个参数,从而接收一个SQL
字符串参数。
好的,我们再进一步。
解析字符串,获得变量
首先当然是解析SQL
字符串,获得变量。如何做呢?为了简便,这里直接采用正则匹配的方式:
def query(self, query_template):def decorator(func):# 解析 SQL 中的 #{变量} 语法param_pattern = re.compile(r"#{(\w+)}")required_params = set(param_pattern.findall(query_template))@wraps(func)def wrapper(*args, **kwargs):return func(*args, **kwargs)return wrapperreturn decorator
没啥问题。
接下来,调用的时候,我们需要检测是否完整给出了SQL
字符串所需的参数。
我们考虑到,如果但凡SQL
中的参数有变化,方法就会有变化,因此每个SQL
都有一个方法也太麻烦了。主要是这么多相似的方法起方法名太烦了
所以,直接上反射,获取 调用 的时侯传入的参数。
值得注意的是,这里说的是 调用 的时候。因为Python
中 定义 方法的时候可以使用**kargs
传入多个参数,但是如果反射直接获取到 定义 的参数,将会只有一个kargs
,这显然不是我们所希望的。
所以,再加一些:
def query(self, query_template):def decorator(func):# 解析 SQL 中的 #{变量} 语法param_pattern = re.compile(r"#{(\w+)}")required_params = set(param_pattern.findall(query_template))@wraps(func)def wrapper(*args, **kwargs):# 获取函数的参数签名sig = inspect.signature(func)bound_args = sig.bind_partial(*args, **kwargs)bound_args.apply_defaults()# 提取传递的参数,包括 **kwargs 中的参数provided_params = set(bound_args.arguments.keys()) | set(kwargs.keys())# 检查缺失的参数missing_params = required_params - provided_paramsif missing_params:raise ValueError(f"Missing required parameters: {', '.join(missing_params)}")return func(*args, **kwargs)return wrapperreturn decorator
这下应该就能够适配到所有的SQL
情况了。
SQL字符串拼接
接下来就是直接替换值了。但是,拼接真的就是对的吗?我们不光是需要考虑不同的变量有着不同的植入格式,同时也需要考虑到植入过程中可能的SQL
注入问题。
所以,我们就直接采用sqlalchemy
的text
函数,对SQL
进行拼接与赋值。
def query(self, query_template):def decorator(func):# 解析 SQL 中的 #{变量} 语法param_pattern = re.compile(r"#{(\w+)}")required_params = set(param_pattern.findall(query_template))@wraps(func)def wrapper(*args, **kwargs):# 获取函数的参数签名sig = inspect.signature(func)bound_args = sig.bind_partial(*args, **kwargs)bound_args.apply_defaults()# 提取传递的参数,包括 **kwargs 中的参数provided_params = set(bound_args.arguments.keys()) | set(kwargs.keys())# 检查缺失的参数missing_params = required_params - provided_paramsif missing_params:raise ValueError(f"Missing required parameters: {', '.join(missing_params)}")# 构建 SQL 语句,并考虑不同类型的数据格式sql_query = text(query_template.replace("#{", ":").replace("}", ""))print(f"Executing SQL: {sql_query}")return func(*args, **kwargs)return wrapperreturn decorator
好了,到这一步也就基本完成了。最后,我们根据数据库存储数据的特点,最后修整一下查询的格式细节,就可以了:
def query(self, query_template):def decorator(func):# 解析 SQL 中的 #{变量} 语法param_pattern = re.compile(r"#{(\w+)}")required_params = set(param_pattern.findall(query_template))@wraps(func)def wrapper(*args, **kwargs):# 获取函数的参数签名sig = inspect.signature(func)bound_args = sig.bind_partial(*args, **kwargs)bound_args.apply_defaults()# 提取传递的参数,包括 **kwargs 中的参数provided_params = set(bound_args.arguments.keys()) | set(kwargs.keys())# 检查缺失的参数missing_params = required_params - provided_paramsif missing_params:raise ValueError(f"Missing required parameters: {', '.join(missing_params)}")# 构建 SQL 语句,并考虑不同类型的数据格式sql_query = text(query_template.replace("#{", ":").replace("}", ""))print(f"Executing SQL: {sql_query}")params = bound_args.arguments.copy()for key, value in params.items():if isinstance(value, datetime):params[key] = value.strftime('%Y-%m-%d')engine = create_engine(self.link)with engine.connect() as conn:result = conn.execute(sql_query, params)search_result = [self.create_item_obj(row) for row in result]return search_resultreturn wrapperreturn decorator
就是这样,我们就完成了这样一个装饰器。
使用装饰器
使用过程,其实就可以类比@Service
中的调用了。而如果拿Python
举例的话,其实更像Flask
的app.route
。于是我们可以这么使用:
sbd = StudentDBManager()
@sbd.query("SELECT * FROM student WHERE id = #{id}")
def find_student_by_id(**kargs): ...
这也就实现了一个方法。
当然,他也没那么智能。虽然写起来是这样,但是依然相当于:
sbd = StudentDBManager()
@sbd.query("SELECT * FROM student WHERE id = #{id}")
def find_student_by_id(id: str): ...
只是说,我们并不需要重复地去写驱动罢了。
相关文章:
Python实现一个类似MybatisPlus的简易SQL注解
文章目录 前言实现思路定义一个类然后开始手撸这个微型框架根据字符串获取到所定义的DTO类构建返回结果装饰器解析字符串,获得变量SQL字符串拼接 使用装饰器 前言 在实际开发中,根据业务拼接SQL所需要考虑的内容太多了。于是,有没有一种办法…...
linux一些使用技巧
linux一些使用技巧 文件名称和路径的提取切换用户执行当前脚本一行演示单引号与双引号的使用curl命令仅输出响应头信息,不输出body体文件名称和路径的提取 文件路径为 /tmp/tkgup/test.sh 方式获取文件名获取文件路径获取文件全路径方式一basename ${file}dirname ${file}real…...
小模型和小数据可以实现AGI吗
小模型和小数据很难实现真正的 通用人工智能(AGI, Artificial General Intelligence),但在特定任务或受限环境下,可以通过高效的算法和优化方法实现“近似 AGI” 的能力。 1. 为什么小模型小数据难以实现 AGI? AGI 需…...

io学习----->文件io
思维导图: 一.文件io的概念 文件IO:指程序和文件系统之间的数据交互 特点: 1.不存在缓冲区,访问速度慢 2.不可以移植,依赖于操作系统 3.可以访问不同的文件类型(软连接,块设备等) 4.文件IO属于系统调…...

kubernetes介绍
文章目录 kubernetes概述kubernetes组件kubernetes概念 kubernetes概述 kubernetes,是一个全新的基于容器技术的分布式架构领先方案,是Google开源的的容器编排工具。 kubernetes的本质是一组服务器集群,它可以在集群的每个节点上运行特定…...
如何高效准备PostgreSQL认证考试?
高效准备 PostgreSQL 中级认证考试,可从知识储备、技能提升、模拟考试等方面入手,以下是具体建议: 深入学习理论知识 系统学习核心知识:依据考试大纲,对 PostgreSQL 的体系结构、数据类型、SQL 语言、事务处理、存储过…...

如何使用Briefing打造私有视频会议系统结合内网穿透异地远程连接
文章目录 前言1.关于briefing2.本地部署briefing3.使用briefing4.cpolar内网穿透工具安装5.创建远程连接公网地址6.固定briefing公网地址 前言 在这个‘云’字当道的时代,远程办公、异地恋已经成了生活常态。视频聊天自然也就成了日常操作。但一不小心,…...

XHR请求解密:抓取动态生成数据的方法
在如今动态页面大行其道的时代,传统的静态页面爬虫已无法满足数据采集需求。尤其是在目标网站通过XHR(XMLHttpRequest)动态加载数据的情况下,如何精准解密XHR请求、捕获动态生成的数据成为关键技术难题。本文将深入剖析XHR请求解密…...

坐标变换介绍与机器人九点标定的原理
【备注】本文的C#代码在下面链接中可以下载:Opencv的C#九点标定代码资源-CSDN文库 https://download.csdn.net/download/qq_34047402/90452336 一、坐标变换的介绍 1.绕原点旋转的坐标变换 一个点(x,y)绕原点旋转u度,其旋转后的坐标(x1,y1)如何计算? 2.绕任意点的坐标变…...

串口调试助手Alien v5.198新版发布
v5.198 更改点: 1.增加USB打印机支持 2.支持特殊波特率/自定义波特率 3.支持窗口透明调整 4.支持接收框文本左/中/右对齐,粗体字,自动换行 5.支持接收时间戳 6.HEX接收自动换行 7.支持文本颜色主题 8.支持文本字体修改 9.增加菜单/增状态栏显示当前接口 下载 alien_v5.198.7z …...
解锁Android RemoteViews:跨进程UI更新的奥秘
一、RemoteViews 简介 在 Android 开发的广阔领域中,RemoteViews 是一个独特且重要的概念,它为开发者提供了一种在其他进程中显示视图结构的有效方式。从本质上讲,RemoteViews 并非传统意义上在当前应用进程内直接渲染和操作的 View…...
编译可以在Android手机上运行的ffmpeg程序
下载代码 git clone gitgithub.com:FFmpeg/FFmpeg.git git checkout n7.0建立build目录 mkdir build cd build创建build.sh脚本 vim build.sh这段脚本的主要功能是配置和编译 FFmpeg,使其能够在 Android 平台上运行,通过设置不同的架构和 API 级别&am…...

Verilog学习方法—基础入门篇(一)
前言: 在FPGA开发中,Verilog HDL(硬件描述语言)是工程师必须掌握的一项基础技能。它不仅用于描述数字电路,还广泛应用于FPGA的逻辑设计与验证。对于初学者来说,掌握Verilog的核心概念和基本语法࿰…...

本地jar包添加到 maven
进入到 你的 maven bin文件夹下 执行cmd ,然后执行命令 mvn install:install-file -Dfilepath/to/your/artifact.jar -DgroupIdyour.group.id -DartifactIdyour-artifact-id -Dversion1.0 -Dpackagingjar 替换path/to/your/artifact.jar为你的JAR文件路径…...

C# Unity 唐老狮 No.6 模拟面试题
本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: 全部 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho 如果你发现了文章内特殊的字体格式,…...
项目工坊 | Python驱动淘宝信息爬虫
目录 前言 1 完整代码 2 代码解读 2.1 导入模块 2.2 定义 TaoBao 类 2.3 search_infor_price_from_web 方法 2.3.1 获取下载路径 2.3.2 设置浏览器选项 2.3.3 反爬虫处理 2.3.4 启动浏览器 2.3.5 修改浏览器属性 2.3.6 设置下载行为 2.3.7 打开淘宝登录页面 2.3.…...
Java8-Stream流介绍和使用案例
Java 8 引入了 Stream API,它提供了一种高效且声明式的方式来处理集合数据。Stream 的核心思想是将数据的操作分为中间操作(Intermediate Operations)和终端操作(Terminal Operations),并通过流水线&#x…...
setlocale()的参数,“zh_CN.UTF-8“, “chs“, “chinese-simplified“的差异。
在 C/C 中,setlocale() 函数的参数 zh_CN.UTF-8、chs 和 chinese-simplified 均用于设置中文简体环境,但它们的语义、平台支持和编码行为存在显著差异: 1. zh_CN.UTF-8(推荐) 含义: zh_CN: 中文&…...

docker 安装达梦数据库(离线)
docker安装达梦数据库,官网上已经下载不了docker版本的了,下面可通过百度网盘下载 通过网盘分享的文件:dm8_20240715_x86_rh6_rq_single.tar.zip 链接: https://pan.baidu.com/s/1_ejcs_bRLZpICf69mPdK2w?pwdszj9 提取码: szj9 上传到服务…...
FastGPT 引申:如何基于 LLM 判断知识库的好坏
文章目录 如何基于 LLM 判断知识库的好坏方法概述示例 Prompt声明抽取器 Prompt声明检查器 Prompt 判断机制总结 下面介绍如何基于 LLM 判断知识库的好坏,并展示了如何利用声明抽取器和声明检查器这两个 prompt 构建评价体系。 如何基于 LLM 判断知识库的好坏 在知…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...

搭建DNS域名解析服务器(正向解析资源文件)
正向解析资源文件 1)准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2)服务端安装软件:bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...