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 判断知识库的好坏 在知…...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
HTML前端开发:JavaScript 常用事件详解
作为前端开发的核心,JavaScript 事件是用户与网页交互的基础。以下是常见事件的详细说明和用法示例: 1. onclick - 点击事件 当元素被单击时触发(左键点击) button.onclick function() {alert("按钮被点击了!&…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...
命令行关闭Windows防火墙
命令行关闭Windows防火墙 引言一、防火墙:被低估的"智能安检员"二、优先尝试!90%问题无需关闭防火墙方案1:程序白名单(解决软件误拦截)方案2:开放特定端口(解决网游/开发端口不通)三、命令行极速关闭方案方法一:PowerShell(推荐Win10/11)方法二:CMD命令…...
