展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > 如何在Python运行时修改业务SQL代码?

如何在Python运行时修改业务SQL代码?

SQLPro
SQLPro

SQLPro 是一款macOS平台上数据库管理应用,避免使用非法,享受更全面的支持和更新。它更贴近日常 macOS 使用场景,处理高频操作时整体会更顺手一些。

¥119
macOS 2026-09-24
正版软件 macOS软件 数据库管理工具 macOS
立即下载

1.缘起最近项目在准备搞SASS化,SASS化有一个特点就是多租户,且每个租户之间的数据都要隔离,对于数据库的隔离方案常见的有数据库隔离,表隔离,字段隔离,目前我只用到表隔离和字段隔离(数据库隔离的原理也是差不多)。对于字段隔离比较简单,就是查询条件不同而已,比如像下面的SQL查询:SELECT*FROMt_demoWHEREtenant_id='xxx'ANDis_del=0但是为了严谨,需求上需要在执行SQL之前检查对应的表是否带上tenant_id的查询字段。对于表隔离就麻烦了一些,

本文目录

    1.缘起

    最近项目在准备搞SASS化,SASS化有一个特点就是多租户,且每个租户之间的数据都要隔离,对于数据库的隔离方案常见的有数据库隔离,表隔离,字段隔离,目前我只用到表隔离和字段隔离(数据库隔离的原理也是差不多)。 对于字段隔离比较简单,就是查询条件不同而已,比如像下面的SQL查询:

    SELECT * FROM t_demo WHERE tenant_id='xxx' AND is_del=0

    但是为了严谨,需求上需要在执行SQL之前检查对应的表是否带上tenant_id的查询字段。

    对于表隔离就麻烦了一些,他需要做到在运行的时候根据对应的租户ID来处理某个数据表,举个例子,假如有下面这样的一条SQL查询:

    SELECT * FROM t_demo WHERE is_del=0

    在遇到租户A时,SQL查询将变为:

    SELECT * FROM t_demo_a WHERE is_del=0

    在遇到租户B时,SQL查询将变为:

    SELECT * FROM t_demo_b WHERE is_del=0

    如果商户数量固定时,一般在代码里编写if-else来判断就可以了,但是常见的SASS化应用的商户是会一直新增的,那么对于这个SQL逻辑就会变成这样:

    def sql_handle(tenant_id: str):
        table_name: str = f"t_demo_{tenant_id}"
        sql: str = f"SELECT * FROM {table_name} WHERE is_del=0"

    但是这有几个问题,对于ORM来说,一开始只创建一个t_demo对应的表对象就可以了,现在却要根据多个商户创建多个表对象,这是不现实的,其次如果是裸写SQL,一般会使用IDE的检查,而对于这样的SQL:

    sql: str = f"SELECT * FROM {table_name} WHERE is_del=0"

    IDE是没办法进行检查的,当然还有一个最为严重的问题,就是当前的项目已经非常庞大了,如果每个相关表的调用都进行适配更改的话,那工程量就非常庞大了,所以最好的方案就是在引擎库得到用户传过来的SQL语句后且还没发送到MySQL服务器之前自动的根据商户ID更改SQL, 而要达到这样的效果,就必须侵入到我们使用的MySQL的引擎库,修改里面的方法来兼容我们的需求。

    不管是使用dbutils还是sqlalchemy,都可以指定一个引擎库,目前常用的引擎库是pymysql,所以下文都将以pymysql为例进行阐述。

    2.侵入库

    由于必须侵入到我们使用的引擎库,所以我们应该先判断我们需要修改引擎库的哪个方法,在经过源码阅读后,我判定只要更改pymysql.cursors.Cursor的mogrify方法:

    def mogrify(self, query, args=None):
        """
        Returns the exact string that is sent to the database by calling the
        execute() method.
    
        This method follows the extension to the DB API 2.0 followed by Psycopg.
        """
        conn = self._get_db()
    
        if args is not None:
            query = query % self._escape_args(args, conn)
        return query

    这个方法的作用就是把用户传过来的SQL和参数进行整合,生成一个最终的SQL,刚好符合我们的需求,于是可以通过继承的思路来创建一个新的属于我们自己的Cursor类:

    import pymysql
    class Cursor(pymysql.cursors.Cursor):
        def mogrify(self, query: str, args: Union[None, list, dict, tuple] = None) -> str:
            # 在此可以编写处理还合成的SQL逻辑
            mogrify_sql: str = super().mogrify(query, args)
            # 在此可以编写处理合成后的SQL逻辑
            return mogrify_sql
    class DictCursor(pymysql.cursors.DictCursorMixin, Cursor):
        """A cursor which returns results as a dictionary"""
        # 直接修改Cursor类的`mogrify`方法并不会影响到`DictCursor`类,所以我们也要创建一个新的`Cursor`类。

    创建好了Cursor类后,就需要考虑如何在pymysql中应用我们自定义的Cursor类了,一般的Mysql连接库都支持我们传入自定义的Cursor类,比如pymysql:

    import pymysql.cursors
    # Connect to the database
    connection = pymysql.connect(
        host='localhost',
        user='user',
        password='passwd',
        database='db',
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor
    )

    我们可以通过cursorclass来指定我们的Cursor类,如果使用的库不支持或者是其它原因则需要使用猴子补丁的方法,具体的使用方法见Python探针完成调用库的数据提取。

    3.获取商户ID

    现在我们已经搞定了在何处修改SQL的问题了,接下来就要思考如何在mogrify方法获取到商户ID以及那些表要进行替换,一般我们在进行一段代码调用时,有两种传参数的方法, 一种是传数组类型的参数:

    with conn.cursor() as cursor:
        cursor.execute("SELECT * FROM t_demo WHERE is_del=%s", (0, ))

    一种是传字典类型的参数:

    with conn.cursor() as cursor:
        cursor.execute("SELECT * FROM t_demo WHERE is_del=%(is_del)s", {"is_del": 0})

    目前大多数的项目都存在这两种类型的编写习惯,而引擎库在执行execute时会经过处理后才把参数sql和args传给了mogrify,如果我们是使用字典类型的参数,那么可以在里面嵌入我们需要的参数,并在mogrify里面提取出来,但是使用了数组类型的参数或者是ORM库的话就比较难传递参数给mogrify方法了,这时可以通过context隐式的把参数传给mogrify方法,具体的分析和原理可见:python如何使用contextvars模块源码分析。

    context的使用方法很简单, 首先是创建一个context封装的类:

    from contextvars import ContextVar, Token
    from typing import Any, Dict, Optional, Set
    context: ContextVar[Dict[str, Any]] = ContextVar("context", default={})
    class Context(object):
        """基础的context调用,支持Type Hints检查"""
        tenant_id: str
        replace_table_set: Set[str]
        def __getattr__(self, key: str) -> Any:
            value: Any = context.get().get(key)
            return value
        def __setattr__(self, key: str, value: Any) -> None:
            context.get()[key] = value
    class WithContext(Context):
        """简单的处理reset token逻辑,和context管理,只用在业务代码"""
        def __init__(self) -> None:
            self._token: Optional[Token] = None
    
        def __enter__(self) -> "WithContext":
            self._token = context.set({})
            return self
        def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
            if self._token:
                context.reset(self._token)
                self._token = None

    接下来在业务代码中,通过context传入当前业务对应的参数:

    with WithContext as context:
        context.tenant_id = "xxx"
        context.replace_table_set = {"t_demo"}
        with conn.cursor() as cursor:
            cursor.execute("SELECT * FROM t_demo WHERE is_del=%s", (0, ))

    然后在mogrify中通过调用context即可获得对应的参数了:

    import pymysql
    class Cursor(pymysql.cursors.Cursor):
        def mogrify(self, query: str, args: Union[None, list, dict, tuple] = None) -> str:
            tenant_id: str = context.tenant_id
            replace_table_set: Set[str] = context.replace_table_set
            # 在此可以编写处理还合成的SQL逻辑
            mogrify_sql: str = super().mogrify(query, args)
            # 在此可以编写处理合成后的SQL逻辑
            return mogrify_sql

    4.修改SQL

    现在,万事俱备,只剩下修改SQL的逻辑,之前在做别的项目的时候,建的表都是十分的规范,它们是以t_xxx的格式给表命名,这样一来替换表名十分方便,只要进行两次替换就可以兼容大多数情况了,代码如下:

    import pymysql
    class Cursor(pymysql.cursors.Cursor):
        def mogrify(self, query: str, args: Union[None, list, dict, tuple] = None) -> str:
            tenant_id: str = context.tenant_id
            replace_table_set: Set[str] = context.replace_table_set
            # 简单示例,实际上正则的效率会更好
            for replace_table in replace_table_set:
                if replace_table in query:
                    # 替换表名
                    query = query.replace(f" {replace_table} ", f" {replace_table}_{tenant_id} ")
                    # 替换查询条件中带有表名的
                    query = query.replace(f" {replace_table}.", f" {replace_table}_{tenant_id}.")
            mogrify_sql: str = super().mogrify(query, args)
            # 在此可以编写处理合成后的SQL逻辑
            return mogrify_sql

    但是现在项目的SQL规范并不是很好,有些表名还是MySQL的关键字,所以靠简单的替换是行不通的,同时这个需求中,一些表只需要字段隔离,需要确保有带上对应的字段查询,这就意味着必须有一个库可以来解析SQL,并返回一些数据使我们可以比较方便的知道SQL中哪些是表名,哪些是查询字段了。

    目前在Python中有一个比较知名的SQL解析库--sqlparse,它可以通过解析引擎把SQL解析成一个Python对象,之后我们就可以通过一些语法来判断哪些是SQL关键字, 哪些是表名,哪些是查询条件等等。但是这个库只实现一些底层的API,我们需要对他和SQL比较了解之后才能实现一些比较完备的功能,比如下面3种常见的SQL:

    SELECT * FROM t_demo
    SELECT * FROM t_demo as demo
    SELECT * FROM t_other as other LEFT JOIN t_demo demo on demo.xxx==other.xxx

    如果我们要通过sqlparse来提取表名的话就需要处理这3种情况,而我们如果要每一个情况都编写出来的话,那将会非常费心费力,同时也可能存在遗漏的情况,这时就需要用到另外一个库--sql_metadata,这个库是基于sqlparse和正则的解析库,同时提供了大量的常见使用方法的封装,我们通过直接调用对应的函数就能知道SQL中有哪些表名,查询字段是什么了。

    目前已知这个库有一个缺陷,就是会自动去掉字段的符号, 比如表名为关键字时,我们需要使用`符号把它包起来:

    SELECT * FROM `case`

    但在经过sql_metadata解析后得到的表名是case而不是`case`,需要人为的处理,但是我并不觉得这是一个BUG,自己不按规范创建表,能怪谁呢。

    接下来就可以通过sql_metadata的方法来实现我需要的功能了,在根据需求修改后,代码长这样(说明见注释):

    from typing import Dict, Set, Tuple, Union
    import pymysql
    import sql_metadata
    class Cursor(pymysql.cursors.Cursor):
        def mogrify(self, query: str, args: Union[None, list, dict, tuple] = None) -> str:
            tenant_id: str = context.tenant_id
            # 生成一个解析完成的SQL对象
            sql_parse: sql_metadata.Parser = sql_metadata.Parser(query)
    
            # 新加的一个属性,这里存下需要校验查询条件的表名
            check_flag = False 
            where_table_set: Set[str] = context.where_table_set
            # 该方法会获取到SQL对应的table,返回的是一个table的数组
            for table_name in sql_parse.tables:
                if table_name in where_table_set:
                    if sql_parse.columns_dict:
                        # 该方法会返回SQL对应的字段,其中分为select, join, where等,这里只用到了where
                        for where_column in sql_parse.columns_dict.get("where", []):
                            # 如果连表,里面存的是类似于t_demo.tenant_id,所以要兼容这一个情况
                            if "tenant_id" in where_column.lower().split("."):
                                check_flag = True
                                break
            if not check_flag:
                # 检查不通过就抛错
                raise RuntimeError()
            # 更换表名的逻辑
            replace_table_set: Set[str] = context.replace_table_set
            new_query: str = query
            for table_name in sql_parse.tables:
                if table_name in replace_table_set:
                    new_query = ""
                    # tokens存放着解析完的数据,比如SELECT * FROM t_demo解析后是
                    # [SELECT, *, FROM, t_demo]四个token
                    for token in sql_parse.tokens:
                        # 判断token是否是表名  
                        if token.is_potential_table_name:
                            # 提取规范的表名
                            parse_table_name: str = token.stringified_token.strip()
                            if parse_table_name in replace_table_set:
                                new_table_name: str = f" {parse_table_name}_{tenant_id}"
                                # next_token代表SQL的下一个字段
                                if token.next_token.normalized != "AS":
                                    # 如果当前表没有设置别名
                                    # 通过AS把替换前的表名设置为新表名的别名,这样一来后面的表名即使没进行更改,也是能读到对应商户ID的表
                                    new_table_name += f" AS {parse_table_name}"
                                query += new_table_name
                                continue
                        # 通过stringified_token获取的数据会自动带空格,比如`FROM`得到的会是` FROM`,这样拼接的时候就不用考虑是否加空格了
                        new_query += token.stringified_token
            mogrify_sql: str = super().mogrify(new_query, args)
            # 在此可以编写处理合成后的SQL逻辑
            return mogrify_sql

    这份代码十分简单,它只做简单介绍,事实上这段逻辑会应用到所有的SQL查询中,我们应该要保证这段代码是没问题的,同时不要有太多的性能浪费,所以在使用的时候要考虑到代码拆分和优化。 比如在使用的过程中可以发现,我们的SQL转换和检查都是在父类的Cursor.mogrify之前进行的,这就意味着不管我们代码逻辑里cursor.execute传的参数是什么,对于同一个代码逻辑来说,传过来的query值是保持不变的,比如下面的代码:

    def get_user_info(uid: str) -> Dict[str, Any]:
        with conn.cursor() as cursor:
            cursor.execute("SELECT * FROM t_user WHERE uid=%(uid)s", {"uid": uid})
            return cursor.fetchone() or {}

    这段代码中传到Cursor.mogrify的query永远为SELECT * FROM t_user WHERE uid=%(uid)s,有变化的只是args中uid的不同。 有了这样的一个前提条件,那么我们就可以把query的校验结果和转换结果缓存下来,减少每次都需要解析SQL再校验造成的性能浪费。至于如何实现缓存则需要根据自己的项目来决定,比如项目中只有几百个SQL执行,那么直接用Python的dict来存放就可以了,如果项目中执行的SQL很多,同时有些执行的频率非常的高,有些执行的频率非常的低,那么可以考虑使用LRU来缓存。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Python SQL
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    Aperty macOS版
    Aperty macOS版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw macOS版
    AionClaw macOS版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy macOS版
    Loomy macOS版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirBuddy 3 macOS版
    AirBuddy 3 macOS版

    AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

    AirServer macOS版
    AirServer macOS版

    AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

    AnyDesk macOS版
    AnyDesk macOS版

    AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

    WINDOWS 更多
    讯飞绘文 网页版
    讯飞绘文 网页版

    讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

    Maya(Autodesk Maya) Windows版
    Maya(Autodesk Maya) Windows版

    Autodesk Maya 是面向影视、电视、动画和游戏行业的专业三维制作软件,集多边形与 NURBS 建模、角色绑定、关键帧和动作捕捉动画、Bifrost 特效模拟、材质开发及 Arnold 渲染于一体,

    Aperty Windows版
    Aperty Windows版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw Windows版
    AionClaw Windows版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy Windows版
    Loomy Windows版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirServer Windows版
    AirServer Windows版

    AirServer Windows版可把电脑变成AirPlay、Google Cast和Miracast无线投屏接收端,适合会议演示、课堂教学及家庭大屏分享。发送设备通常无需另装客户端,并可同时接收和智能排列多个镜像会话。

    即将离开本站
    您即将前往第三方网站,请确认是否继续?