登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python sqlite3 怎样用 URI 参数打开只读连接

来源:17golang原创

时间:2026-10-20 01:53:10 159浏览 收藏

如果程序只是读取一份已经准备好的 SQLite 数据库,普通的 sqlite3.connect("data/app.db") 并不能表达“文件必须存在、连接不能写入”这两个要求。SQLite 默认可能创建缺失文件,排查配置错误时就容易把一个空库当成成功连接。

更明确的写法是使用 URI 参数:sqlite3.connect("file:data/app.db?mode=ro", uri=True)。其中 file: 表示 SQLite URI,mode=ro 表示只读打开,uri=True 告诉 Python 把第一个参数按 URI 解释。文件不存在时连接应直接失败,连接建立后执行写操作也会被拒绝。

官方文档:https://docs.python.org/3/library/sqlite3.html

先记住这三个判断
  • uri=True 不是只读开关;没有它,mode=ro 只是普通字符串的一部分。
  • mode=ro 要求目标数据库已经存在,不会因为路径写错而悄悄创建新库。
  • 只读连接仍然可以执行 SELECT;真正的 INSERT、UPDATE 或 DDL 会在写入阶段失败。

先把只读连接拆成三个条件

这个问题常常不是 Python 语法错了,而是三个层次混在了一起。file: 是 SQLite URI 的方案前缀,负责让后面的路径和查询参数组成一个 URI;mode=ro 是 SQLite 的打开模式,负责要求只读;uri=True 是 Python sqlite3.connect() 的参数,负责启用 URI 解析。

因此,下面两种写法的含义不同:

import sqlite3

# 这里明确启用 URI 解析;mode=ro 才会被 SQLite 当作只读参数。
conn = sqlite3.connect("file:data/app.db?mode=ro", uri=True)
conn.close()  # 连接用完后主动释放文件句柄。
import sqlite3

# 没有 uri=True 时,不要把整个字符串误认为已经启用了只读模式。
conn = sqlite3.connect("file:data/app.db?mode=ro")
conn.close()  # 这不是本文要求的 URI 只读连接。
Python sqlite3 URI 只读连接中 connect、uri=True、mode=ro 与 SQLite 文件和异常的静态关系图
图1:把 Python 连接函数、URI 解析、只读参数、已有文件和异常边界放在同一张静态结构说明图中;它不是运行截图。

用 mode=ro 打开已经存在的数据库

最小可用代码可以先固定一个相对路径:

from pathlib import Path
import sqlite3

db_uri = "file:data/app.db?mode=ro"
conn = None
try:
    # uri=True 让 SQLite 读取 mode=ro,而不是把它当成普通文件名。
    conn = sqlite3.connect(db_uri, uri=True)
    rows = conn.execute("SELECT id, name FROM users ORDER BY id").fetchall()
    for row in rows:
        print(row)
except sqlite3.OperationalError as exc:
    # 连接阶段或查询阶段的 SQLite 错误都保留原始信息,便于定位路径和权限问题。
    raise RuntimeError(f"只读数据库访问失败: {exc}") from exc
finally:
    if conn is not None:
        conn.close()  # 只读不等于不占用句柄,连接仍然需要关闭。

示例中的 Path 只是提醒路径应由配置决定;真正传给 connect() 的值仍是带 file: 前缀和查询串的 URI。读取失败时保留原始 OperationalError,比只输出“数据库不可用”更容易区分文件路径、权限和 SQL 问题。

文件不存在时,为什么应该在连接阶段失败

普通路径连接的一个常见误区是:程序启动后看到一个新的 app.db,就以为部署成功。实际上,路径拼写错误、挂载目录没有准备好、工作目录改变,都可能让程序打开了一个刚创建的空文件。

mode=ro 把这个边界前移到连接阶段。目标不存在时,SQLite 不应创建数据库,而是返回打开失败的 OperationalError。这正是只读数据目录适合使用 URI 的原因:缺文件会暴露出来,而不会被“自动初始化”掩盖。

import sqlite3

def open_existing_readonly(path: str) -> sqlite3.Connection:
    # 只读连接要求部署先准备好数据库文件;不会替缺失文件创建空库。
    uri = f"file:{path}?mode=ro"
    return sqlite3.connect(uri, uri=True)

try:
    conn = open_existing_readonly("data/app.db")
except sqlite3.OperationalError as exc:
    # 这里优先检查工作目录、挂载路径和文件权限,而不是先重试查询。
    raise RuntimeError(f"无法打开既有 SQLite 文件: {exc}") from exc
