Python实现一个类似MybatisPlus的简易SQL注解

文章目录

  • 前言
  • 实现思路
  • 定义一个类
  • 然后开始手撸这个微型框架
  • 根据字符串获取到所定义的`DTO`类
  • 构建返回结果
  • 装饰器
    • 解析字符串,获得变量
    • SQL字符串拼接
  • 使用装饰器

前言

在实际开发中,根据业务拼接SQL所需要考虑的内容太多了。于是,有没有一种办法,可以像MyBatisPlus一样通过配置注解实现SQL注入呢?

就像是:

@mybatis.select("select * from user where id = #{id}")
def get_user(id): ...

那可就降低了好多工作量。

P.S.:本文并不希望完全复现MyBatisPlus的所有功能,能够基本配置SQL注解就基本能够完成大部分工作了。

实现思路

那我们这么考虑:

  1. 首先,我们需要定义一个类,类中给一个或者多个装饰器;
  2. 我们先在类内定义一个字符串,这个字符串能够配置到指定的DTO类,用于存储结果;
  3. 我们针对装饰器中的SQL字符串进行解析,解析到其中的变量个数与名称;
  4. 我们针对被装饰的函数进行解析,与SQL变量进行匹配;
  5. 替换变量;
  6. 执行SQL

听起来并不难。我们一步步来。

定义一个类

首先定义:

# dto/student.py
class Student:
    def __init__(self, name, age):
        self.name = name
        self.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 = name
        self.age = age
    def __iter__(self):
        for key, value in self.__dict__.items():
            yield key, value
    def __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, value
    def __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 = name
        self.age = age

至于实际业务中还有很多复杂的联立等操作需要新的类,受限于篇幅,就不展开了。如果能够把本篇看懂的话,相信各位也没什么其他的困难了。

然后开始手撸这个微型框架

# db/common.py
from pydantic import BaseModel, Field

class 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, Field

class 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 cls
        else:
            raise ImportError(f"{class_name} not found in {package_name}")

这样子类就可以调用这个方法获得所需的类了。

构建返回结果

既然都已经能够动态导入类了,那我把返回结果导入到Student中,没问题吧?

其中需要注意的是,我这边采用的数据库驱动是sqlalchemy,所以构造返回结果所需要的参数是sqlalchemyRow类型。

同样的,为了减少子类重写的代码量,直接在父类给出来:

# db/common.py
from pydantic import BaseModel, Field
from sqlalchemy.engine.row import Row

class 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 wrapper
    return 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 wrapper
    return 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_params
            if missing_params:
                raise ValueError(f"Missing required parameters: {', '.join(missing_params)}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

这下应该就能够适配到所有的SQL情况了。

SQL字符串拼接

接下来就是直接替换值了。但是,拼接真的就是对的吗?我们不光是需要考虑不同的变量有着不同的植入格式,同时也需要考虑到植入过程中可能的SQL注入问题。

所以,我们就直接采用sqlalchemytext函数,对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_params
            if 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 wrapper
    return 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_params
            if 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_result
        return wrapper
    return decorator

就是这样,我们就完成了这样一个装饰器。

使用装饰器

使用过程,其实就可以类比@Service中的调用了。而如果拿Python举例的话,其实更像Flaskapp.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): ...

只是说,我们并不需要重复地去写驱动罢了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/981732.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

复试准备日常

实验室目前投了 aiot 这周四 感知计算面试3.5号下午2点开始(面完了他问我有没有项目) 532图像处理实验室(我的项目大多也是图像处理的)(预计下周末)提前到3.4号下午6点 我不在第一批里面 软专不知道要几个 …

HiRT:利用分层机器人Transformer 增强机器人控制

25年2月来自清华、伯克利分校和上海姚期智研究院的论文“HiRT: Enhancing Robotic Control with Hierarchical Robot Transformers”。 大型视觉-语言-动作 (VLA) 模型利用强大的预训练视觉-语言模型 (VLM) 后端,由于其深刻的泛化能力而在机器人控制方面显示出良好…

Netty笔记3:NIO编程

Netty笔记1:线程模型 Netty笔记2:零拷贝 Netty笔记3:NIO编程 Netty笔记4:Epoll Netty笔记5:Netty开发实例 Netty笔记6:Netty组件 Netty笔记7:ChannelPromise通知处理 Netty笔记8&#xf…

MySQL-高级查询

查询处理 排序(默认不是按主键排序的) order by 字段1[,字段2] [asc|desc] 默认是升序排序也可以指定 select 列表中列的序号进行排序如果是多个字段,那么在上一个字段排序完的基础上排序下一个 限制数量 limit 行数&#xff0…

解决各大浏览器中http地址无权限调用麦克风摄像头问题(包括谷歌,Edge,360,火狐)后续会陆续补充

项目场景: 在各大浏览器中http地址调用电脑麦克风摄像头会没有权限,http协议无法使用多媒体设备 原因分析: 为了用户的隐私安全,http协议无法使用多媒体设备。因为像摄像头和麦克风属于可能涉及重大隐私问题的API,ge…

权限系统设计方案实践(Spring Security + RBAC 模型)

