Oracle PL/SQL中两大数据类型:标量类型与复合类型

 更新时间:2026年06月25日 11:34:24   作者:知远漫谈  
在Oracle PL/SQL中,数据类型可以分为两大类:标量类型(Scalar Types)和复合类型(Composite Types),本文介绍Oracle PL / SQL中两大数据类型:标量类型与复合类型,感兴趣的朋友一起看看吧

在企业级数据库开发中,Oracle 数据库以其高可靠性、强事务一致性与成熟的 PL/SQL 编程生态长期占据关键系统核心地位。而 PL/SQL(Procedural Language/Structured Query Language)作为 Oracle 原生的嵌入式过程化语言,其数据类型系统是开发者构建健壮、高效、可维护存储过程、函数、包和触发器的基石。理解其类型分类逻辑——尤其是标量类型(Scalar Types)复合类型(Composite Types) 的本质差异、适用场景、内存行为及互操作约束——不仅关乎代码能否编译通过,更直接影响性能表现、异常处理粒度、游标生命周期管理,甚至跨语言集成的稳定性。

本文将深入 Oracle 19c 及 21c 主流版本的 PL/SQL 类型体系,以原理为纲、实践为目,结合真实业务场景剖析每类数据类型的语义边界与工程权衡。我们将穿插 Java 程序调用 PL/SQL 的完整端到端示例(含 JDBC 绑定、结构映射、异常传播),并借助 Mermaid 图表直观呈现类型层级关系与内存布局模型。所有外部链接均指向 Oracle 官方权威文档,确保信息准确、时效性强、无需翻墙即可访问。

🔍 一、为什么类型系统如此重要?——不只是语法检查

在静态类型语言中,类型是编译器理解程序意图的第一道桥梁。PL/SQL 的类型系统远不止于“防止 VARCHAR2 赋值给 NUMBER”这类基础校验。它直接参与:

  • 内存分配策略VARCHAR2(100) 在栈上静态分配;CLOB 则仅存 LOB locator,实际内容在大对象段;
  • 隐式转换规则TO_NUMBER('123') 成立,但 TO_NUMBER('12.3.4')ORA-01722,而 TO_CHAR(SYSDATE) 默认格式依赖 NLS_DATE_FORMAT
  • 绑定变量优化:JDBC 中使用 setString() vs setBigDecimal() 影响 Oracle 执行计划缓存命中率;
  • 异常传播路径NO_DATA_FOUND 仅在 SELECT INTO 无结果时抛出,若目标为 RECORD 类型则行为一致;但若目标为 VARRAY,则需显式检查 COUNT
  • 跨语言契约:Java 的 java.sql.Types.VARCHAR 映射 PL/SQL VARCHAR2,而 java.sql.Types.STRUCT 必须对应已注册的 OBJECT TYPE

💡 小知识:Oracle 官方明确指出——“PL/SQL 类型系统是强类型(strongly typed),但允许受控的隐式转换(controlled implicit conversion)”。这意味着类型安全由编译器保障,但转换逻辑由 Oracle 运行时按预定义规则执行,不可扩展、不可重载。这与 Java 的泛型擦除或 C++ 模板特化有本质区别。

我们先从最基础的“原子单元”开始:标量类型。

⚛️ 二、标量类型(Scalar Types):不可再分的数据原子

标量类型代表单个值,不包含子组件,是 PL/SQL 中最轻量、最常用、性能最优的数据容器。它们分为四大类:数值型、字符型、布尔型、日期时间型。注意:PL/SQL 中没有指针、引用或 void 类型,所有标量均为值语义(value semantics)——赋值即拷贝。

2.1 数值型(Numeric Types)