else:
    conn.close()  # 成功建立连接后也要释放资源。

这段代码适合表达“文件必须由部署步骤准备好”的约束。若路径来自用户输入或包含 ?、# 等 URI 保留字符,不能简单拼接字符串,应先按照 SQLite URI 规则对路径部分进行编码,并把查询参数放在编码后的路径之后。

写入被拒绝,不代表读取失败

只读连接的另一个排查现场是:SELECT 正常,但第一次写入时报 attempt to write a readonly database 一类错误。这通常说明 URI 已经按预期生效,问题不在连接建立,而在代码越过了只读边界。

import sqlite3

conn = sqlite3.connect("file:data/app.db?mode=ro", uri=True)
try:
    # 查询属于只读连接允许的操作。
    row = conn.execute("SELECT count(*) FROM users").fetchone()
    print("当前记录数:", row[0])

    # 这个写操作应被 SQLite 拒绝,用来暴露误写路径。
    conn.execute("INSERT INTO users(name) VALUES (?)", ("demo",))
except sqlite3.OperationalError as exc:
    # 生产代码应记录 SQL 场景,但不要吞掉只读约束错误。
    raise RuntimeError(f"只读连接拒绝写入: {exc}") from exc
finally:
    conn.close()  # 无论查询还是写入失败,都关闭连接。

这里不要把只读 URI 改成普通路径来“修复”错误。先确认调用方是否本来就应该写入;如果需要写入,应把写库职责放到拥有写权限的连接或服务中,而不是让一个只读数据源偷偷承担写操作。

相对路径、绝对路径和工作目录要分开判断

URI 只读模式能约束打开方式,但不能替你决定相对路径从哪里开始解析。file:data/app.db?mode=ro 依赖当前工作目录;同一个服务在本地启动、进程管理器启动和容器启动时,工作目录可能不同。

排查时可把路径解析和 URI 拼装分开记录:

from pathlib import Path
from urllib.parse import quote
import sqlite3

db_path = Path("data/app.db").resolve()
# quote 只处理路径部分,保留斜杠,避免特殊字符被误当成 URI 查询语法。
encoded_path = quote(str(db_path), safe="/:\\")
db_uri = f"file:{encoded_path}?mode=ro"

conn = sqlite3.connect(db_uri, uri=True)
try:
    conn.execute("SELECT 1").fetchone()
finally:
    conn.close()  # 只做探测也要及时关闭连接。

这个版本适合配置系统已经给出绝对路径的场景。Windows 路径、网络挂载和包含特殊字符的路径应在目标运行环境中按 SQLite URI 文档单独确认;不要因为本机相对路径可用,就假设服务部署目录也一致。

SQLite URI 只读模式中相对路径、绝对路径、file 方案、mode=ro、已有文件、查询和写入拒绝的静态关系图
图2:路径表达、URI 解析、已有文件、读取和写入拒绝之间的静态边界说明图,不代表某次真实运行结果。

上线前按这张清单复查

检查点应该看到的结果异常时先看哪里
URI 形式以 file: 开头,并带 mode=ro配置拼接和路径编码
Python 参数sqlite3.connect(..., uri=True)是否漏传 uri=True
文件存在性缺失文件在连接阶段失败工作目录、挂载点、部署产物
查询行为SELECT 可以执行文件权限、数据库损坏、SQL
写入行为写操作被明确拒绝调用方是否误用了只读连接
资源释放所有分支最终调用 close()异常处理和连接生命周期

归纳起来,Python sqlite3 的 URI 只读连接写法就是 file:数据库路径?mode=ro 配合 uri=True。先用它保证“文件必须存在”,再用查询成功和写入拒绝分别确认读取路径与只读边界,最后把绝对路径解析和连接关闭纳入部署代码。

常见问题

只传 mode=ro,不传 uri=True 可以吗?

不建议这样做。uri=True 是 Python 端启用 URI 解析的开关;缺少它时,查询串不会按本文需要的 SQLite URI 参数处理。

只读连接能执行事务吗?

可以执行不改变数据的查询,但事务语义不能把写入权限变出来。是否显式开启事务应服从应用的查询模式,写操作仍会被只读数据库拒绝。

文件不存在时想自动创建怎么办?

那是初始化或写入职责,不应和只读连接混在一起。先由部署或初始化流程创建并准备数据库,再用 mode=ro 提供读取连接,能更早暴露环境缺失。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>