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()vssetBigDecimal()影响 Oracle 执行计划缓存命中率; - ✅ 异常传播路径:
NO_DATA_FOUND仅在SELECT INTO无结果时抛出,若目标为RECORD类型则行为一致;但若目标为VARRAY,则需显式检查COUNT; - ✅ 跨语言契约:Java 的
java.sql.Types.VARCHAR映射 PL/SQLVARCHAR2,而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_INTEGER | 32 位有符号整数,PL/SQL 内部优化类型 | -2³¹ ~ 2³¹−1 | 循环索引、数组下标(性能敏感场景) | int |
PLS_INTEGER | BINARY_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 引擎不支持的类型!意味着你无法在 SELECT、INSERT 或表定义中使用 BOOLEAN。
| 类型 | 值域 | 用途 | Java 注意点 |
|---|---|---|---|
BOOLEAN | TRUE / 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.LocalDateTime 或 java.sql.Timestamp |
TIMESTAMP WITH TIME ZONE | 带时区偏移的时间戳 | 纳秒 | ✅ 保存 +08:00 或 Asia/Shanghai | java.time.OffsetDateTime 或 ZonedDateTime |
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运算比NUMBER快 3.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 结果集 |
| 自定义 RECORD | TYPE 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 必须通过以下方式之一处理:
- 解构为 OUT 参数(最常用、最兼容)
- 封装为
OBJECT TYPE并注册 STRUCT(面向对象风格) - 返回关联游标(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 TABLE、VARRAY。它们的关键差异在于存储位置、大小约束、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-45673.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 TABLE 和 VARRAY(关联数组不支持)。
// 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.GETXML | SYS.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 Type | Java Type | 关键注意事项 |
|---|---|---|---|
NUMBER | Types.DECIMAL | BigDecimal | ✅ 精确;getInt()/getDouble() 会丢失精度 |
VARCHAR2 | Types.VARCHAR | String | ✅ 安全;getBytes() 注意字符集 |
DATE | Types.DATE | java.sql.Date | ⚠️ 仅日期;getTime() 返回毫秒(午夜) |
TIMESTAMP | Types.TIMESTAMP | Timestamp | ✅ 推荐;toInstant() 转 java.time.Instant |
CLOB | Types.CLOB | Clob | ✅ 流式读取;避免 getClob().getSubString() 加载全量 |
BLOB | Types.BLOB | Blob | ✅ 同上;用 getBinaryStream() |
OBJECT TYPE | OracleTypes.STRUCT | STRUCT 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_amount | NUMBER, 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 数据类型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Oracle 要慌了!华为终于开源了自家的 Huawei JDK——毕昇 JDK!
毕昇 JDK 是华为内部 OpenJDK 定制版 Huawei JDK 的开源版本,是一个高性能、可用于生产环境的 OpenJDK 发行版,感兴趣的朋友跟随小编一起看看吧2020-12-12
解决The Network Adapter could not establish the conn问题
这篇文章主要介绍了解决The Network Adapter could not establish the conn问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-02-02


最新评论