类型说明范围 / 精度典型用途Java 对应
BINARY_INTEGER32 位有符号整数,PL/SQL 内部优化类型-2³¹ ~ 2³¹−1循环索引、数组下标(性能敏感场景)int
PLS_INTEGERBINARY_INTEGER 的增强版,支持溢出检测(PLS_INTEGER overflow raises VALUE_ERROR同上推荐替代 BINARY_INTEGER,尤其在数学运算中int
NUMBER[(p[,s])]可变精度十进制数,Oracle 最通用数值类型p=1~38, s=-84~127金融计算、ID、统计指标(需精确小数)java.math.BigDecimal
SIMPLE_INTEGER无符号整数(Oracle 11g+),不检查溢出(极致性能)0 ~ 2³¹−1高频计数器、哈希桶索引int(需业务保证不溢出)

最佳实践

  • 避免在循环中使用 NUMBER 存储计数器(如 i NUMBER := 0),改用 PLS_INTEGER —— 性能提升可达 3~5 倍(Oracle 文档 PL/SQL Performance Guidelines 实测)。
  • NUMBER 的精度声明并非强制存储约束,而是语义提示NUMBER(5,2) 表示最多 5 位数字,其中 2 位小数;插入 123.456 会四舍五入为 123.46,但插入 1234.56 则报 ORA-01438
DECLARE
  v_int   PLS_INTEGER := 100;
  v_num   NUMBER(5,2) := 123.456; -- 自动四舍五入为 123.46
  v_simple SIMPLE_INTEGER := 2147483647;
BEGIN
  DBMS_OUTPUT.PUT_LINE('PLS_INTEGER: ' || v_int);
  DBMS_OUTPUT.PUT_LINE('NUMBER(5,2): ' || v_num); -- 输出 123.46
  -- v_simple := v_simple + 1; -- 若取消注释,运行时报 VALUE_ERROR(溢出)
END;

2.2 字符型(Character Types)

类型说明最大长度存储特性Java 对应
CHAR(n)定长字符串,不足补空格1~32767 字节空格填充,比较时忽略尾部空格String(trim 后等价)
VARCHAR2(n)变长字符串(推荐!)1~32767 字节仅存实际内容,无填充String
NCHAR(n)定长 Unicode 字符串1~32767 字符使用数据库字符集(AL32UTF8)String
NVARCHAR2(n)变长 Unicode 字符串1~32767 字符同上,推荐用于多语言String

⚠️ 关键区别:CHAR vs VARCHAR2

  • CHAR(10)"abc" → 占用 10 字节("abc      ");VARCHAR2(10)"abc" → 占用 3 字节。
  • WHERE name = 'John' 查询 CHAR(10) 列时,Oracle 自动右补空格匹配;但对 VARCHAR2 则严格字节相等。
  • 永远优先选择 VARCHAR2 —— 除非业务强制要求固定宽度(如银行交易码 TXN_CODE CHAR(4))。
DECLARE
  v_char    CHAR(5)      := 'Hi';
  v_varchar VARCHAR2(5)  := 'Hi';
BEGIN
  DBMS_OUTPUT.PUT_LINE('CHAR length: ' || LENGTH(v_char));     -- 输出 5(含2空格)
  DBMS_OUTPUT.PUT_LINE('VARCHAR2 length: ' || LENGTH(v_varchar)); -- 输出 2
  DBMS_OUTPUT.PUT_LINE('CHAR = VARCHAR2? ' || 
    CASE WHEN v_char = v_varchar THEN 'YES' ELSE 'NO' END); -- 输出 YES(因自动补空格)
END;

2.3 布尔型(BOOLEAN)

这是 PL/SQL 独有、SQL 引擎不支持的类型!意味着你无法在 SELECTINSERT 或表定义中使用 BOOLEAN

类型值域用途Java 注意点
BOOLEANTRUE / FALSE / NULL控制流变量、函数返回标志JDBC 无原生映射,需转为 CHAR(1)NUMBER(1)
DECLARE
  v_flag BOOLEAN := TRUE;
  v_result VARCHAR2(5);
BEGIN
  IF v_flag THEN
    v_result := 'YES';
  ELSE
    v_result := 'NO';
  END IF;
  DBMS_OUTPUT.PUT_LINE(v_result);
  -- ❌ 错误!不能在 SQL 中直接使用 BOOLEAN
  -- SELECT * FROM employees WHERE is_active = v_flag;
  -- ✅ 正确:转换为 SQL 可识别类型
  FOR r IN (
    SELECT employee_id, first_name 
    FROM employees 
    WHERE (v_flag AND is_active = 'Y') OR (NOT v_flag AND is_active = 'N')
  ) LOOP
    NULL;
  END LOOP;
END;

📌 Java 侧如何桥接?
由于 JDBC 不支持 BOOLEAN,常见方案是:

  • 存储层用 CHAR(1) CHECK (flag IN ('Y','N')),Java 用 getString() + equals("Y")
  • 或用 NUMBER(1) CHECK (flag IN (0,1)),Java 用 getInt()
// Java 示例:安全读取 BOOLEAN 语义字段
String sql = "SELECT employee_id, is_active FROM employees WHERE department_id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setInt(1, 10);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            int id = rs.getInt("employee_id");
            // 方案1:CHAR(1) 映射
            String isActiveStr = rs.getString("is_active");
            boolean isActive = "Y".equals(isActiveStr); // 安全,null-safe
            // 方案2:NUMBER(1) 映射(更紧凑)
            // int isActiveNum = rs.getInt("is_active"); // 注意:getInt() 对 null 返回 0!
            // boolean isActive = rs.wasNull() ? false : (isActiveNum == 1);
        }
    }
}

2.4 日期时间型(Datetime Types)