前言 权限系统设计基本上是所有项目中都会涉及的一个重要部分。通过权限系统,我们将对用户角色、功能模块访问进行限制,从而保证系统安全性。本文将介绍中大型项目中常用的一套权限系统设计方案,通过 SpringSecurity 安全管理框架&#xff0c…

数学软件Matlab下载|支持Win+Mac网盘资源分享

如大家所了解的,Matlab与Maple、Mathematica并称为三大数学软件。Matlab应用广泛,常被用于数据分析、无线通信、深度学习、图像处理与计算机视觉、信号处理、量化金融与风险管理、机器人,控制系统等领域。 Matlab将数值分析、矩阵计算、科学…

植物大战僵尸杂交版v3.3最新版本(附下载链接)

B站游戏作者潜艇伟伟迷于12月21日更新了植物大战僵尸杂交版3.3版本!!!,有b站账户的记得要给作者三连关注一下呀! 不多废话下载链接放上: 夸克网盘链接::https://pan.quark.cn/s/6f2a…

GPU、NPU与LPU:大语言模型(LLM)硬件加速器全面对比分析

引言:大语言模型计算基础设施的演进 随着大语言模型(LLM)的快速发展与广泛应用,高性能计算硬件已成为支撑LLM训练与推理的关键基础设施。目前市场上主要有三类处理器用于加速LLM相关任务:GPU(图形处理单元…

计算机网络数据传输探秘:包裹如何在数字世界旅行?

计算机网络数据传输探秘:包裹如何在数字世界旅行? 一、从快递网络看数据传输本质 想象你网购了一件商品: 打包:商家用纸箱包装,贴上地址标签(数据封装)运输:包裹经过网点→分拣中心→运输车(网络节点与链路)签收:快递员核对信息后交付(数据校验与接收)数据的网络…

VirtualBox虚拟机MacOS从Big Sur升级到Sequoia(失败)

VirtualBox虚拟机里安装好Big Sur版本,尝试升级到Sequoia,但是最终失败了。 软件升级 直接在系统偏好-软件更新里可以看到提示,提示可以升级到15版本Sequoia 点击同意,看能不能升级到Sequoia吧。升级前先用时光做了备份。 升级…

从数据到决策,永洪科技助力良信电器“智”领未来

在数字经济浪潮汹涌的时代,数字化转型已成为企业增强竞争力、实现可持续发展的必由之路。良信电器,作为国内知名的电气设备制造企业,积极响应时代号召,携手永洪科技,共同开启了数字化转型的新篇章。 上海良信电器股份有…

dify接入语音转文本模型后报错: microphone not authorized

遇到microphone not authorized莫慌,这是因为没有获取到设备的麦克风权限导致的 解决方法:(三种选其一,我实际使用的是第三种) 1.将http路径转换成https 2.接入的前端增加获取麦克风权限的功能 3.打开设备麦克风权限:(能快速验证…

华为hcia——Datacom实验指南——配置手工模式以太网链路聚合

什么是以太网链路聚合(Eth-trunk) 是一种将多个物理链路捆绑在一起,让设备以为是一条大链路,能够增加带宽,增加冗余度,提升可靠性,实现负载平衡。 传输方式有两种 基于数据流传输和基于数据包…

【随手笔记】利尔达NB模组

1.名称 移芯EC6263GPP 参数 指令备注 利尔达上电输出 [2025-03-04 10:24:21.379] I_AT_WAIT:i_len2 [2025-03-04 10:24:21.724] LI_AT_WAIT:i_len16 [2025-03-04 10:24:21.724] [2025-03-04 10:24:21.733] Lierda [2025-03-04 10:24:21.733] [2025-03-04 10:24:21.745] OK移…

RNN实现精神分裂症患者诊断(pytorch)

RNN理论知识 RNN(Recurrent Neural Network,循环神经网络) 是一种 专门用于处理序列数据(如时间序列、文本、语音、视频等)的神经网络。与普通的前馈神经网络(如 MLP、CNN)不同,RNN…

阿里万相,正式开源

大家好,我是小悟。 阿里万相正式开源啦。这就像是AI界突然开启了一扇通往宝藏的大门,而且还是免费向所有人敞开的那种。 你想想看,在这个科技飞速发展的时代,AI就像是拥有神奇魔法的魔法师,不断地给我们带来各种意想…

json介绍、python数据和json数据的相互转换

目录 一 json介绍 json是什么? 用处 Json 和 XML 对比 各语言对Json的支持情况 Json规范详解 二 python数据和json数据的相互转换 dumps() : 转换成json loads(): 转换成python数据 总结 一 json介绍 json是什么? 实质上是一条字符串 是一种…

250301-OpenWebUI配置DeepSeek-火山方舟+硅基流动+联网搜索+推理显示

A. 最终效果 B. 火山方舟配置(一定要点击添加) C. 硅基流动配置(最好要点击添加,否则会自动弹出所有模型) D. 联网搜索配置 E. 推理过程显示 默认是没有下面的推理过程的显示的 F. SearXNG配置 注意:此…

Linux中死锁问题的探讨

在 Linux 中,死锁(Deadlock) 是指多个进程或线程因为竞争资源而相互等待,导致所有相关进程或线程都无法继续执行的状态。死锁是一种严重的系统问题,会导致系统资源浪费,甚至系统崩溃。 死锁的定义 死锁是指…