Mingcedar

所属产品: Mingcedar CONTROL

Python 与 SQL:查询、写入及公共数据访问方法 | Mingcedar

先完成公共模块与离线依赖,再依次练习 SQLite、业务库连接、插入、查询和更新。PostgreSQL 与 MySQL 任选一条分支。

首次发布 最近更新

<div class="manual-doc-article manual-doc-article-section"><h2 id="python-sql">开发教程: Python 与 SQL:查询、写入及公共数据访问方法</h2>

先用标准库完成 SQL 基础练习,再使用 psycopg 或 PyMySQL 连接外部业务数据库,实现参数化查询、事务和重复请求核对。

<div class="doc-card-list doc-tutorial">

<article class="doc-card">

<h3 id="python-sql-item-1">准备与练习顺序</h3>

<p>先完成公共模块与离线依赖,再依次练习 SQLite、业务库连接、插入、查询和更新。PostgreSQL 与 MySQL 任选一条分支。</p>

<ul>

<li>示例表属于外部业务数据库,不是 Mingcedar 内部运行数据库;不要直接改写 tag_latest 或运行配置表。</li>

<li>Run Test 会执行脚本中的普通 Python 调用;SQL、HTTP 和邮件操作可能实际生效,请先连接独立的练习数据库或接口。</li>

<li>连接信息写入 common/business_config.py;密码通过当前项目的 Secret DATABASE_PASSWORD 读取。这里使用的是项目显式配置的环境,不依赖宿主机环境变量继承。</li>

</ul>

</article>

<article class="doc-card">

<h3 id="python-sql-item-2">1. 运行无需安装的 SQL 练习</h3>