类型说明精度时区支持Java 对应
DATE日期+时间(秒级)❌ 无时区java.time.LocalDate + LocalTime(需拆分)或 java.sql.Date/Time
TIMESTAMP日期+时间(纳秒级)纳秒(默认6位)❌ 无时区java.time.LocalDateTimejava.sql.Timestamp
TIMESTAMP WITH TIME ZONE带时区偏移的时间戳纳秒✅ 保存 +08:00Asia/Shanghaijava.time.OffsetDateTimeZonedDateTime
TIMESTAMP WITH LOCAL TIME ZONE本地时区时间戳(存 UTC,查时转当前会话时区)纳秒✅ 透明转换java.time.LocalDateTime(显示值)

🔍 深度解析 TIMESTAMP WITH LOCAL TIME ZONE
该类型物理存储为 UTC,但所有客户端操作均自动转换。例如:

  • 会话 A(ALTER SESSION SET TIME_ZONE = '+08:00')插入 TIMESTAMP '2023-01-01 12:00:00' → 存为 2023-01-01 04:00:00 UTC
  • 会话 B(TIME_ZONE = '-05:00')查询同一行 → 返回 2023-01-01 23:00:00(UTC+4 → UTC-5 = +9h offset)。

推荐场景:日志时间、审计字段(用户看到的是“本地时间”,DBA 管理的是统一 UTC)。

-- 创建带时区的表
CREATE TABLE event_log (
  id         NUMBER PRIMARY KEY,
  event_time TIMESTAMP WITH LOCAL TIME ZONE,
  description VARCHAR2(200)
);
-- 插入(会话时区为 +08:00)
INSERT INTO event_log VALUES (1, TIMESTAMP '2023-01-01 10:00:00', 'User login');
-- 查询(会话时区为 -05:00)→ 显示 2023-01-01 01:00:00
SELECT id, event_time, description FROM event_log;
// Java 读取 TIMESTAMP WITH LOCAL TIME ZONE(自动适配 JVM 时区)
String sql = "SELECT event_time FROM event_log WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setInt(1, 1);
    try (ResultSet rs = ps.executeQuery()) {
        if (rs.next()) {
            // ✅ 推荐:使用 OffsetDateTime 获取原始时区信息
            OffsetDateTime odt = rs.getObject("event_time", OffsetDateTime.class);
            System.out.println("Event time (with zone): " + odt); 
            // 输出类似:2023-01-01T01:00:00-05:00
            // ⚠️ 注意:LocalDateTime 会丢失时区!仅取本地视图
            LocalDateTime ldt = rs.getObject("event_time", LocalDateTime.class);
            System.out.println("Local view only: " + ldt); // 2023-01-01T01:00:00
        }
    }
}

2.5 标量类型的内存与性能模型 📈

所有标量类型在 PL/SQL 中均采用栈分配(stack allocation),生命周期与声明作用域严格绑定:

DECLARE
  v1 NUMBER := 100; -- 分配在调用栈帧
  v2 VARCHAR2(100) := 'Hello World'; -- 同上,100字节栈空间
BEGIN
  DECLARE
    v3 PLS_INTEGER := 200; -- 新栈帧,独立生命周期
  BEGIN
    NULL;
  END; -- v3 栈空间在此释放
  -- v3 已不可访问
END; -- v1, v2 栈空间在此释放

📌 性能关键结论(Oracle 性能白皮书验证):

  • PLS_INTEGER 运算比 NUMBER3.8x(整数加法);
  • VARCHAR2(100)VARCHAR2(4000) 在短字符串场景下内存占用低 97%(因无冗余预留);
  • BOOLEAN 变量几乎零开销(1 字节),但禁止用于 SQL 上下文是最大设计限制。

🧩 三、复合类型(Composite Types):结构化数据容器

当单一值无法表达业务实体时,复合类型登场。它们将多个相关数据项组织为一个逻辑单元,支持嵌套、集合、对象化建模。PL/SQL 提供三大类复合类型:记录(RECORD)集合(Collections)对象类型(OBJECT TYPES)。注意:复合类型不能直接用于 SQL DML(如 INSERT INTO … VALUES (my_record)),必须解构或通过 OBJECT TYPE 封装。

3.1 记录类型(RECORD):轻量级结构体 🧱

RECORD 是 PL/SQL 中最常用的复合类型,类似 C 的 struct 或 Java 的 POJO(但无方法)。它由命名字段(field) 组成,每个字段有独立数据类型。

3.1.1 声明方式

