背景与痛点

InterSystems IRIS 是一个强大的数据平台,其底层支持名为 Global 的数据结构。然而,并非所有数据都会映射到标准的 SQL 表中:

  • 数据可能直接存储在 Global 中。
  • 数据可能定义在类的参数里。

对于这些数据,常规的 SELECT ... FROM table 方式无法直接查询。通常,我们只能通过调用存储过程(CALL) 来获取它们。但这带来了几个问题:

  1. 迁移兼容性:代码迁移到低版本 Caché 数据库时,数据类型定义可能出错,且可能不再支持 SELECT 调用存储过程的方式。
  2. 对第三方开发者不友好:对于熟悉 SQL 但不熟悉 ObjectScript 的开发者来说,使用 CALL 语法不够直观。

本文将介绍一种称为 “进程表” 的方案,它巧妙结合了 Global 和 SQL 表的特性,让您能够用标准的 SELECT 语句查询这类“非表”数据。

什么是“进程表”?

进程表是一种虚拟的 SQL 表。它的核心思路是:

  1. 创建一个标准的持久化类(对应 SQL 表结构)。
  2. 手动修改其存储定义,将数据存储位置指向进程私有 Global(格式为 ^||GlobalName)。这种 Global 仅存在于当前进程中,生命周期随进程结束而结束,非常适合用作临时数据容器。
  3. 编写一个类方法(标记为 SQL 存储过程),在该方法中将您想查询的数据(无论是来自 Global、类参数还是其他逻辑)写入这个进程私有 Global。
  4. 通过一个特殊的 WHERE 子句调用该方法,从而“填充”这个虚拟表,并用 SELECT 查询出结果。

调用形式最终将统一为:

sql

SELECT * FROM YourTable WHERE YourTable_Method() = 1

完整示例

以下是一个名为 User.People 的“进程表”实现示例,它生成 100 条模拟的人员数据。

objectscript实体类

Class User.People Extends %Persistent{
   /// 姓名
   Property Name As %String;
   /// 出生日期
   Property DateOfBirth As %Date;
   /// 性别
   Property Gender As %String;
}

Query代码

/// 核心方法:填充进程表数据
/// 将此方法定义为SQL存储过程,它将在SELECT的WHERE子句中被调用
ClassMethod GLB(Args...) As %Status [ Language = objectscript, SqlProc ]
{
    // 设置错误处理
    s $zt="Error"
  
    // 清空进程Global,确保每次查询都是新数据
    k ^||User.PeopleD, ^||User.PeopleI
  
    // 循环生成100条示例数据
    for i=1:1:100 {
        // 随机生成性别代码 (1 或 2)
        s genderCode = $r(2) + 1
    
        // 使用工具类生成随机的姓名和日期
        s name = ##class(%PopulateUtils).Name(genderCode)
        s dob = ##class(%PopulateUtils).Date(+$h-2000, +$h-10)
    
        // 将性别代码转换为文本
        s genderText = $case(genderCode, 1:"Male", 2:"Female", :"Unknown")
    
        // 将数据以 $listbuild 格式存入进程Global
        // 注意:第一个元素通常留空或用于其他用途
        s ^||User.PeopleD($i(^||User.PeopleD)) = $lb("", name, dob, genderText)
    }
  
    // 返回成功状态
    q $$$OK
  
Error
    // 错误处理块
    s $zt=""
    // 将错误信息写入Global,以便在查询结果中看到
    s ^||User.PeopleD($i(^||User.PeopleD)) = $lb("", "ERROR: "_$ze)
    q $$$OK
}

/// 存储定义 - 这是关键,将数据位置指向进程私有Global
Storage Default
{
    <Data name="PeopleDefaultData">
    <Value name="1">
        <Value>%%CLASSNAME</Value>
    </Value>
    <Value name="2">
        <Value>Name</Value>
    </Value>
    <Value name="3">
        <Value>DateOfBirth</Value>
    </Value>
    <Value name="4">
        <Value>Gender</Value>
    </Value>
    </Data>
    <!-- 注意前缀 ^|| ,这表示进程私有Global -->
    <DataLocation>^||User.PeopleD</DataLocation>
    <DefaultData>PeopleDefaultData</DefaultData>
    <IdLocation>^||User.PeopleD</IdLocation>
    <IndexLocation>^||User.PeopleI</IndexLocation>
    <StreamLocation>^||User.PeopleS</StreamLocation>
    <Type>%Storage.Persistent</Type>
}

如何使用?

  1. 编译上述 User.People 类。
  2. 在 SQL Shell 或任何 SQL 查询工具中,执行以下查询:

sql

SELECT * FROM User.People WHERE People_GLB() = 1

执行过程解析:

  1. WHERE People_GLB() = 1 会触发 GLB 类方法的执行。
  2. GLB 方法会向 ^||User.PeopleD 中写入100条数据。
  3. SQL 引擎会从 ^||User.PeopleD 中读取数据,并按照 User.People 表的定义进行格式化。
  4. 最终返回一个包含100行记录的标准结果集。

关键要点与注意事项

  1. 固定调用模式SELECT ... WHERE TableName_Method() = 1 是一个固定模式= 右边的值会作为参数传递给方法,但方法本身必须返回状态,=1 是约定俗成的写法,表示“执行该方法以准备数据”。
  2. 异常捕获至关重要GLB 方法中的 $zt 错误处理是必须的。它可以防止存储过程执行出错导致整个 SQL 查询崩溃,而是将错误信息作为一行数据返回,保证查询能完成。
  3. 内存考虑:此方案会在 IRIS 进程内存中构建结果集。如果用于导出海量数据(例如百万、千万行),请确保为 IRIS 分配了足够的内存,否则可能导致 错误。
  4. 方案适用性:进程表是一种将任意数据“伪装”成 SQL 表的通用适配器方案。它特别适用于:
    • 将复杂的 Global 结构暴露给 BI 报表工具。
    • 让只会 SQL 的第三方人员能查询系统内部数据。
    • 临时性的、一次性的数据抽取和转换。
    • 是否采用,需根据具体的性能要求、数据量和使用场景来权衡。