一篇能让新手在 30 分钟内跑通的文章。本文不堆术语,只解决三件事:怎么连、怎么用、踩哪些坑。所有代码均已在 KingbaseES V009R001C010 + Python 3.14 上实测通过。
一、为什么用 Python 接金仓?
国产数据库的教程生态里,Java/Spring Boot 的例子一抓一把,Python 却寥寥无几。但实际工作中,数据分析、运维脚本、ETL、AI Agent 后端------这些场景里 Python 才是主力语言。

金仓数据库(KingbaseES)的通信协议是 PostgreSQL 兼容 的,这意味着我们不需要等官方驱动"赏饭吃",用 psycopg2 这类成熟的 PG 生态库就能直接接入。本文就把完整链路跑一遍,并且把我在实操中踩到的 三个真实坑也一起交付给你。
技术栈:
| 组件 | 版本 | 说明 |
|---|---|---|
| KingbaseES | V009R001C010 | 国产数据库,PG 内核 |
| Python | 3.14 | 任意 3.9+ 均可 |
| psycopg2-binary | 2.9.12 | PG 协议驱动 |
| SQLAlchemy | 2.x | ORM 框架 |
二、环境准备
2.1 金仓数据库安装确认
假设你已经按官方文档装好了金仓。我的实例配置如下(后面代码就用这套参数):
- 端口:54321(金仓默认,PG 是 5432)
- 实例:
kes_dev - 用户:
system(默认管理员) - 密码:
Kingbase@2026 - 业务库:
test - 兼容模式:MySQL

小贴士:兼容模式在创建数据库时指定,可选 ORACLE / PG / MySQL / SQLSERVER。新手建议 MySQL,语法最熟悉。
2.2 Python 依赖安装
打开 PowerShell,执行:
Python
pip install psycopg2-binary sqlalchemy -i https://pypi.tuna.tsinghua.edu.cn/simple

这里就迎来了 第一个坑。
三、踩坑实录(三个真实的坑)
这三个坑是本文最值钱的部分,因为它们在官方文档里搜不到、在中文博客里也没人写。
坑 1:PyPI 上根本没有 kingbase8 这个包
我一开始按"国产数据库标配"的思路,去 pip 找官方驱动:
pip install kingbase8
结果 Tsinghua 源、阿里源、官方源全部报 No matching distribution found for kingbase8。
翻金仓安装目录 E:\major\V9R1C10\installpackage\KESRealPro\V009R001C010\Client 才找到一个 Ksycopg2 压缩包,解压一看------只支持 Python 2.7 / 3.5。对现在的 Python 3.14 完全不兼容。
解决方案:金仓的通信协议本来就是 PG 协议,直接用 psycopg2-binary 就行,一行代码都不用改:
Python
import psycopg2
conn = psycopg2.connect(host="localhost", port=54321, ...)
坑 2:SQLAlchemy 不认识金仓的版本串
坑 1 解决后,原生 psycopg2 连接顺利通过。但当我切到 SQLAlchemy,立刻又炸了:
AssertionError: Could not determine version from string 'KingbaseES V009R001C010'

原因:SQLAlchemy 的 PG dialect 启动时会调 SELECT version(),然后用正则解析版本号。金仓返回的 KingbaseES V009R001C010 不是标准 PG 格式,解析失败直接抛异常。
解决方案 :Monkey-patch 一下 PGDialect_psycopg2.initialize,捕获 AssertionError 后手动塞一个版本号进去:
python
from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
_orig_initialize = PGDialect_psycopg2.initialize
def _patched_initialize(self, connection):
try:
_orig_initialize(self, connection)
except AssertionError:
# 金仓 V009R001C010 ≈ PG (9, 1, 0)
self.server_version_info = (9, 1, 0)
self.isolation_level = "READ COMMITTED"
PGDialect_psycopg2.initialize = _patched_initialize
这段代码放在 create_engine 之前即可。别害怕 monkey-patch,对国产化适配来说,这是最干净的做法------不侵入 SQLAlchemy 源码,升级时也不会留坑。
坑 3:MySQL 模式下字符串比较默认大小写不敏感
第三个坑是我在写登录校验时踩的:用户名 'admin' 居然能匹配上数据库里的 'Admin' 和 'ADMIN',密码校验形同虚设。
最小复现:
Python
SELECT 'ABC' = 'abc' AS result;
| 兼容模式 | 返回结果 |
|---|---|
| MySQL | t(true) |
| PG / ORACLE | f(false) |
原因 :金仓为了兼容 MySQL 的默认排序规则,在 MySQL 模式下使用了大小写不敏感的字符串比较。这个行为对从 PG/ORACLE 迁移过来的业务是致命的 ------所有依赖精确大小写的逻辑(用户名、API key、邀请码、序列号......)都会失效,而且是静默失效,不报错也不告警。
解决方案:
= 运算符的大小写不敏感是硬编码的,连 COLLATE "C"、LIKE BINARY 都不能绕过。要拿到大小写敏感比较,得用下面两种方式之一:
SQL
-- 方式 A:转 bytea(二进制)做字节级比较,最简洁
SELECT 'ABC'::bytea = 'abc'::bytea AS strict_result; -- 返回 f
-- 方式 B:比 md5(适合做等值校验,比如登录密码盐)
SELECT md5('ABC') = md5('abc') AS strict_result; -- 返回 f