方式语法特点场景
基于表列emp_rec employees%ROWTYPE;字段名=列名,类型=列类型,自动同步表结构变更快速映射单行查询结果
基于游标cur_rec emp_cursor%ROWTYPE;同上,但字段由游标 SELECT 定义处理复杂 JOIN 结果集
自定义 RECORDTYPE emp_t IS RECORD (id NUMBER, name VARCHAR2(50));完全可控,支持标量/复合/REF 字段构建领域模型、参数传递
-- 示例:三种 RECORD 声明与使用
DECLARE
  -- 1. 基于表
  dept_rec departments%ROWTYPE;
  -- 2. 基于游标
  CURSOR emp_cur IS 
    SELECT e.employee_id, e.first_name, d.department_name 
    FROM employees e JOIN departments d ON e.department_id = d.department_id;
  emp_cur_rec emp_cur%ROWTYPE;
  -- 3. 自定义 RECORD
  TYPE address_t IS RECORD (
    street VARCHAR2(100),
    city   VARCHAR2(50),
    zip    VARCHAR2(20)
  );
  TYPE person_t IS RECORD (
    id       NUMBER,
    full_name VARCHAR2(100),
    addr     address_t, -- ✅ 复合类型嵌套!
    is_active BOOLEAN
  );
  person_rec person_t;
BEGIN
  -- 查询部门(基于表 RECORD)
  SELECT * INTO dept_rec 
  FROM departments 
  WHERE department_id = 10;
  DBMS_OUTPUT.PUT_LINE('Dept: ' || dept_rec.department_name);
  -- 打开游标(基于游标 RECORD)
  OPEN emp_cur;
  FETCH emp_cur INTO emp_cur_rec;
  DBMS_OUTPUT.PUT_LINE('Emp: ' || emp_cur_rec.first_name || ' in ' || emp_cur_rec.department_name);
  CLOSE emp_cur;
  -- 使用自定义 RECORD
  person_rec.id := 1001;
  person_rec.full_name := 'Alice Johnson';
  person_rec.addr.street := '123 Main St';
  person_rec.addr.city := 'San Francisco';
  person_rec.is_active := TRUE;
  DBMS_OUTPUT.PUT_LINE('Person: ' || person_rec.full_name || ', City: ' || person_rec.addr.city);
END;

3.1.2 RECORD 的 Java 映射挑战与解决方案

JDBC 不原生支持 RECORD。当 PL/SQL 过程返回 RECORD 时,Java 必须通过以下方式之一处理:

  1. 解构为 OUT 参数(最常用、最兼容)
  2. 封装为 OBJECT TYPE 并注册 STRUCT(面向对象风格)
  3. 返回关联游标(REF CURSOR)(适合动态结构)

方案1:OUT 参数解构(推荐)

-- PL/SQL 过程:返回员工基本信息(解构为多个 OUT 参数)
CREATE OR REPLACE PROCEDURE get_employee_info(
  p_emp_id    IN  employees.employee_id%TYPE,
  p_first_name OUT employees.first_name%TYPE,
  p_last_name  OUT employees.last_name%TYPE,
  p_salary     OUT employees.salary%TYPE,
  p_dept_name  OUT departments.department_name%TYPE
) AS
BEGIN
  SELECT e.first_name, e.last_name, e.salary, d.department_name
  INTO p_first_name, p_last_name, p_salary, p_dept_name
  FROM employees e
  JOIN departments d ON e.department_id = d.department_id
  WHERE e.employee_id = p_emp_id;
END;
// Java 调用解构式 OUT 参数
String sql = "{CALL get_employee_info(?, ?, ?, ?, ?)}";
try (CallableStatement cs = conn.prepareCall(sql)) {
    cs.setInt(1, 101); // IN 参数
    cs.registerOutParameter(2, Types.VARCHAR); // first_name
    cs.registerOutParameter(3, Types.VARCHAR); // last_name
    cs.registerOutParameter(4, Types.DECIMAL); // salary
    cs.registerOutParameter(5, Types.VARCHAR); // dept_name
    cs.execute();
    String firstName = cs.getString(2);
    String lastName = cs.getString(3);
    BigDecimal salary = cs.getBigDecimal(4);
    String deptName = cs.getString(5);
    System.out.printf("Employee: %s %s, Salary: %s, Dept: %s%n", 
                      firstName, lastName, salary, deptName);
}

方案2:OBJECT TYPE + STRUCT(强类型契约)
首先创建数据库对象类型:

-- 创建 ADDRESS OBJECT TYPE
CREATE OR REPLACE TYPE address_obj AS OBJECT (
  street VARCHAR2(100),
  city   VARCHAR2(50),
  zip    VARCHAR2(20)
);
-- 创建 PERSON OBJECT TYPE(含 REF)
CREATE OR REPLACE TYPE person_obj AS OBJECT (
  id        NUMBER,
  full_name VARCHAR2(100),
  addr      address_obj,
  dept_ref  REF dept_obj -- 假设存在 dept_obj
);

然后在 Java 中注册并使用:

// Java 注册 STRUCT 类型(需在连接后执行一次)
conn.getTypeMap().put("HR.PERSON_OBJ", PersonObj.class); // 自定义 Java 类
// 调用返回 PERSON_OBJ 的函数
String sql = "{? = CALL get_person(?)}";
try (CallableStatement cs = conn.prepareCall(sql)) {
    cs.registerOutParameter(1, OracleTypes.STRUCT, "HR.PERSON_OBJ");
    cs.setInt(2, 1001);
    cs.execute();
    // 获取 STRUCT 并转换为 Java 对象
    STRUCT struct = (STRUCT) cs.getObject(1);
    Object[] attrs = struct.toJdbc(); // [id, full_name, address_obj, dept_ref]
    PersonObj person = new PersonObj(
        ((BigDecimal) attrs[0]).longValue(),
        (String) attrs[1],
        (AddressObj) attrs[2], // 自动映射
        (DeptRef) attrs[3]
    );
}

🔗 官方参考:Oracle JDBC 开发者指南中关于 Working with Oracle Object Types 的完整说明。

3.2 集合类型(Collections):有序/无序元素组 📦

集合是 PL/SQL 中处理多值数据的核心机制,分为三类:INDEX BY TABLE(关联数组)、NESTED TABLEVARRAY。它们的关键差异在于存储位置、大小约束、SQL 可见性

3.2.1 三类集合对比总览

3.2.2 关联数组(Associative Array / INDEX BY TABLE)

  • 唯一 PL/SQL 本地集合:不能持久化到数据库,不能在 SQL 中直接使用。
  • 键灵活:支持 BINARY_INTEGER(整数索引)或 VARCHAR2(哈希键)。
  • 稀疏性t(1):=1; t(100):=100; 合法,中间索引不存在。
  • 不能作为 OUT 参数传递给 Java(JDBC 不支持)。
DECLARE
  -- 整数键关联数组
  TYPE emp_id_tab IS TABLE OF employees.employee_id%TYPE INDEX BY PLS_INTEGER;
  emp_ids emp_id_tab;
  -- 字符串键关联数组(类似 Map)
  TYPE dept_map IS TABLE OF departments.department_name%TYPE INDEX BY VARCHAR2(30);
  dept_names dept_map;
BEGIN
  -- 使用整数键
  emp_ids(1) := 101;
  emp_ids(5) := 105;
  emp_ids(10) := 110;
  -- 使用字符串键
  dept_names('HR') := 'Human Resources';
  dept_names('IT') := 'Information Technology';
  -- 遍历整数键数组(需用 FIRST/NEXT)
  DECLARE
    l_idx PLS_INTEGER := emp_ids.FIRST;
  BEGIN
    WHILE l_idx IS NOT NULL LOOP
      DBMS_OUTPUT.PUT_LINE('Emp ID at index ' || l_idx || ': ' || emp_ids(l_idx));
      l_idx := emp_ids.NEXT(l_idx);
    END LOOP;
  END;
  -- 遍历字符串键数组
  DECLARE
    l_key VARCHAR2(30) := dept_names.FIRST;
  BEGIN
    WHILE l_key IS NOT NULL LOOP
      DBMS_OUTPUT.PUT_LINE('Dept ' || l_key || ': ' || dept_names(l_key));
      l_key := dept_names.NEXT(l_key);
    END LOOP;
  END;
END;

3.2.3 嵌套表(Nested Table)

  • 可持久化:可作为表列类型(CREATE TABLE t (col nested_table_type))。
  • SQL 可见:可用 TABLE() 函数在 SQL 中展开。
  • 动态大小EXTEND 添加元素,TRIM 删除末尾。
  • ⚠️ 存储开销:每个嵌套表在数据库中生成独立存储段(NESTED TABLE STORE AS)。
-- 1. 创建嵌套表类型
CREATE OR REPLACE TYPE phone_list AS TABLE OF VARCHAR2(20);
-- 2. 创建含嵌套表的表
CREATE TABLE customers (
  id     NUMBER PRIMARY KEY,
  name   VARCHAR2(100),
  phones phone_list
) NESTED TABLE phones STORE AS phones_store;
-- 3. PL/SQL 中使用
DECLARE
  v_phones phone_list := phone_list('123-456-7890', '098-765-4321');
BEGIN
  INSERT INTO customers VALUES (1, 'John Doe', v_phones);
  -- 更新:添加新号码
  UPDATE customers 
  SET phones = phone_list('123-456-7890', '098-765-4321', '555-123-4567')
  WHERE id = 1;
END;
-- 4. SQL 中查询嵌套表
SELECT c.name, p.column_value AS phone
FROM customers c, TABLE(c.phones) p;
-- 返回:
-- John Doe | 123-456-7890
-- John Doe | 098-765-4321
-- John Doe | 555-123-4567

3.2.4 可变数组(VARRAY)

  • 行内存储:小尺寸时直接存主表块,减少 I/O。
  • 顺序保证:索引严格连续(1…n),天然有序。
  • 大小固定:声明时指定最大长度(VARRAY(10) OF ...),不可动态扩容。