<p>保存为 scripts/sql_basics.py,点击 Run Test。示例在内存库建表、插入、更新和查询,输出 [(&#39;measurement-001&#39;, 13.0)];执行后数据不保留。</p>

<ol>

<li>在代码页选择练习项目,在 scripts/ 新建 sql_basics。</li>

<li>复制完整代码并保存;选中 sql_basics.py,点击 Run Test。</li>

<li>观察输出后再运行一次,仍应只有一行。本示例每次建立新的内存数据库。</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>Python</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: Python">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/sql_basics.py — invocation-local SQL practice</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> sqlite3</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> sqlite3.connect(</span><span style="color:#032F62">":memory:"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"CREATE TABLE measurements (record_id TEXT PRIMARY KEY, value REAL NOT NULL)"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"INSERT INTO measurements VALUES (?, ?)"</span><span style="color:#24292E">, (</span><span style="color:#032F62">"measurement-001"</span><span style="color:#24292E">, </span><span style="color:#005CC5">12.5</span><span style="color:#24292E">))</span></span>

<span class="line"><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"UPDATE measurements SET value = ? WHERE record_id = ?"</span><span style="color:#24292E">, (</span><span style="color:#005CC5">13.0</span><span style="color:#24292E">, </span><span style="color:#032F62">"measurement-001"</span><span style="color:#24292E">))</span></span>

<span class="line"><span style="color:#24292E"> rows </span><span style="color:#D73A49">=</span><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"SELECT record_id, value FROM measurements WHERE record_id = ?"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (</span><span style="color:#032F62">"measurement-001"</span><span style="color:#24292E">,)).fetchall()</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(rows)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"rows"</span><span style="color:#24292E">: rows}</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 控制台为 [(&#39;measurement-001&#39;, 13.0)]:INSERT 写入 12.5,UPDATE 改为 13.0,SELECT 读回 13.0。</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-3">2. 准备外部业务表</h3>

<p>由维护人员在专用业务库建表并授予脚本账号所需权限。record_id 标识业务记录:同一记录沿用,新记录重新分配。</p>

<ol>

<li>由业务数据库管理员提供数据库地址、端口、库名、脚本账号和 CA 文件。确认这是外部业务库。</li>

<li>在该库的 SQL 管理工具中执行下方建表语句一次。不要粘贴到 Python 编辑器。</li>

<li>确认 integration_measurements 表存在,脚本账号可以 SELECT、INSERT、UPDATE 此表。</li>

<li>确定本次练习 record_id,后续插入、查询、更新使用同一个标识;不要把 &lt;record-id&gt; 原样写入库。</li>

</ol>

<div class="manual-code" data-code-language="sql"><div class="manual-code-toolbar"><span>SQL</span><span class="manual-code-tools"><small>SQL</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: SQL">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">CREATE</span><span style="color:#D73A49"> TABLE</span><span style="color:#6F42C1"> integration_measurements</span><span style="color:#24292E"> (</span></span>

<span class="line"><span style="color:#24292E"> record_id </span><span style="color:#D73A49">VARCHAR</span><span style="color:#24292E">(</span><span style="color:#005CC5">128</span><span style="color:#24292E">) </span><span style="color:#D73A49">PRIMARY KEY</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> tag_ref </span><span style="color:#D73A49">VARCHAR</span><span style="color:#24292E">(</span><span style="color:#005CC5">512</span><span style="color:#24292E">) </span><span style="color:#D73A49">NOT NULL</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#D73A49"> value</span><span style="color:#D73A49"> DOUBLE PRECISION</span><span style="color:#D73A49"> NOT NULL</span></span>

<span class="line"><span style="color:#24292E">);</span></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 业务表存在且三列类型与示例一致。当前步骤尚未写入业务记录。</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-6">3. 配置数据库地址、证书与密码</h3>

<p>使用 Settings → Python → Python 模块 → 高级项目配置中的 Secrets 保存数据库密码。下方配置通过 os.environ 读取这个明确配置的 Secret;不会自动获得操作系统登录用户的密码。</p>

<ol>

<li>选择当前项目,展开 Secrets,在名称填 DATABASE_PASSWORD,在值中填写数据库管理员提供的密码,点击“创建 Secret(Create Secret)”。</li>

<li>确认 Secret 列表出现 DATABASE_PASSWORD。值提交后不回显;不要把密码放入普通环境变量或日志。</li>

<li>点击“保存并应用配置”,让新的运行版本使用该配置修订。仅创建或替换 Secret 不会改变已发布的旧版本。</li>

<li>回到代码页,在 common/ 创建 business_config,复制下方模板;只保留所选数据库对应配置。</li>

<li>替换 host、库名、user 和 CA 路径。PostgreSQL 通常使用 5432,MySQL 通常使用 3306,以实际部署为准。容器中的 CA 路径必须在容器内可读。</li>

<li>保存该文件。密码由 DATABASE_PASSWORD 获取,代码中无需再填写密码。</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/business_config.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: common/business_config.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># common/business_config.py — configuration template, no real credentials</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> os</span></span>

<span class="line"></span>

<span class="line"><span style="color:#005CC5">POSTGRES_CONFIG</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> {</span></span>

<span class="line"><span style="color:#032F62"> "host"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-host>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"port"</span><span style="color:#24292E">: </span><span style="color:#005CC5">5432</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "dbname"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-name>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"user"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-user>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "password"</span><span style="color:#24292E">: os.environ[</span><span style="color:#032F62">"DATABASE_PASSWORD"</span><span style="color:#24292E">], </span><span style="color:#032F62">"sslmode"</span><span style="color:#24292E">: </span><span style="color:#032F62">"verify-full"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "sslrootcert"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;ca-file-path>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E">}</span></span>

<span class="line"><span style="color:#005CC5">MYSQL_CONFIG</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> {</span></span>

<span class="line"><span style="color:#032F62"> "host"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-host>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"port"</span><span style="color:#24292E">: </span><span style="color:#005CC5">3306</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "database"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-name>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"user"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-user>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "password"</span><span style="color:#24292E">: os.environ[</span><span style="color:#032F62">"DATABASE_PASSWORD"</span><span style="color:#24292E">], </span><span style="color:#032F62">"charset"</span><span style="color:#24292E">: </span><span style="color:#032F62">"utf8mb4"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "ssl_ca"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;ca-file-path>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"ssl_verify_cert"</span><span style="color:#24292E">: </span><span style="color:#005CC5">True</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "ssl_verify_identity"</span><span style="color:#24292E">: </span><span style="color:#005CC5">True</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E">}</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> Secret 可见、占位符已替换、配置已应用。先以只读查询验证连接,再插入。</p>

<div class="doc-step-troubleshooting"><p><strong>如果结果不一致</strong></p><ul>

<li>KeyError: DATABASE_PASSWORD:回到所选项目确认 Secret 名称拼写、保存结果和当前使用的配置版本。</li>

<li>证书错误:核对 host 与证书主机名、CA 文件路径及读取权限;保持证书校验开启。</li>

</ul></div>

</article>

<article class="doc-card">

<h3 id="python-sql-item-4">4A. 保存 PostgreSQL 写入方法</h3>

<p>保存为 common/postgres_store.py。config 使用 host、port、dbname、user、password,并按部署要求提供 sslmode/sslrootcert。with 块正常结束提交事务,发生异常回滚并关闭连接;参数通过独立元组传入 SQL。</p>

<ol>

<li>先运行 Python 教程的依赖检查,确认 psycopg 可导入。</li>

<li>在 common/ 新建 postgres_store,复制下方完整方法,点击保存。</li>

<li>该文件只保存数据库方法,不需要单独运行;后续脚本通过 import 调用。</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/postgres_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: common/postgres_store.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># common/postgres_store.py</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> psycopg</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> save_measurement</span><span style="color:#24292E">(config, record_id, tag_ref, value):</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> psycopg.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> options</span><span style="color:#D73A49">=</span><span style="color:#032F62">"-c statement_timeout=5000"</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "INSERT INTO integration_measurements (record_id, tag_ref, value) "</span></span>

<span class="line"><span style="color:#032F62"> "VALUES (</span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">) ON CONFLICT (record_id) DO NOTHING"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id, tag_ref, value),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT tag_ref, value FROM integration_measurements WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id,),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> stored </span><span style="color:#D73A49">=</span><span style="color:#24292E"> cursor.fetchone()</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> stored </span><span style="color:#D73A49">!=</span><span style="color:#24292E"> (tag_ref, value):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record ID already exists with a different payload"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"tag_ref"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">0</span><span style="color:#24292E">], </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">1</span><span style="color:#24292E">]}</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 后续入口须使用 PostgreSQL 分支的模块名和配置常量。</p>

</article>

<article class="doc-card">

<h3 id="sql-postgres-query-update">4A(续). 查询与更新方法</h3>

<p>选择 PostgreSQL 时执行本节。把代码追加到 common/postgres_store.py 的现有 save_measurement 函数之后,保留文件顶部 import psycopg。</p>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/postgres_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: common/postgres_store.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Append to common/postgres_store.py; psycopg is imported above.</span></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> list_measurements</span><span style="color:#24292E">(config, after</span><span style="color:#D73A49">=</span><span style="color:#032F62">""</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> psycopg.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> options</span><span style="color:#D73A49">=</span><span style="color:#032F62">"-c statement_timeout=5000"</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id, tag_ref, value FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id > </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> ORDER BY record_id LIMIT 100"</span><span style="color:#24292E">, (after,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> cursor.fetchall()</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> update_measurement</span><span style="color:#24292E">(config, record_id, value):</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> psycopg.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> options</span><span style="color:#D73A49">=</span><span style="color:#032F62">"-c statement_timeout=5000"</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> FOR UPDATE"</span><span style="color:#24292E">, (record_id,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> cursor.fetchone() </span><span style="color:#D73A49">is</span><span style="color:#005CC5"> None</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record does not exist"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "UPDATE integration_measurements SET value = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (value, record_id),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: value}</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 同一文件现在包含 save_measurement、list_measurements、update_measurement 三个函数。</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-5">4B. 保存 MySQL 写入方法</h3>

<p>保存为 common/mysql_store.py。config 使用 host、port、database、user、password、charset=&quot;utf8mb4&quot;,TLS 参数按数据库要求设置。代码显式提交、异常回滚并关闭连接,使用默认元组游标。</p>

<ol>

<li>先运行 Python 教程的依赖检查,确认 pymysql 可导入。</li>

<li>在 common/ 新建 mysql_store,复制下方完整方法,点击保存。</li>

<li>该文件只保存数据库方法,不需要单独运行;后续脚本通过 import 调用。</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/mysql_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: common/mysql_store.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># common/mysql_store.py</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> pymysql</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> save_measurement</span><span style="color:#24292E">(config, record_id, tag_ref, value):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> pymysql.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">read_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> write_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">autocommit</span><span style="color:#D73A49">=</span><span style="color:#005CC5">False</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "INSERT INTO integration_measurements (record_id, tag_ref, value) "</span></span>

<span class="line"><span style="color:#032F62"> "VALUES (</span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">) ON DUPLICATE KEY UPDATE record_id = record_id"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id, tag_ref, value),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT tag_ref, value FROM integration_measurements WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id,),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> stored </span><span style="color:#D73A49">=</span><span style="color:#24292E"> cursor.fetchone()</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> stored </span><span style="color:#D73A49">!=</span><span style="color:#24292E"> (tag_ref, value):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record ID already exists with a different payload"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> connection.commit()</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"tag_ref"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">0</span><span style="color:#24292E">], </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">1</span><span style="color:#24292E">]}</span></span>

<span class="line"><span style="color:#D73A49"> except</span><span style="color:#005CC5"> Exception</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.rollback()</span></span>

<span class="line"><span style="color:#D73A49"> raise</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 后续入口须使用 MySQL 分支的模块名和配置常量。</p>

</article>

<article class="doc-card">

<h3 id="sql-mysql-query-update">4B(续). 查询与更新方法</h3>

<p>选择 MySQL/MariaDB 时执行本节。把代码追加到 common/mysql_store.py 的现有 save_measurement 函数之后,保留 import pymysql。查询方法始终关闭连接;更新失败回滚。</p>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/mysql_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: common/mysql_store.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Append to common/mysql_store.py; pymysql is imported above.</span></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> list_measurements</span><span style="color:#24292E">(config, after</span><span style="color:#D73A49">=</span><span style="color:#032F62">""</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> pymysql.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> read_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">write_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">autocommit</span><span style="color:#D73A49">=</span><span style="color:#005CC5">False</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id, tag_ref, value FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id > </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> ORDER BY record_id LIMIT 100"</span><span style="color:#24292E">, (after,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> cursor.fetchall()</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> update_measurement</span><span style="color:#24292E">(config, record_id, value):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> pymysql.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> read_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">write_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">autocommit</span><span style="color:#D73A49">=</span><span style="color:#005CC5">False</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> FOR UPDATE"</span><span style="color:#24292E">, (record_id,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> cursor.fetchone() </span><span style="color:#D73A49">is</span><span style="color:#005CC5"> None</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record does not exist"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "UPDATE integration_measurements SET value = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (value, record_id),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> connection.commit()</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: value}</span></span>

<span class="line"><span style="color:#D73A49"> except</span><span style="color:#005CC5"> Exception</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.rollback()</span></span>

<span class="line"><span style="color:#D73A49"> raise</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 同一文件现在包含 save_measurement、list_measurements、update_measurement 三个函数。</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-9">5. 先运行只读查询</h3>

<p>新建 scripts/read_measurements.py,复制代码并按数据库调整两条 import,保存后运行 Run Test。空表返回 [] 也表示连接成功。</p>

<ol>

<li>PostgreSQL 保持 postgres_store 和 POSTGRES_CONFIG;MySQL 同时改成 mysql_store 和 MYSQL_CONFIG。</li>

<li>检查返回 rows。每页最多 100 行,按 record_id 排序。</li>

<li>读取下一页时,把入口中的 after 从空字符串改为上一页返回的 next_after,再运行;空页表示结束。此顺序不表示采集时间顺序。</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>scripts/read_measurements.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: scripts/read_measurements.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/read_measurements.py</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.postgres_store </span><span style="color:#D73A49">import</span><span style="color:#24292E"> list_measurements</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.business_config </span><span style="color:#D73A49">import</span><span style="color:#005CC5"> POSTGRES_CONFIG</span><span style="color:#D73A49"> as</span><span style="color:#005CC5"> DATABASE_CONFIG</span></span>

<span class="line"><span style="color:#6A737D"># For MySQL, change postgres_store to mysql_store and POSTGRES_CONFIG to MYSQL_CONFIG.</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#24292E"> rows </span><span style="color:#D73A49">=</span><span style="color:#24292E"> list_measurements(</span><span style="color:#005CC5">DATABASE_CONFIG</span><span style="color:#24292E">, ctx.get(</span><span style="color:#032F62">"after"</span><span style="color:#24292E">, </span><span style="color:#032F62">""</span><span style="color:#24292E">))</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(rows)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"rows"</span><span style="color:#24292E">: rows, </span><span style="color:#032F62">"next_after"</span><span style="color:#24292E">: rows[</span><span style="color:#D73A49">-</span><span style="color:#005CC5">1</span><span style="color:#24292E">][</span><span style="color:#005CC5">0</span><span style="color:#24292E">] </span><span style="color:#D73A49">if</span><span style="color:#24292E"> rows </span><span style="color:#D73A49">else</span><span style="color:#005CC5"> None</span><span style="color:#24292E">}</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({</span><span style="color:#032F62">"after"</span><span style="color:#24292E">: </span><span style="color:#032F62">""</span><span style="color:#24292E">})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 得到 [] 或已有记录列表,且无连接、认证或表权限错误。</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-7">6. 插入一条业务记录</h3>

<p>保存为 scripts/store_measurement.py,替换业务标识、TagRef 和数据库分支的两条 import。点击 Run Test 会向所配置的数据库写入一条 value=12.5 的记录,请先确认目标是本次练习使用的业务表。事件调用 main(ctx) 时传入 record_id、tag_ref、value。</p>

<ol>

<li>在 scripts/ 创建 store_measurement,复制下方完整代码,并保持数据库分支与查询脚本一致。</li>

<li>替换入口底部的 &lt;record-id&gt; 和 tag_ref;value 保持 12.5。</li>

<li>选中 store_measurement.py,通过 Run Test 执行一次。</li>

<li>重新运行 read_measurements.py,找到相同 record_id,核对 tag_ref 和 value=12.5。</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>scripts/store_measurement.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: scripts/store_measurement.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/store_measurement.py</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.postgres_store </span><span style="color:#D73A49">import</span><span style="color:#24292E"> save_measurement</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.business_config </span><span style="color:#D73A49">import</span><span style="color:#005CC5"> POSTGRES_CONFIG</span><span style="color:#D73A49"> as</span><span style="color:#005CC5"> DATABASE_CONFIG</span></span>

<span class="line"><span style="color:#6A737D"># For MySQL, use mysql_store and MYSQL_CONFIG instead.</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> save_measurement(</span></span>

<span class="line"><span style="color:#005CC5"> DATABASE_CONFIG</span><span style="color:#24292E">, ctx[</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">], ctx[</span><span style="color:#032F62">"tag_ref"</span><span style="color:#24292E">], </span><span style="color:#005CC5">float</span><span style="color:#24292E">(ctx[</span><span style="color:#032F62">"value"</span><span style="color:#24292E">])</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;record-id>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "tag_ref"</span><span style="color:#24292E">: </span><span style="color:#032F62">"hq://io/&#x3C;io-domain-id>/&#x3C;device-uid>/&#x3C;tag-uid>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: </span><span style="color:#005CC5">12.5</span><span style="color:#24292E">})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 查询脚本能读回新记录。再次插入相同标识及相同内容不会新增一行;相同标识但不同内容会报错。</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-8">7. 更新记录并重新查询</h3>

<p>新建 scripts/update_measurement.py。本例明确把上一节记录改为 13.0;UPDATE 与重复 INSERT 有不同的业务含义。</p>

<ol>

<li>按数据库分支调整两条 import。</li>

<li>把 &lt;record-id&gt; 改为刚写入的标识,保存并运行一次。</li>

<li>重新运行只读查询,核对该记录 value=13.0,记录总数未增加。</li>

<li>再使用一个不存在的标识验证更新,预期 Record does not exist;不要把该错误转换成成功。</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>scripts/update_measurement.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="已复制" data-failed="请选中代码后复制" aria-label="复制代码: scripts/update_measurement.py">复制代码</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/update_measurement.py</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.postgres_store </span><span style="color:#D73A49">import</span><span style="color:#24292E"> update_measurement</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.business_config </span><span style="color:#D73A49">import</span><span style="color:#005CC5"> POSTGRES_CONFIG</span><span style="color:#D73A49"> as</span><span style="color:#005CC5"> DATABASE_CONFIG</span></span>

<span class="line"><span style="color:#6A737D"># For MySQL, change postgres_store to mysql_store and POSTGRES_CONFIG to MYSQL_CONFIG.</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#24292E"> result </span><span style="color:#D73A49">=</span><span style="color:#24292E"> update_measurement(</span><span style="color:#005CC5">DATABASE_CONFIG</span><span style="color:#24292E">, ctx[</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">], </span><span style="color:#005CC5">float</span><span style="color:#24292E">(ctx[</span><span style="color:#032F62">"value"</span><span style="color:#24292E">]))</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(result)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> result</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;record-id>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: </span><span style="color:#005CC5">13.0</span><span style="color:#24292E">})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>完成标志</strong> 已有行被更新;不存在的行报错并回滚。修改后的记录再以旧 value=12.5 调用插入方法会被识别为内容冲突。</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-10">数据库连接与运行问题</h3>

<p>依次验证连通、认证、TLS、表权限、参数化 SQL 和事务,再接入周期任务。不要将数据库异常直接替换成成功结果。</p>

<ul>

<li>连接失败:从实际 Script 服务或容器所在网络检查地址、端口、DNS、证书及账号;不要关闭 TLS 校验来绕过错误。</li>

<li>SQL 类型错误:核对列类型和 Python 值;日期使用明确时区,不把字符串拼进 SQL。</li>

<li>执行或提交超时:连接丢失时事务可能已经提交;按 record_id 只读查询业务库后再决定下一步,不盲目重试。</li>

<li>确认重复业务请求不会产生重复记录;确认异常会保留失败状态,连接会关闭。</li>

<li>事务只覆盖本数据库。一次 PLC 写入与一次 SQL 提交不构成跨系统原子事务,需用业务状态和核对流程处理。</li>

</ul>

</article>

</div>

</div>