这是迁移类项目最贵的坑:开发环境跑通、测试用例全过、上线后才发现用户能靠大小写变体登进别人的账号。
四、第一步:原生连接测试
把这三个坑趟完,剩下的代码就非常顺了。先写一个最小的连接测试,确认网络层通。

step1_jdbc/01_test_conn.py:
python
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
import psycopg2
from psycopg2.extras import DictCursor
DB_HOST = "localhost"
DB_PORT = 54321
DB_NAME = "test"
DB_USER = "system"
DB_PASS = "Kingbase@2026"
def main():
print("→ 正在连接金仓数据库 ...")
conn = psycopg2.connect(
host=DB_HOST, port=DB_PORT, dbname=DB_NAME,
user=DB_USER, password=DB_PASS, connect_timeout=5,
)
print("✓ 连接成功!")
with conn.cursor(cursor_factory=DictCursor) as cur:
cur.execute("SELECT version();")
print(f"\n[版本] {cur.fetchone()[0]}")
cur.execute("SELECT current_database(), current_user;")
row = cur.fetchone()
print(f"[当前库] {row[0]} [当前用户] {row[1]}")
cur.execute("SHOW database_mode;")
print(f"[兼容模式] {cur.fetchone()[0]}")
cur.execute("SELECT 1 AS hello;")
print(f"[SELECT 1] {cur.fetchone()['hello']}")
conn.close()
print("\n✓ 全部测试通过,可以开干了。")
if __name__ == "__main__":
main()
第 3 行的
sys.stdout重定向是为了解决 Windows 控制台 GBK 编码下中文报UnicodeEncodeError的问题------这是 Windows 上跑 Python 的老问题,跟金仓无关,但新手容易卡住。
运行结果:
python
→ 正在连接金仓数据库 ...
✓ 连接成功!
[版本] KingbaseES V009R001C010 ...
[当前库] test [当前用户] system
[兼容模式] mysql
[SELECT 1] 1
✓ 全部测试通过,可以开干了。

五、第二步:SQLAlchemy ORM 完整 CRUD
原生接口能跑通后,生产代码一般都上 ORM。下面这段脚本一次性演示建表、增、查、改、删、事务回滚六个场景,是写业务代码的最小骨架。