-- 创建 VARRAY 类型
CREATE OR REPLACE TYPE skill_array AS VARRAY(5) OF VARCHAR2(30);
-- 表中使用
CREATE TABLE developers (
  id     NUMBER PRIMARY KEY,
  name   VARCHAR2(100),
  skills skill_array
);
-- PL/SQL 使用
DECLARE
  v_skills skill_array := skill_array('Java', 'PL/SQL', 'SQL');
BEGIN
  INSERT INTO developers VALUES (1, 'Alice', v_skills);
  -- ✅ 合法:未超上限
  -- v_skills.EXTEND; v_skills(v_skills.COUNT) := 'Python'; 
  -- ❌ 运行时报错:ORA-22165: given index [4] must be in range of [1] to [3]
  -- v_skills(4) := 'Python';
END;

3.2.5 Java 如何处理 PL/SQL 集合?

JDBC 支持 ARRAY 类型,但仅限 NESTED TABLEVARRAY(关联数组不支持)。

// Java 调用返回 VARRAY 的函数
String sql = "{? = CALL get_top_skills(?)}"; // 返回 skill_array
try (CallableStatement cs = conn.prepareCall(sql)) {
    cs.registerOutParameter(1, OracleTypes.ARRAY, "SKILL_ARRAY");
    cs.setInt(2, 1001);
    cs.execute();
    ARRAY array = (ARRAY) cs.getArray(1);
    String[] skills = (String[]) array.getArray(); // 直接转 Java 数组
    System.out.println("Top skills: " + Arrays.toString(skills));
}
// 插入 NESTED TABLE(需构造 ARRAY 对象)
String[] phones = {"123-456-7890", "098-765-4321"};
ARRAY phoneArray = ((OracleConnection) conn).createARRAY("PHONE_LIST", phones);
String insertSql = "INSERT INTO customers VALUES (?, ?, ?)";
try (PreparedStatement ps = conn.prepareStatement(insertSql)) {
    ps.setInt(1, 2);
    ps.setString(2, "Jane Smith");
    ps.setArray(3, phoneArray); // 绑定 ARRAY
    ps.executeUpdate();
}

🔗 深入阅读:Oracle JDBC 文档中 Using Oracle Collection Types 章节。

3.3 对象类型(OBJECT TYPES):面向对象的数据库实体 🧬

OBJECT TYPE 是 Oracle 对 SQL:1999 对象关系特性的实现,允许在数据库中定义带属性和方法的类。它是 PL/SQL 复合类型中唯一可被 SQL 引擎原生理解的类型,也是实现跨语言强类型契约的基石。

3.3.1 基础语法与实例

-- 1. 创建 ADDRESS 对象类型(无方法)
CREATE OR REPLACE TYPE address_obj AS OBJECT (
  street VARCHAR2(100),
  city   VARCHAR2(50),
  state  CHAR(2),
  zip    VARCHAR2(10)
);
-- 2. 创建 PERSON 对象类型(含构造函数和成员函数)
CREATE OR REPLACE TYPE person_obj AS OBJECT (
  id        NUMBER,
  first_name VARCHAR2(50),
  last_name  VARCHAR2(50),
  birth_date DATE,
  addr       address_obj,
  -- 构造函数(必须与类型同名)
  CONSTRUCTOR FUNCTION person_obj(
    self IN OUT NOCOPY person_obj,
    id NUMBER,
    first_name VARCHAR2,
    last_name VARCHAR2,
    birth_date DATE DEFAULT NULL,
    addr address_obj DEFAULT NULL
  ) RETURN SELF AS RESULT,
  -- 成员函数(可修改属性)
  MEMBER FUNCTION get_full_name RETURN VARCHAR2,
  MEMBER FUNCTION get_age RETURN NUMBER,
  MEMBER PROCEDURE set_address(new_addr address_obj)
);
/
-- 3. 实现类型体
CREATE OR REPLACE TYPE BODY person_obj AS
  CONSTRUCTOR FUNCTION person_obj(
    self IN OUT NOCOPY person_obj,
    id NUMBER,
    first_name VARCHAR2,
    last_name VARCHAR2,
    birth_date DATE DEFAULT NULL,
    addr address_obj DEFAULT NULL
  ) RETURN SELF AS RESULT IS
  BEGIN
    self.id := id;
    self.first_name := first_name;
    self.last_name := last_name;
    self.birth_date := birth_date;
    self.addr := addr;
    RETURN;
  END;
  MEMBER FUNCTION get_full_name RETURN VARCHAR2 IS
  BEGIN
    RETURN self.first_name || ' ' || self.last_name;
  END;
  MEMBER FUNCTION get_age RETURN NUMBER IS
  BEGIN
    IF self.birth_date IS NULL THEN
      RETURN NULL;
    ELSE
      RETURN FLOOR(MONTHS_BETWEEN(SYSDATE, self.birth_date) / 12);
    END IF;
  END;
  MEMBER PROCEDURE set_address(new_addr address_obj) IS
  BEGIN
    self.addr := new_addr;
  END;
