# Database

> 把工厂系统保存在数据库表中的值（SQL Server、PostgreSQL、MySQL 和 MariaDB、SQLite，或通过 ODBC 访问的任何数据库）读成点位，并在点位要求的地方写回：“数据库”和“登录”卡片，按表、列和行读取单元格或执行自定义查询的点位，点位写入的两种方式，为每一列提供一个点位的扫描，四个完整示例，以及驱动不做的事。

Database 驱动读取工厂系统保存在数据库表中的值：MES 保存的生产订单，ERP 保存的配方或设定值，另一个测试系统记录的结果。每个点位读取一个表中的一个值，它的值和其他任何点位的值一样送达各处：Logic、仪表画面，以及 [Process](process) 测试台的运行记录。在点位要求时，驱动也会写回：写入点位所读取的单元格，或通过点位自己的命令写入。

SQL Server、PostgreSQL、MySQL 和 MariaDB 以及 SQLite 无需在计算机上安装任何东西即可连接。其他数据库（Oracle、Access、Firebird 等）通过它们自己的 ODBC 驱动连接，该驱动的 64 位版本必须安装在计算机上。

读取是主要用途，路径很短：设备说明其数据库在哪里以及工作站如何登录，扫描为其各表的每一列提供一个点位，每个点位新建时都是只读。属于写入的内容，只有在点位的访问方式允许工作站写入之后才会出现在该点位上。

此驱动的一台设备就是一个数据库：同一服务器上的两个数据库就是两台设备。设备位于驱动之下，可以归入文件夹，**驱动 → 文件夹 → 设备 → 点位**，和任何没有串行线路或 Broker 的驱动一样。

## 驱动页面 {#the-drivers-page}