5.1 模型定义
python
from datetime import date
from typing import List
from sqlalchemy import (
create_engine, String, Integer, Numeric, Date, ForeignKey, select
)
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, relationship, Session, sessionmaker
)
class Base(DeclarativeBase):
pass
class Department(Base):
__tablename__ = "t_department"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)
location: Mapped[str] = mapped_column(String(100), nullable=True)
employees: Mapped[List["Employee"]] = relationship(back_populates="department")
class Employee(Base):
__tablename__ = "t_employee"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)
dept_id: Mapped[int] = mapped_column(ForeignKey("t_department.id"), nullable=False)
salary: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=0)
hire_date: Mapped[date] = mapped_column(Date, nullable=False)
department: Mapped["Department"] = relationship(back_populates="employees")
5.2 连接 + 坑 2 的 monkey-patch
python
from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
_orig_initialize = PGDialect_psycopg2.initialize
def _patched_initialize(self, connection):
try:
_orig_initialize(self, connection)
except AssertionError:
self.server_version_info = (9, 1, 0)
self.isolation_level = "READ COMMITTED"
PGDialect_psycopg2.initialize = _patched_initialize
# 注意:密码里的 @ 必须 URL-encode → Kingbase%402026
DB_URL = "postgresql+psycopg2://system:Kingbase%402026@localhost:54321/test"
engine = create_engine(DB_URL, echo=False, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False)
密码 URL 编码:
@→%40,#→%23,/→%2F。如果密码含特殊字符又没编码,会报password authentication failed。
5.3 增 / 查 / 改 / 删
python
def init_schema():
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
def seed_data(session: Session):
it_dept = Department(name="研发部", location="北京")
hr_dept = Department(name="人力资源部", location="上海")
sales_dept = Department(name="销售部", location="深圳")
session.add_all([it_dept, hr_dept, sales_dept])
session.flush()
session.add_all([
Employee(name="张三", dept_id=it_dept.id, salary=18000, hire_date=date(2022, 3, 1)),
Employee(name="李四", dept_id=it_dept.id, salary=22000, hire_date=date(2021, 7, 15)),
Employee(name="王五", dept_id=hr_dept.id, salary=12000, hire_date=date(2023, 2, 20)),
Employee(name="赵六", dept_id=sales_dept.id, salary=15000, hire_date=date(2022, 11, 1)),
Employee(name="钱七", dept_id=sales_dept.id, salary=17000, hire_date=date(2020, 5, 10)),
])
session.commit()
def demo_update(session: Session):
emp = session.execute(select(Employee).where(Employee.name == "张三")).scalar_one()
emp.salary += 2000 # 18000 → 20000
session.commit()
def demo_delete(session: Session):
emp = session.execute(select(Employee).where(Employee.name == "赵六")).scalar_one_or_none()
if emp:
session.delete(emp)
session.commit()
5.4 复杂查询:JOIN + 聚合
ORM 的真正价值在复杂查询。两个典型场景:
python
# 场景 A:薪资 > 15000 的员工(带部门名,JOIN)
stmt = (
select(Employee, Department)
.join(Department, Employee.dept_id == Department.id)
.where(Employee.salary > 15000)
.order_by(Employee.salary.desc())
)
for emp, dept in session.execute(stmt):
print(f" {emp.name:<6} {dept.name:<10} {emp.salary}")
输出:
李四 研发部 22000.00
张三 研发部 20000.00
钱七 销售部 17000.00
# 场景 B:各部门平均薪资(GROUP BY 聚合)
from sqlalchemy import func
stmt = (
select(
Department.name.label("dept"),
func.count(Employee.id).label("headcount"),
func.avg(Employee.salary).label("avg_salary"),
)
.join(Department, Employee.dept_id == Department.id)
.group_by(Department.name)
.order_by(Department.name)
)
for row in session.execute(stmt):
print(f" {row.dept:<12} 人数={row.headcount} 平均={float(row.avg_salary):.2f}")
输出:
人力资源部 人数=1 平均=12000.00
研发部 人数=2 平均=21000.00
销售部 人数=1 平均=17000.00

5.5 事务回滚(业务校验失败场景)
python
def demo_transaction(session: Session):
emp = session.execute(select(Employee).where(Employee.name == "李四")).scalar_one()
original_salary = emp.salary
print(f"改前: 李四薪资 = {original_salary}")
try:
emp.salary = 99999 # 1. 修改 ORM 对象
session.flush() # 2. 写到 DB(尚未提交)
raise ValueError("业务校验失败:薪资超出上限") # 3. 主动抛业务异常
except Exception:
session.rollback() # 4. 回滚
print("已回滚")
emp2 = session.execute(select(Employee).where(Employee.name == "李四")).scalar_one()
assert emp2.salary == original_salary
print(f"验证通过:当前薪资 = {emp2.salary}(与改前一致)")
输出:
python
改前: 李四薪资 = 22000.00
已回滚
验证通过:当前薪资 = 22000.00(与改前一致)
事务是否真的回滚,一定要在同一个 session 里重新 SELECT 验证 ------很多人只 rollback 不验证,结果数据库其实没回滚都不知道。

六、写在最后
三坑总结
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 1 | pip install kingbase8 失败 |
PyPI 上没有,官方包只支持老 Python | 用 psycopg2-binary |
| 2 | SQLAlchemy 报 Could not determine version |
金仓 version() 串非标准 PG 格式 | monkey-patch initialize |
| 3 | 'ABC' = 'abc' 返回 true |
MySQL 模式 = 运算符硬编码大小写不敏感 |
::bytea 字节级比较,或对比 md5 |
几点心得
- 国产数据库的内核选型决定生态可用性。金仓选了 PG 内核,意味着 PG 庞大的 Python 生态(psycopg2、SQLAlchemy、asyncpg、pandas......)几乎都能直接用,这是它在信创里相对好接入的原因。
- 版本探测类问题是国产适配的高频坑。同类问题在达梦、OceanBase、GaussDB 上都有。学会 monkey-patch 这一个套路,以后碰到类似的都能解决。
- 兼容模式的隐性行为差异要警惕 。看似一样的 SQL,在 MySQL / PG / ORACLE 兼容模式下行为可能完全不同(排序规则、隐式类型转换、日期解析都不一样)。从其他数据库迁移过来时,业务层一定要补一层回归测试,不能光靠 SQL 语法兼容就放心。
完整代码目录结构:
Python
kes-python-demo/
├── step1_jdbc/
│ └── 01_test_conn.py # 原生连接测试
└── step2_sqlalchemy/
└── _repro_bug2.py
└── 02_orm_crud.py
└── 03_query_demo.py # ORM 完整 CRUD
本文代码实测可用,欢迎留言交流。