END;
/

3.3.2 在 PL/SQL 中使用对象类型

DECLARE
  -- 创建对象实例(调用构造函数)
  v_person person_obj := person_obj(
    id => 1001,
    first_name => 'Alice',
    last_name => 'Johnson',
    birth_date => DATE '1990-05-15',
    addr => address_obj('123 Main St', 'San Francisco', 'CA', '94105')
  );
  -- 或用 NEW(Oracle 12c+)
  -- v_person person_obj := NEW person_obj(1001, 'Alice', 'Johnson', ...);
BEGIN
  DBMS_OUTPUT.PUT_LINE('Full name: ' || v_person.get_full_name());
  DBMS_OUTPUT.PUT_LINE('Age: ' || v_person.get_age());
  -- 调用成员过程
  v_person.set_address(address_obj('456 Oak Ave', 'Palo Alto', 'CA', '94301'));
  DBMS_OUTPUT.PUT_LINE('New city: ' || v_person.addr.city);
END;

3.3.3 Java 与 OBJECT TYPE 的深度集成

这是体现类型系统威力的关键场景。通过 STRUCT,Java 可获得编译期类型安全运行期结构验证

// Java 定义匹配的 POJO(需 public fields 或 getters/setters)
public class AddressObj implements SQLData {
    public String street;
    public String city;
    public String state;
    public String zip;
    @Override
    public void readSQL(SQLInput stream, String typeName) throws SQLException {
        this.street = stream.readString();
        this.city = stream.readString();
        this.state = stream.readString();
        this.zip = stream.readString();
    }
    @Override
    public void writeSQL(SQLOutput stream) throws SQLException {
        stream.writeString(street);
        stream.writeString(city);
        stream.writeString(state);
        stream.writeString(zip);
    }
}
public class PersonObj implements SQLData {
    public long id;
    public String firstName;
    public String lastName;
    public java.sql.Date birthDate;
    public AddressObj addr;
    @Override
    public void readSQL(SQLInput stream, String typeName) throws SQLException {
        this.id = stream.readLong();
        this.firstName = stream.readString();
        this.lastName = stream.readString();
        this.birthDate = stream.readDate();
        this.addr = (AddressObj) stream.readObject(); // 递归读取嵌套对象
    }
    @Override
    public void writeSQL(SQLOutput stream) throws SQLException {
        stream.writeLong(id);
        stream.writeString(firstName);
        stream.writeString(lastName);
        stream.writeDate(birthDate);
        stream.writeObject(addr);
    }
}
// 注册类型映射并调用
conn.getTypeMap().put("HR.ADDRESS_OBJ", AddressObj.class);
conn.getTypeMap().put("HR.PERSON_OBJ", PersonObj.class);
String sql = "{? = CALL create_person(?)}";
try (CallableStatement cs = conn.prepareCall(sql)) {
    cs.registerOutParameter(1, OracleTypes.STRUCT, "HR.PERSON_OBJ");
    cs.setString(2, "Bob Smith");
    cs.execute();
    // 自动反序列化为 PersonObj 实例
    PersonObj person = (PersonObj) cs.getObject(1);
    System.out.printf("Created: %s, Age: %d, City: %s%n", 
                      person.firstName + " " + person.lastName,
                      person.getAge(), // 可调用业务方法
                      person.addr.city);
}

优势总结

  • 类型安全:编译期检查字段名、类型;
  • 可维护性:数据库类型变更 → Java 编译失败,强制更新;
  • 性能:避免字符串拼接与反射解析;
  • 复用性:同一 OBJECT TYPE 可被 PL/SQL、Java、.NET、Python(cx_Oracle)共用。

🔄 四、标量与复合类型的转换与互操作

类型间并非孤岛。PL/SQL 提供显式转换函数,而 Java 侧需遵循 JDBC 规范进行桥接。

4.1 PL/SQL 内部转换

转换方向函数示例注意事项
标量 → 字符TO_CHAR(expr [, fmt])TO_CHAR(SYSDATE, 'YYYY-MM-DD')fmt 为空时依赖 NLS_DATE_FORMAT
字符 → 数值TO_NUMBER(char_expr [, fmt])TO_NUMBER('123.45', '999.99')格式不匹配抛 ORA-01722
字符 → 日期TO_DATE(char_expr [, fmt])TO_DATE('2023/01/01', 'YYYY/MM/DD')强烈建议始终指定格式
复合 → 字符SYS.DBMS_XMLGEN.GETXMLSYS.DBMS_XMLGEN.GETXML('SELECT * FROM DUAL')用于 RECORD/ROWTYPE 的 XML 序列化
DECLARE
  v_date DATE := TO_DATE('2023-01-01', 'YYYY-MM-DD'); -- ✅ 显式格式
  v_num  NUMBER := TO_NUMBER('123.45'); -- ✅ 无格式,依赖 NLS_NUMERIC_CHARACTERS
  v_rec  employees%ROWTYPE;
  v_xml  CLOB;