标题下面的一行写着“每台设备读取一个数据库。添加设备，填写其数据库的位置和工作站的登录方式，然后扫描它，把其中的列添加为点位。”有配置 Connector 的权限时，页面上有此驱动自己的扫描范围卡片，其中是扫描要读取其数据库的设备（参见[设备发现](#discovery)）。下面是和任何驱动一样的**设备**表（参见[设备表](connector-drivers#the-device-table)），用标题右侧的**添加设备**增加设备；表为空时显示“还没有设备。”驱动的工具栏上有**发现设备**，在扫描范围卡片填写完整之前是灰色的。此驱动列在**文件和数据库**之下，位于 [File](driver-file) 驱动旁边。

## 设备 {#device}

此驱动的设备没有“连接”卡片：取而代之的是**数据库**卡片，除了没有登录的 SQLite 之外，其后还有**登录**卡片。“设备”卡片的**轮询间隔（ms）**默认为 1000，表示点位读取数据库的频率，和任何驱动一样，每个点位都可以覆盖它。

新设备是一台没有服务器的 SQL Server：它显示“尚未填写服务器”，其页面显示“在这台设备说明其数据库的位置之前，工作站不会连接它，因此暂时不读取任何数据，也没有错误可显示。在下面的‘数据库’卡片里填写（服务器和数据库名称、文件或数据源），工作站就会连接。”在说明位置之前不读取任何内容。标题中显示的基础 URI 是运行时地址，从不包含用户或密码：SQL Server 实例为 `database://sqlserver?server=mes-sql&instance=SQLEXPRESS&database=MES`，SQLite 文件为 `database://sqlite?file=C%3A%5CData%5Crecipes.db`，ODBC 数据源为 `database://odbc?source=ERP`。

### “数据库”卡片 {#the-database-card}

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **数据库类型** | 始终 | **SQL Server**、**PostgreSQL**、**MySQL / MariaDB**、**SQLite** 或 **其他（ODBC）**，后者显示“使用此计算机上为该数据库安装的 64 位 ODBC 驱动（Oracle、Access、Firebird 等）。” | **SQL Server** |
| **服务器** | SQL Server、PostgreSQL、MySQL / MariaDB | 在 SQL Server 上：“主机名或 IP 地址。SERVER\INSTANCE 和 SERVER,PORT 会拆分到各自的框中。”在另外两种上：“主机名或 IP 地址。冒号后写的端口会移到‘端口’框。” | 空 |
| **端口** | 同上 | 留空即数据库自己的端口，空框中会显示该端口：1433、5432 或 3306。SQL Server 实例没有端口时，框中不显示任何内容，由服务器的 SQL Server Browser 告知端口。 | 空 |
| **实例名称** | SQL Server | “默认实例留空。没有端口的命名实例通过 SQL Server Browser 查找。”最多 16 个字母、数字、`_` 和 `$`，以字母或 `_` 开头。 | 空 |
| **数据库名称** | SQL Server、PostgreSQL、MySQL / MariaDB | 服务器上存放这些表的数据库，最多 128 个字符。 | 空 |
| **数据库文件** | SQLite | “SQLite 文件的完整路径，使用运行工作站的账户的 Windows 权限访问。除非有点位写入，否则以只读方式打开。”框中以 `C:\Data\recipes.db` 作为提示。从不创建该文件。 | 空 |
| **数据源或连接字符串** | 其他（ODBC） | “Windows‘ODBC 数据源（64 位）’中的数据源名称，或类似 Driver={...};Dbq=C:\Data\plant.accdb 的连接字符串。密码填在密码框里，绝不写进连接字符串。” | 空 |

所选类型不读取的字段不会显示，并保留其值以备将来读取。选择另一种类型时，仍停留在旧数据库自身端口的端口会被清空，这样新数据库就在它自己的端口上连接。“服务器”框会把数据库工具写在一起的内容拆开：

| 在“服务器”中输入 | 结果 |
| --- | --- |
| `mes-sql\SQLEXPRESS`（SQL Server） | 服务器 `mes-sql`，实例名称 `SQLEXPRESS` |
| `mes-sql,1433`（SQL Server） | 服务器 `mes-sql`，端口 1433 |
| `10.0.0.5:5433` | 服务器 `10.0.0.5`，端口 5433 |

在卡片说明数据库的位置之前，设备按原样保存并等待，灰色圆点，不读取任何内容，也不显示错误：

| 数据库类型 | 等待 | 设备显示 |
| --- | --- | --- |
| SQL Server、PostgreSQL、MySQL / MariaDB | 服务器，然后是数据库名称 | “尚未填写服务器”，然后是“尚未填写数据库名称” |
| SQLite | 文件 | “尚未填写数据库文件” |
| 其他（ODBC） | 数据源或连接字符串 | “尚未填写数据源” |

卡片无法接受的设置会显示在卡片下方，修正之前设备不会保存：

| 您输入 | 卡片显示 |
| --- | --- |
| 含协议前缀、路径或空格的服务器，例如 `http://mes-sql` | “服务器只填主机名或 IP 地址：不含协议前缀、路径或空格。与之一起写的 SQL Server 实例和端口会移到各自的框中。” |
| 含空格或 `_`、`$` 以外符号的实例名称 | “实例名称最多 16 个字符，可用字母、数字、下划线和美元符号。” |
| 超过 128 个字符的数据库名称 | “数据库名称最多 128 个字符，不含控制字符。” |
| 不是完整路径的文件，例如 `recipes.db` | “数据库文件是完整路径：驱动器、文件夹和文件（C:\Data\recipes.db），或网络共享（\\server\data\recipes.db）。” |
| 文件夹，或含 `*`、`?`、`<`、`>` 或引号的路径 | “请填写数据库文件本身的路径，而不是文件夹，不含 * ? \| < > 或引号。” |
| 含方括号、大括号、圆括号、逗号、分号或 `? * ! @ \` 之一的数据源名称 | 卡片要求填写该数据源在 ODBC 数据源中的名称，或连接字符串。 |
| 不是 `key=value` 对的连接字符串，例如留着未闭合的大括号 | “连接字符串是以分号分隔的 key=value 对；含分号的值要放在大括号中。” |
| 含 `PWD=` 或 `Password=` 的连接字符串 | “请把密码从连接字符串中拿出，填到密码框里：连接字符串会显示并写入诊断日志，而密码框会加以保护。” |

保存遵循同样的规则，拒绝时动态栏会说明：“设备‘MES’有驱动无法使用的数据库设置。请打开它并修正面板标出的字段。”

### “登录”卡片 {#the-sign-in-card}

除 SQLite 之外的每种数据库都有**登录**卡片，其中是工作站登录所用的用户。

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **使用工作站的 Windows 账户登录** | SQL Server | 打开：“使用运行工作站的 Windows 账户登录 SQL Server；不保存用户或密码。”关闭：“使用数据库用户及其密码登录 SQL Server。” | 关闭 |
| **用户名** | 用用户登录的服务器，以及可选的 ODBC | 数据库用户。 | 空 |
| **密码** | 同上 | 用户的密码，和驱动的其他凭据一样受保护地保存：不再显示，不写入地址或诊断日志，也不返回给智能体。 | 空 |
| **加密** | SQL Server | **加密，校验证书**：“连接经过加密，服务器证书会与此计算机信任的证书进行校验。”**加密，接受任何证书**：“连接经过加密，并接受任何证书，这是使用自有证书的工厂服务器所需要的。不校验服务器的身份。”**不加密**：“值和命令以明文传输。仅用于不提供加密的旧服务器。” | **加密，校验证书** |

PostgreSQL 和 MySQL 没有“加密”字段：只要服务器提供，它们的连接就会加密。使用自有证书的 SQL Server（工厂服务器常常如此）会以数据库自己的话拒绝连接，说证书链由不受信任的机构颁发：这种服务器需要选择**加密，接受任何证书**。

在字段下方，卡片说明应当申请什么样的用户：“请向数据库管理员申请一个只具备工作站所需权限的用户：读取权限，以及仅对其点位修改的表的写入权限。”工作站以该用户能做的一切去读写另一个系统的数据，因此这个用户才是工作站能修改内容的真正边界。每次写入都会记入诊断日志，和任何驱动一样。

用用户登录的服务器尚未填写用户时，会改为显示“请填写工作站登录所用的用户。在此之前，工作站不会连接这台设备。”这不是拒绝：设备会保存，在填写用户之前，它的错误会说明尚未填写数据库用户。

### 在线、等待和离线 {#online-waiting-and-offline}

| 情况 | 设备及其点位的行为 |
| --- | --- |
| “数据库”卡片没有说明数据库的位置 | 设备等待，灰色圆点，不读取任何内容，也没有错误（见上文）。 |
| 用用户登录的服务器，且未填写用户 | 工作站不连接该设备；其错误说明尚未填写数据库用户。 |
| 服务器没有响应、拒绝登录，或 SQLite 文件不存在 | 设备离线，并附上数据库自己给出的原因，工作站在可以时立即连接。连接最多等待 10 s。 |
| 连接断开（服务器重启、网络中断） | 设备离线，然后重新连接。 |
| 数据库拒绝的语句，或 10 s 后仍在运行的语句 | 该点位读取失败，并附上数据库自己的话；设备继续读取其他点位。 |
| 点位读取还没有任何行的表的最新一行 | 该点位等待，没有错误，在写入第一行之前，设备的健康状态不把它算在内：设备是否在线由其他点位说明。点位读取过之后被清空的表不属于这种等待：点位保留上一次读数，和任何驱动一样，当它比轮询间隔加 10 s 还旧时就算作失败。 |

每台设备都有自己的连接，在连接时打开，从不共享，也不放入连接池，因此服务器断开的连接不会再被使用。

### 准备服务器 {#prepare-the-server}

在常见的安装中，这三种网络数据库出厂时对其他计算机是关闭的。下面每一项设置都由服务器的管理员在服务器上完成；工作站不会改动其中任何一项。

| 数据库 | 把工作站挡在外面的原因 | 修改位置 |
| --- | --- | --- |
| SQL Server | TCP/IP 关闭（SQL Server Express 和 Developer 出厂即如此） | SQL Server Configuration Manager > SQL Server Network Configuration > Protocols for 实例：启用 TCP/IP，然后在 SQL Server Services 中重启 SQL Server（该实例） |
| SQL Server | 命名实例在自己选择的端口上侦听 | TCP/IP > IP Addresses > IPAll：清空 TCP Dynamic Ports，在 TCP Port 中填入端口，然后重启；或让设备的“端口”框留空并填写实例名称 |
| SQL Server | 没有端口的命名实例要靠 SQL Server Browser 定位，而它已停止或被阻挡（错误 26） | SQL Server Services：启动 SQL Server Browser，Start Mode 设为 Automatic；在服务器防火墙中允许 UDP 1434 |
| PostgreSQL | listen_addresses 是默认值 localhost | postgresql.conf：listen_addresses = '*'，然后重启 PostgreSQL |
| PostgreSQL | pg_hba.conf 中没有一行放行本工作站的地址（SQLSTATE 28000） | pg_hba.conf：为该数据库、该用户和本工作站的地址加一行 host，方法为 scram-sha-256（以 MD5 保存的密码用 md5），然后执行 SELECT pg_reload_conf(); |
| MySQL、MariaDB | bind-address 是 127.0.0.1（许多软件包都这样设置），或 skip-networking 已开启 | 配置文件的 [mysqld] 段：bind-address = 0.0.0.0，删除 skip-networking，然后重启 |
| MySQL、MariaDB | 没有对应本工作站地址的账户（错误 1130） | 用 CREATE USER 为本工作站地址上的该用户建账户，并用 GRANT 授予点位所需的权限 |

无论哪种情况，都还需要在服务器所在计算机的防火墙中允许数据库端口。服务器以其中一种方式答复时，设备页面会代替数据库的英文错误，说明是哪项设置，并给出这些步骤和此设备的值（端口、实例、服务器看到的本工作站地址、pg_hba.conf 的那一行和 MySQL 账户，各带一个复制按钮）。设备一旦连接，或服务器以别的方式答复，此提示就会自行消失。日志和代理读到的是同样的步骤。

## 点位寻址 {#tag-addressing}

### 点位如何读取 {#how-a-tag-reads}

此驱动的点位没有自己的地址框：它的**信号源**卡片询问它如何读取。

| **读取内容** | 点位显示的内容 | 其字段 |
| --- | --- | --- |
| **表和列** | 一个单元格：表中某一行的值列。 | **表**、**值列**、**要读取的行**以及行的字段 |
| **自定义查询** | 自己的 SELECT 返回的第一行第一列。 | **查询文本** |

新点位按键读取**表和列**，类型为 Double。提供的类型有 Boolean、Int32、Int64、Float、Double、String 和 DateTime。读取单元格时，整数点位会拒绝带小数部分的值而不是将其舍入（写入则相反，见[写入](#writing)），文本只有在它是数字时才按数字读取。每个点位新建时都是**只读**；写入增加的内容见[写入](#writing)。

### 表和列 {#table-and-column}

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **表** | 表和列 | “一个名称，或架构加名称（dbo.Orders）。含空格或大写字母的名称要放在引号或方括号中，例如 [Order Lines]。” | 空 |
| **值列** | 表和列 | 其单元格就是点位值的那一列。 | 空 |
| **要读取的行** | 表和列 | **按键**：“键列中为该键值的那一行。同一表、同一行的点位在一次查询中一起读取。”**最新一行**：“日期列值最大的那一行：日期时间，或只增不减的数字。同一表、同一行的点位在一次查询中一起读取。” | **按键** |
| **键列** | 按键 | 选定行的列，例如 `OrderNo`。 | 空 |
| **键值** | 按键 | 要读取的那一行中键列的值，例如 `4711`，以文本输入：数据库按该列的值来读取它。 | 空 |
| **日期列** | 最新一行 | 其最大值所在的行即最新一行的列，例如 `updated_at`。 | 空 |

名称按数据库读取的方式读取：

| 写法 | 读取方式 |
| --- | --- |
| `Orders` | 普通名称，按数据库读取任何普通名称的方式读取：PostgreSQL 为小写，Oracle 为大写。 |
| `"Order Lines"`、`[Order Lines]` | 名称本身，不论数据库自己接受哪种引号。 |
| `dbo.Orders` | 架构和表：引号外的点把两者分开。 |

驱动会把名称的每一部分放进其数据库接受的引号中，因此名称从不会作为文本拼接进语句。字段无法工作的点位不会保存，其页面会说明缺少什么：

| 您留空或输入 | 点位页面显示 |
| --- | --- |
| 没有表 | “请填写值所在的表。” |
| 由三部分组成的表，或未闭合的引号 | “表是一个名称，或架构加名称（dbo.Orders）；含空格的名称要放在引号或方括号中。每部分最多 128 个字符。” |
| 没有值列 | “请填写值所在的列。” |
| 由两部分组成的值列 | “值列是单个名称；含空格的名称要放在引号或方括号中。” |
| 按键，没有键列 | “请填写用来选定行的键列。” |
| 按键，没有键值 | “请填写键值：要读取的那一行中键列的值。” |
| 最新一行，没有日期列 | “请填写日期列，其最大值所在的行即最新的一行。” |

只读的**源地址（总线）**一行显示驱动收到的内容：按键选行为 `row?table=dbo.ProductionOrders&column=Quantity&key=OrderNo&equals=4711`，最新一行为 `newest?table=recipe_setpoints&column=oven_temperature&by=updated_at`。

### 自定义查询 {#own-query}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **查询文本** | “一个只读的 SELECT；值为第一行第一列。@now 是工作站的日期和时间。”以 WITH 开头且只读的查询也可以接受。 | 空 |

查询按数据库读取的方式读取：引号中的文本、引号中的名称或注释中的内容不算语句中的词。不只读取的查询会在输入处被拒绝：

| 您输入 | 点位页面显示 |
| --- | --- |
| 什么都没有 | “请填写查询：一个 SELECT，其第一行第一列就是值。” |
| 不是 SELECT 的语句，例如 `UPDATE` 或 `DELETE` | “查询以 SELECT 或 WITH 开头。要写入，请让点位的访问方式允许写入，并使用写入命令。” |
| 修改数据库的 SELECT：`SELECT ... INTO`、`FOR UPDATE`，或含 `INSERT`、`UPDATE`、`EXEC` 的 SELECT | “该查询会修改数据库（INSERT、UPDATE、SELECT INTO、FOR UPDATE、EXEC 等）。查询只能读取。” |
| 两条语句 | “查询包含多条语句。请只保留一个 SELECT。” |
| 未闭合的引号、括号或注释 | “查询中的引号、括号或注释没有闭合。” |
| `@now` 以外的值，例如 `@order` | “查询只接受 @now，即工作站的日期和时间。其他值请直接写进文本。” |

查询点位的总线地址是 `query?sql=` 后接经过百分号编码的查询。

## 读取 {#reading}

| 读取的点位 | 读取方式 |
| --- | --- |
| 按键 | 在键列等于键值的那一行中读取其列；键值作为参数传给数据库。 |
| 最新一行 | 在日期列取该列最大值的那一行中读取其列（`WHERE ... = (SELECT MAX(...) FROM ...)`），这种写法所有数据库和 ODBC 都以同样方式读取；两行并列时读取第一行。 |
| 同一表、同一行的多个点位 | 每轮用一次查询读取它们所有的列，以免为每个点位都询问工厂的数据库。如果数据库拒绝这次查询（表中没有的列），就单独读取该点位自己的列，此后该行逐列读取：错误的列只影响它自己的点位。 |
| 自定义查询 | 按原样执行查询，`@now` 作为参数传入。 |

| 数据库返回 | 点位 |
| --- | --- |
| 点位可读取类型的值 | 读取它。 |
| 空单元格（NULL） | 读取失败。 |
| 没有行含该键值，或不止一行 | 读取失败：一个点位只读取一行。 |
| 还没有任何行的表的最新一行 | 等待，没有错误。 |
| 不返回任何行的查询 | 读取失败。 |
| 点位类型无法读取的值（Int32 点位中的文本、整数点位中的小数部分） | 读取失败，并附上原因。 |

读取失败时点位保留上一次读数，和任何驱动一样，当它比轮询间隔加 10 s 还旧时就算作失败。每次读取都会询问数据库：其他程序做的修改在下一次读取时才能看到，而不是修改的那一刻。

## 写入 {#writing}

只有在点位的**访问**为**读写**或**只写**之后，点位才会写入。此时它的**信号源**卡片会增加写入的方式：

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **写入方式** | 点位写入时 | **修改所读取的单元格**：“修改此点位所读取的单元格，只改一行。仍在等待的较新值会取代较旧的值。”**执行其写入命令**：“执行下面的写入命令。每次写入都算数：仍在等待的写入不会因为较新的写入而被丢弃。”读取自定义查询的点位没有可修改的单元格，只提供**执行其写入命令**。 | **修改所读取的单元格** |
| **写入命令** | 执行其写入命令 | “带 WHERE 的 UPDATE、INSERT 或存储过程调用。@value 是写入的值，@now 是工作站的日期和时间；值总是作为参数传入，从不拼接进文本。”空框中以 `INSERT INTO results (result, recorded_at) VALUES (@value, @now)` 作为提示。 | 空 |

执行写入命令的**只写**点位不读取任何内容，因此它的**信号源**卡片只有写入。

| 写入 | 执行方式 |
| --- | --- |
| 修改所读取的单元格 | 一条 UPDATE，在点位那一行把值列设为 `@value`，放在事务中执行，除非恰好改动一行，否则撤销：没有行含有的键值，或两行都含有的键值，都不写入任何内容并说明原因。和任何设定值一样，仍在等待的较新值会取代较旧的值。 |
| 执行其写入命令 | 每次写入执行一次命令，值作为 `@value`，工作站的日期和时间作为 `@now`。每次写入都算数：两次 INSERT 就是两行，仍在等待的写入不会因为较新的写入而被丢弃。存储过程用 `EXEC`、`CALL` 或 ODBC 的 `{call ...}` 调用。 |
| 整数点位上带小数部分的值 | 被舍入，而不是被拒绝：向没有量程换算和校准的整数点位（Int32 或 Int64）写入带小数部分的值，会在到达数据库之前四舍五入到最近的整数，一半时远离零（2.5 写入 3，-2.5 写入 -3）。拒绝小数部分是读取单元格的方式，不是写入值的方式。 |
| 答复 | 完成，或以数据库自己的话拒绝：用户缺少的权限、该行违反的约束、该列不接受的值。 |

驱动无法执行的命令会在点位页面被拒绝：

| 您输入 | 点位页面显示 |
| --- | --- |
| 什么都没有 | “请填写写入命令：包含 @value 的 UPDATE、INSERT 或存储过程调用。” |
| 以其他词开头的命令，例如 `DELETE` | “写入命令以 UPDATE、INSERT、EXEC 或 CALL 开头。” |
| 两条语句 | “写入命令包含多条语句。请只保留一条，或调用存储过程。” |
| 未闭合的引号、括号或注释 | “写入命令中的引号、括号或注释没有闭合。” |
| 没有 `@value` | “请把 @value 放在写入值的位置：值总是作为参数传入，从不拼接进文本。” |
| `@value` 和 `@now` 以外的值，例如 `@line` | “写入命令只接受 @value（写入的值）和 @now（工作站的日期和时间）。” |
| 不带 WHERE 的 UPDATE | “不带 WHERE 的 UPDATE 会修改表中的每一行。请加上选定该行的 WHERE。” |

以 `@@` 开头的名称（SQL Server 的函数）和 `EXEC` 的参数名（`@speed = @value`）不是值，按原样保留。保存遵循同样的规则，拒绝点位时动态栏会说明：“点位‘Order quantity’没有说明其值在数据库中的位置，或含有驱动拒绝的查询或写入命令。请打开它并修正点位面板标出的字段。”

总线地址会加上写入：单元格为 `row?table=recipes&column=setpoint&key=code&equals=R-100&write=cell`，执行命令的**只写**点位为 `none?write=command&command=` 后接命令。

## 设备发现 {#discovery}

### “扫描范围”卡片 {#the-scan-scope-card}

有配置 Connector 的权限时，驱动页面上有**扫描范围**卡片，其下一行写着“以设备的方式登录，为其各表的每一列提供一个点位，取最新一行或键最小的那一行。”

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **要扫描的设备** | 按名称选择此驱动的一台设备。数据库只能通过保存其登录信息的设备来扫描；设备中仍在等待保存的修改会先保存。 | 按名称排序的第一台设备 |
| **表** | “留空表示所有表，也可以写名称或用 * 代表任意字符的模式，例如 Recipe*。” | 空 |

没有设备的驱动只显示“数据库通过保存其登录信息的设备来扫描。请添加此驱动的设备，并说明其数据库的位置。”范围中仍缺少的内容会以红色显示在卡片下方，在范围完整之前**发现设备**一直是灰色的：

| 范围 | 卡片显示 |
| --- | --- |
| 没有说明数据库位置的设备，或有驱动无法使用的设置的设备 | “设备‘Recipes’尚未说明其数据库的位置，或有驱动无法使用的设置。请先完成它的‘数据库’卡片。” |
| 含架构、引号、方括号或分号的模式，例如 `dbo.Recipe*` | “‘dbo.Recipe*’不是表名或模式。请写一个名称，或用 * 代表任意字符的模式，不带架构或引号。” |
| 已不存在的设备 | “所选的扫描设备已不存在。请另选一台要扫描的设备。” |

要扫描的设备有无法保存的修改时，扫描不会开始：“设备发现没有开始，因为要扫描的设备的修改无法保存。”

### 扫描提供的内容 {#what-the-scan-offers}

扫描以设备的方式登录，读取数据库自己保存的表和视图（不含系统表）及其列的清单，并说明正在读取哪个表，例如“正在读取 recipes 的列…”。它提供一个候选项，即设备本身，标记为**已经配置过**：其卡片列出设备尚无点位读取的列，并已勾选，**添加所选点位**位于**打开**之前，把勾选的列以只读方式添加到设备。

| 表 | 其候选点位 |
| --- | --- |
| 有日期时间列 | 每列一个，按第一个这样的列读取最新一行。 |
| 没有 | 除键之外每列一个，读取主键（没有主键的表则为第一列）值最小的那一行：要读取另一行，请修改点位的**键值**。 |
| 没有，也没有任何行 | 不提供。 |
| 任何点位类型都无法读取的列（二进制数据、几何数据） | 不提供，并在结束扫描的那一行中计数。 |

点位以其表和列命名（`recipes.oven_temperature`），类型取自列的类型，每一个都是**只读**。一次扫描最多读取 100 个表和 500 个值；在此截断的扫描会显示“Recipes 的扫描提前停止。一次扫描最多读取 100 个表和 500 个值：请指定表名或模式以查看其余部分。”扫描的其他行为见[设备发现](connector-discovery)。

设备自己的页面不经过驱动页面也能做同样的事：点位卡片标题栏里的**发现点位**像设备那样登录，按设备保存的样子扫描它的数据库，只列出它还没有点位的列（见[发现一台设备的点位](connector-discovery#discovering-a-devices-tags)）。

## 示例：来自 SQL Server 的生产订单 {#example-a-production-order-from-sql-server}

MES 把生产订单保存在 SQL Server 中 `MES` 数据库的 `dbo.ProductionOrders` 表里，每个订单号一行：

1. 在驱动页面点**添加设备**，命名为 `MES`。在**数据库**卡片上保留 **SQL Server**，**服务器**填写 `mes-sql`（命名实例则为 `mes-sql\SQLEXPRESS`），**端口**留空，**数据库名称**填写 `MES`。
2. 在**登录**卡片上，**用户名**填写 `station_reader` 及其**密码**，或打开**使用工作站的 Windows 账户登录**。对于使用自有证书的工厂服务器，选择**加密，接受任何证书**。
3. **添加点位** `Order quantity`，Int32：**表** `dbo.ProductionOrders`，**值列** `Quantity`，**要读取的行** **按键**，**键列** `OrderNo`，**键值** `4711`。其总线地址为 `row?table=dbo.ProductionOrders&column=Quantity&key=OrderNo&equals=4711`。
4. **添加点位** `Product`，String，同一表同一行，**值列** `ProductCode`：两个点位在一次查询中读取。

使用工具时：

```
device_upsert name=MES driver=Database databaseKind=SqlServer host="mes-sql\SQLEXPRESS" databaseName=MES username=station_reader password=<password> databaseEncryption=TrustServerCertificate
tag_upsert device=MES name="Order quantity" opcUaType=Int32 databaseTable=dbo.ProductionOrders address=Quantity databaseRow=Key databaseKeyColumn=OrderNo databaseKeyValue=4711
tag_upsert device=MES name=Product opcUaType=String databaseTable=dbo.ProductionOrders address=ProductCode databaseRow=Key databaseKeyColumn=OrderNo databaseKeyValue=4711
```

## 示例：来自 PostgreSQL 的配方设定值 {#example-a-recipe-setpoint-from-postgresql}

配方系统每次设定值改变时都会在 `recipe_setpoints` 中添加一行，并在 `updated_at` 列中记录日期：

1. 点**添加设备**，命名为 `Recipes`，**数据库类型** **PostgreSQL**，**服务器** `recipes-db`，**端口**留空（PostgreSQL 的 5432），**数据库名称** `recipes`，然后在**登录**卡片上填写**用户名**和**密码**。
2. **添加点位** `Oven setpoint`，Double：**表** `recipe_setpoints`，**值列** `oven_temperature`，**要读取的行** **最新一行**，**日期列** `updated_at`。

每个新行会在点位的下一次读取时读到。PostgreSQL 按小写读取普通名称，因此用大写字母创建的列（例如 `OvenTemp`）要放在引号中写成 `"OvenTemp"`。使用工具时：

```
device_upsert name=Recipes driver=Database databaseKind=PostgreSql host=recipes-db databaseName=recipes username=station password=<password>
tag_upsert device=Recipes name="Oven setpoint" opcUaType=Double databaseTable=recipe_setpoints address=oven_temperature databaseRow=Newest databaseDateColumn=updated_at
```

## 示例：用 INSERT 写入的结果行 {#example-a-result-line-written-as-an-insert}

质量系统希望每个测量过的零件在其 `test_results` 表中有一行：

1. 在该数据库的设备上，**添加点位** `Result`，Double，**访问**设为**只写**。
2. **写入方式**选**执行其写入命令**，并在**写入命令**中写入添加该行的 `INSERT INTO test_results (result, recorded_at) VALUES (@value, @now)`。

Logic 写入 `Result` 的每个值都成为单独的一行，旁边是工作站的日期和时间；快速连续的写入都会按顺序保留，不会因为较新的写入而丢弃。读取单元格的**读写**点位则会在每次写入时修改它的那一行。使用工具时：

```
tag_upsert device=Quality name=Result opcUaType=Double accessType=WriteOnly databaseWriteMode=Command databaseCommand="INSERT INTO test_results (result, recorded_at) VALUES (@value, @now)"
```

## 示例：通过 ODBC 访问 Access 和 Oracle {#example-access-and-oracle-through-odbc}

| 数据库 | 此计算机上需要 | 设备上的设置 |
| --- | --- | --- |
| Microsoft Access | Microsoft 的 Access Database Engine，64 位 | **数据库类型** **其他（ODBC）**，**数据源或连接字符串** `Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=C:\Data\plant.accdb`，无用户 |
| Oracle | Oracle 的 64 位 ODBC 驱动，以及用它在 ODBC 数据源（64 位）中创建的数据源 `ERP` | **数据库类型** **其他（ODBC）**，**数据源或连接字符串** `ERP`，在**登录**卡片上填写**用户名**和**密码** |

当设备指定的数据源或驱动只对 32 位程序存在（32 位 Office 的 Access、在“ODBC 数据源（32 位）”中创建的数据源），或这台计算机上根本没有时，只要设备处于启用状态且未能连接，设备面板就会连同设备给出的名称说明是哪一种情况，并给出完整的说明。设备的错误用英文说明同样的内容，`driver_capabilities` 也在 Database 驱动的 `prerequisite`（`perDevice` 为 true）中说明同样的内容：

| 部分 | 内容 |
| --- | --- |
| 缺少什么 | 该数据源或驱动的 64 位 ODBC 驱动：Ganter Lab 是 64 位程序，只能看到 64 位 ODBC 驱动以及用它们创建的数据源，并且不能包含其他厂商的驱动 |
| 从哪里获取 | 数据库厂商；对于 Access，是 Microsoft 的页面“Download and install Microsoft 365 Access Runtime”，其运行时带有 Access Database Engine |
| 选择哪个 | 64 位版本（对于 Access，即您所用语言的 64 位下载按钮），不要选 32 位版本 |
| 如何安装 | 以管理员身份运行；然后，如果设备指定的是数据源名称，请在“ODBC 数据源（64 位）”中用相同的名称和相同的数据库创建它 |
| 常见问题 | 在装有 Office 的计算机上安装 Access 时：Microsoft 的页面说，安装的 Access Runtime 与已安装的 Office 相匹配，并且与通过 Windows Installer 安装的 Office 不兼容；Microsoft 关于在 Office 64 位和 32 位版本之间选择的页面说，要先卸载一个版本，再安装另一个版本 |
| 之后 | 在下一次连接尝试时，设备以现有配置连接，无需重启 Ganter Lab |

提示的最后列出这台计算机上的 64 位 ODBC 驱动，名称即 `Driver={...}` 中所写的名称。64 位一侧已有、却仍然失败的数据源或驱动，显示驱动程序管理器自己的错误。

通过 ODBC 时，普通名称按数据库读取的方式读取（Oracle 上为大写），引号中的名称按原样读取；日期和时间精确到整秒发送，这是每个 ODBC 驱动都接受的最细精度。使用工具时：

```
device_upsert name="Plant book" driver=Database databaseKind=Odbc databaseOdbc="Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=C:\Data\plant.accdb"
device_upsert name=ERP driver=Database databaseKind=Odbc databaseOdbc=ERP username=STATION password=<password>
```

## 智能体工具 {#agent-tools}

`driver_capabilities` 在 `database` 下描述此驱动：简单路径，每种数据库及其字段和自己的端口，加密选项，点位的两种读取方式和两种写入方式，值只能以 `@value` 或 `@now` 进入语句的规则，类型，超时，关于数据库用户的建议，设备发现，以及四个完整示例。`device_upsert driver=Database` 接受 `databaseKind`（默认的 `SqlServer`、`PostgreSql`、`MySql`、`Sqlite` 或 `Odbc`），服务器接受 `host`（SQL Server 的 `server\INSTANCE` 或 `server,port` 会被拆开）、`port`、`databaseName`，SQL Server 还接受 `databaseInstance`、`databaseWindowsSignIn` 和 `databaseEncryption`（`Verified`、`TrustServerCertificate` 或 `Off`）；SQLite 接受 `databaseFile`，ODBC 接受 `databaseOdbc`；以及 `username` 和从不返回的 `password`。它返回的设备带有 `database`，其中有地址以及设备是否仍在等待其用户。`tag_upsert` 以 `address` 接受值列，并接受 `databaseSource`（`Fields` 或 `Query`）、`databaseTable`、`databaseRow`（`Key` 或 `Newest`）、`databaseKeyColumn`、`databaseKeyValue`、`databaseDateColumn`、`databaseQuery`，在 `accessType` 写入时还接受 `databaseWriteMode`（`Cell` 或 `Command`）和 `databaseCommand`；除非 `opcUaType` 另有指定，新点位为只读的 Double。`device_discover driver=Database` 接受此驱动已配置设备的名称 `databaseDevice`，以及可选的 `databaseTables`（表名或带 `*` 的模式）；设备缺少的点位用 `tag_upsert` 添加。`connector_browse` 返回每台设备的 `databaseKind`。其他工具见[智能体工具](agent-tools)。

## 值得了解的限制 {#limits-worth-knowing}

- 驱动读写其点位的值，每个点位一个值。它不会把工作站的运行记录写入数据库：每次运行一行、包含序列号、判定和测量值，这不是此驱动写入的内容。
- 产品不附带任何 ODBC 驱动。Oracle、Access、Firebird 以及直接连接的四种以外的任何数据库，都需要在计算机上安装其自己的 64 位 ODBC 驱动；32 位的驱动无法识别。
- 设备不保存任何 Windows 密码：登录 SQL Server 的 Windows 登录，以及网络共享上的 SQLite 文件，都使用运行工作站的账户，因此数据库或共享必须允许该账户访问。
- 查询只能读取。修改数据库的是写入，而写入只发生在访问方式允许写入的点位上。
- 值从不进入语句的文本：写入的值总是参数 `@value`，工作站的日期和时间是 `@now`，键值也是参数。命名任何其他值的语句都会被拒绝。
- 单元格写入恰好修改一行；UPDATE 命令需要它的 WHERE。
- 每次读取都会询问数据库：其他程序做的修改在下一次读取时看到，同一行有许多点位的数据库每轮只为它们全部询问一次。
- 不读取二进制列。