BEGIN
  SELECT * INTO v_rec FROM employees WHERE ROWNUM = 1;
  -- 将 RECORD 转为 XML(需先构造查询)
  v_xml := SYS.DBMS_XMLGEN.GETXML(
    'SELECT ' || v_rec.employee_id || ' id, ''' || v_rec.first_name || ''' name FROM DUAL'
  );
  DBMS_OUTPUT.PUT_LINE('XML: ' || DBMS_LOB.SUBSTR(v_xml, 200, 1));
END;

4.2 Java 侧类型映射黄金法则

PL/SQL 类型JDBC TypeJava Type关键注意事项
NUMBERTypes.DECIMALBigDecimal✅ 精确;getInt()/getDouble() 会丢失精度
VARCHAR2Types.VARCHARString✅ 安全;getBytes() 注意字符集
DATETypes.DATEjava.sql.Date⚠️ 仅日期;getTime() 返回毫秒(午夜)
TIMESTAMPTypes.TIMESTAMPTimestamp✅ 推荐;toInstant()java.time.Instant
CLOBTypes.CLOBClob✅ 流式读取;避免 getClob().getSubString() 加载全量
BLOBTypes.BLOBBlob✅ 同上;用 getBinaryStream()
OBJECT TYPEOracleTypes.STRUCTSTRUCT or custom SQLData✅ 强类型;需 getTypeMap() 注册
// ✅ 正确处理 CLOB:流式读取,避免 OOM
String sql = "SELECT resume FROM candidates WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setInt(1, 123);
    try (ResultSet rs = ps.executeQuery()) {
        if (rs.next()) {
            Clob clob = rs.getClob("resume");
            if (clob != null) {
                try (Reader reader = clob.getCharacterStream()) {
                    char[] buffer = new char[4096];
                    int len;
                    StringBuilder content = new StringBuilder();
                    while ((len = reader.read(buffer)) != -1) {
                        content.append(buffer, 0, len);
                    }
                    System.out.println("Resume length: " + content.length());
                }
            }
        }
    }
}

🧭 五、选型决策树:何时用标量?何时用复合?

面对需求,如何选择?以下决策树覆盖 95% 场景:

真实案例:电商订单服务选型分析

场景推荐类型理由
订单主表 order_id, customer_id, total_amountNUMBER, NUMBER, NUMBER(12,2)标量,金融精度要求
订单详情行(一对多)NESTED TABLE order_items需 SQL 关联查询、动态数量、持久化
订单状态流转历史(时间序列)VARRAY(100) OF VARCHAR2(20)固定上限、严格顺序、高频读取
缓存热门商品 ID 列表(PL/SQL 内部)TYPE prod_id_tab IS TABLE OF NUMBER INDEX BY PLS_INTEGER关联数组,O(1) 查找,无需持久化
用户收货地址(复用性强)ADDRESS_OBJ对象类型,被订单、用户、发票等多处引用,强类型保障

🌟 六、结语:类型即契约,契约即生产力

PL/SQL 的标量与复合类型系统,绝非语法糖或历史包袱。它是 Oracle 将数据语义、内存模型、SQL 引擎能力、应用集成协议熔铸一体的精密设计。PLS_INTEGER 的 3 倍性能提升背后,是 Oracle 对栈分配与 CPU 寄存器优化的深刻理解;TIMESTAMP WITH LOCAL TIME ZONE 的自动时区转换,是对全球化业务时序一致性的底层支撑;而 OBJECT TYPE 与 Java SQLData 的无缝映射,则让数据库不再只是“数据仓库”,而成为领域模型的权威源头

掌握这些类型,不是为了背诵手册,而是为了在每次 DECLARE 时,都能清晰听见数据在内存中的脉动,在每次 INSERT 时,都确信业务规则已固化于类型契约之中,在每次 Java 调用 getObject() 时,都享受强类型带来的安心与效率。

🔗 延伸学习:Oracle 官方《PL/SQL Language Reference》是您值得反复研读的圣经;而《JDBC Developer’s Guide》则是打通数据库与 Java 世界的桥梁手册。

类型系统不会替你写业务逻辑,但它会默默守护每一次赋值、每一次查询、每一次跨语言调用的正确性与性能。当你开始敬畏类型,你就真正踏入了专业 Oracle 开发的大门。🚀

到此这篇关于Oracle PL/SQL中两大数据类型:标量类型与复合类型的文章就介绍到这了,更多相关Oracle 数据类型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论