4.1 SQL Data Types and Schemas
内置数据类型拓展
在基础SQL数据类型外,支持用户自定义类型/域,用于标准化属性类型,提升代码可读性和一致性。
1. 自定义类型(CREATE TYPE)
| create type Dollars as numeric(12,2); -- 定义美元类型为定点数
create table department(
dept_name varchar(20),
building varchar(15),
budget Dollars -- 直接使用自定义类型
);
|
2. 自定义域(CREATE DOMAIN)
支持为域添加约束(如NOT NULL)和默认值,是对基础类型的约束化封装:
| create domain Dollars as numeric(12,2) not null; -- 非空约束
create domain Pounds as numeric(12,2);
create table instructor(
ID char(5) primary key,
name varchar(20),
dept_name varchar(20),
salary Dollars, -- 应用自定义域
comm Pounds
);
|
3. TYPE与DOMAIN的核心区别
- Domain:可添加约束(NOT NULL/CHECK)、定义默认值;弱类型,底层类型兼容时可互相赋值。
- Type:无内置约束;强类型,类型不同即使底层兼容也不能直接赋值。
4. 大对象类型(Large-Object Types)
用于存储大文件(图片/视频/CAD文件),查询仅返回指针而非实际数据,避免大数据传输开销:
- blob:二进制大对象,存储无解释的二进制数据。
- clob:字符大对象,存储大量字符数据。
5. 目录、模式与环境(Catalogs, Schemas and Environments)
数据库的三层逻辑结构,用于分级管理数据库对象,避免命名冲突:
- Catalog(目录):最高级,包含多个模式,对应物理上的数据库实例。
- Schema(模式):目录下的子级,包含表、视图、索引等数据库对象(如
university模式下的instructor/department表)。 - 数据库对象:模式下的具体元素(表、视图、触发器等)。
4.2 Integrity Constraints(完整性约束)
核心定义
完整性约束是数据库实例必须遵循的规则,由DBMS自动维护,防止授权的数据库修改导致数据一致性丢失,避免意外的脏数据/无效数据。
约束分类
对应关系型数据库四大完整性:
- 域完整性:属性值的取值范围约束。
- 实体完整性:主键约束(非空+唯一)。
- 参照完整性:外键约束(引用的主键必须存在)。
- 用户定义的完整性:自定义CHECK/断言/触发器等。
4.2.1 单关系上的约束
直接在CREATE TABLE中定义,约束单个表的属性/元组:
- NOT NULL:属性禁止空值。
- PRIMARY KEY:主键(非空+唯一,可多列联合)。
- UNIQUE:候选键,属性值唯一(允许空值)。
- CHECK(P):谓词P为真时元组才合法,支持域级/表级CHECK。
示例:表级多约束组合
| CREATE TABLE instructor2(
ID char(5) primary key,
name varchar(20) not null,
dept_name varchar(20),
salary numeric(8,2) not null,
check (salary >= 0) -- 工资非负约束
);
|
示例:域级CHECK约束(带约束名)
为域添加约束并命名,便于定位约束违规原因:
| create domain hourly-wage numeric(5,2)
constraint value-test check(value >= 4.00); -- 时薪不低于4.00
|
4.2.2 Referential Integrity(参照完整性约束)
1. 形式化定义
设关系r1(R1)(被参照关系)主键为K1,r2(R2)(参照关系)的子集α为外键,对r2中每个元组t2,必存在r1中元组t1满足t1[K1] = t2[α],即: \(\prod_\alpha (r2) \subseteq \prod_{K1} (r1)\)
2. 核心概念
- 参照关系:包含外键的表(如
学生表)。 - 被参照关系(目标关系):外键引用的表(如
专业表)。 - 外键规则:参照关系的外键值必须在被参照关系的主键中存在,或为NULL。
示例:经典参照关系
| 学生(学号,姓名,专业号) -- 参照关系,专业号为外键
专业(专业号,专业名称) -- 被参照关系,专业号为主键
instructor(dept_name) -- 参照关系
department(dept_name) -- 被参照关系
|
3. 数据库修改时的参照完整性检查
对r2(外键α)引用r1(主键K)的场景,DBMS会在插入/删除/更新时自动检查约束,避免违规:
(1)插入(Insert)
向r2插入元组t2时,必须满足\(t2[α] ∈ \prod_K(r1)\),否则拒绝插入。
(2)删除(Delete)
从r1删除元组t1时,先查找r2中引用t1的元组\(σ_α=t1[K](r2)\):
- 若该集合非空:拒绝删除,或级联删除
r2中所有引用元组(支持多级级联)。
(3)更新(Update)
- 若更新
r2的外键α:检查新值是否在r1的主键中,否则拒绝。 - 若更新
r1的主键K:同删除规则,拒绝或级联更新r2的外键值。
4. SQL中的参照完整性实现
(1)外键定义的3种方式
| -- 方式1:表级定义,默认引用被参照表主键
create table instructor(
ID char(5) primary key,
dept_name varchar(20),
foreign key (dept_name) references department
);
-- 方式2:列级简写,单列外键直接跟在属性后
create table instructor(
ID char(5) primary key,
dept_name varchar(20) references department
);
-- 方式3:显式指定被参照列(需是被参照表的主键/候选键,列名可不同)
create table instructor(
ID char(5) primary key,
dept_name varchar(20),
foreign key (dept_name) references department (dept_name)
);
|
(2)级联操作(Cascading Actions)
通过ON DELETE/ON UPDATE指定引用关系的级联规则,核心规则:
- ON DELETE CASCADE:删除被参照表元组时,级联删除参照表中所有引用元组。
- ON UPDATE CASCADE:更新被参照表主键时,级联更新参照表中所有引用的外键值。
- 支持多级级联:若存在外键依赖链,级联操作会沿链传播。
示例:级联操作定义
| create table course(
course_id varchar(8) primary key,
dept_name varchar(20),
foreign key (dept_name) references department
on delete cascade -- 删除院系时,级联删除该院系的所有课程
on update cascade -- 更新院系名时,级联更新课程的院系名
);
|
(3)级联的替代规则
若不希望级联删除/更新,可指定外键置空/置默认值:
ON DELETE SET NULL:删除被参照元组时,将参照表的外键置为NULL(需外键允许NULL)。 ON DELETE SET DEFAULT:删除被参照元组时,将参照表的外键置为默认值。
(4)可延迟约束(Deferrable Constraint)
默认情况下,约束立即检查(每一步操作都需满足);可延迟约束则延迟到事务结束时检查,解决双向引用的插入问题(如夫妻关系表):
- 场景:
marriedperson(name, spouse),spouse引用同表的name,插入夫妻双方时,单独插入一方会因外键违规被拒绝,延迟约束则允许事务内先插入双方,最后统一检查。
(5)外键NULL的特殊规则
若外键的任意属性为NULL,则该元组默认满足外键约束(无需匹配被参照表),因此建议通过NOT NULL约束外键,避免隐性违规。
4.2.3 Assertions(断言)
1. 核心定义
断言是对数据库全局的自定义谓词,要求数据库始终满足该谓词,用于实现跨多个关系的复杂约束(单表约束用CHECK,多表约束用断言)。
2. SQL语法
| CREATE ASSERTION <断言名> CHECK <谓词>;
|
- DBMS会在任何可能违反断言的更新操作后自动检查谓词,若为假则拒绝更新。
- 注意:断言检查会带来较大性能开销,需谨慎使用。
3. 关键技巧:全称量词的转换
SQL不直接支持全称量词∀,需通过存在量词的否定转换: $\(\forall x P(x) \equiv \neg \exists x \neg P(x)\)$ (所有x满足P(x) = 不存在x不满足P(x))
示例1:学生总学分等于已修课程学分和
要求student表的tot_cred等于该学生修完(成绩非F且非NULL)的课程学分总和:
| create assertion credits_earned_constraint check(
not exists(
select ID from student
where tot_cred <> (
select sum(credits)
from takes natural join course
where student.ID = takes.ID
and grade is not null and grade <> 'F'
)
)
);
|
示例2:教师同一时间不能在两个教室授课
| create assertion instructor_classroom_constraint check(
not exists(
select ID, semester, year, time_slot_id
from instructor natural join teaches natural join section
group by ID, semester, year, time_slot_id
having count(distinct building, room_number) > 1
)
);
|
4.2.4 Triggers(触发器)
1. 核心定义
触发器是数据库修改时自动执行的语句,由触发事件和触发动作组成,用于实现复杂的自定义业务规则(如数据校验、日志记录、外部联动)。 - SQL:1999纳入标准,各数据库(Oracle/SQL Server)有非标准拓展。 - 与断言的区别:断言是声明式约束(只定义规则,不定义动作),触发器是过程式约束(定义触发条件和具体执行的动作)。
2. 触发器的设计要素
- 触发事件:触发触发器的操作(
INSERT/DELETE/UPDATE,UPDATE可指定具体属性)。 - 触发时机:
BEFORE(操作前执行)/AFTER(操作后执行)。 - 触发对象:哪个表/视图的操作会触发。
- 触发条件:满足谓词时才执行动作。
- 触发动作:条件满足时执行的SQL语句(可回滚/插入/更新/删除)。
3. 触发器的核心语法(SQL:1999)
| create trigger <触发器名> <触发时机> <触发事件> on <表名>
referencing <旧元组/新元组> as <别名>
for each row/statement -- 行级/语句级触发器
when <触发条件>
begin
<触发动作> -- 如rollback/insert/update
end;
|
关键概念
- 行级触发器(FOR EACH ROW):操作影响每一行都执行一次动作(常用)。
- 语句级触发器(FOR EACH STATEMENT):无论操作影响多少行,仅执行一次动作(高效,适合批量操作)。
- 过渡元组/表:
OLD ROW AS orow:删除/更新前的旧元组(仅DELETE/UPDATE可用)。 NEW ROW AS nrow:插入/更新后的新元组(仅INSERT/UPDATE可用)。 OLD TABLE/NEW TABLE:语句级触发器的过渡表,存储所有受影响的旧/新元组。
4. 经典示例
示例1:检查section表的time_slot_id是否存在(插入后校验)
| create trigger timeslot_check1 after insert on section
referencing new row as nrow
for each row
when (nrow.time_slot_id not in (select time_slot_id from time_slot))
begin
rollback; -- 不存在则回滚插入操作
end;
|
示例2:删除time_slot表元组时校验引用(删除后校验)
| create trigger timeslot_check2 after delete on time_slot
referencing old row as orow
for each row
when (orow.time_slot_id not in (select time_slot_id from time_slot)
and orow.time_slot_id in (select time_slot_id from section))
begin
rollback; -- 最后一个该时段被删除且被section引用,回滚
end;
|
示例3:库存低于警戒线时自动生成订货单(更新后触发)
| -- 库存表/警戒线表/订货量表/订单表
create trigger reorder-trigger after update of level on inventory
referencing old row as orow, new row as nrow
for each row
when (nrow.level <= (select level from minlevel where minlevel.item = nrow.item)
and orow.level > (select level from minlevel where minlevel.item = orow.item))
begin
insert into orders (select item, amount from reorder where reorder.item = orow.item);
end;
|
- 触发条件:本次更新导致库存从警戒线以上降至以下(避免重复生成订单)。
5. 各数据库的触发器语法拓展
(1)SQL Server触发器
- 用
inserted/deleted过渡表替代nrow/orow,无显式for each row,默认行级。 - 示例:账户余额为负时自动创建贷款记录
| create trigger overdraft-trigger on account
for update as
if inserted.balance < 0
begin
insert into borrower (select customer-name, account-number from depositor, inserted where inserted.account-number = depositor.account-number);
insert into loan values (inserted.account-number, inserted.branch-name, -inserted.balance);
update account set balance = 0 from account, inserted where account.account-number = inserted.account-number;
end;
|
(2)Oracle触发器
- 支持
BEFORE触发,可直接抛出异常阻止操作;支持系统函数(如sysdate)。 - 示例:仅允许工作时间修改student表
| create or replace trigger secure_student
before insert or delete or update on student
begin
if (to_char(sysdate, 'DY') in ('星期六', '星期日'))
or (to_char(sysdate, 'HH24') not between 8 and 17)
then
raise_application_error(-20506, '你只能在上班时间修改数据');
end if;
end;
|
- 删除触发器:
drop trigger <触发器名>;
6. 触发器的适用场景与禁用场景
适用场景
- 实现跨表的复杂业务规则(如库存联动、日志自动记录)。
- 实现外部系统联动(如触发器记录待执行动作到表,外部程序轮询执行)。
- 实现数据校验的自定义错误提示。
禁用场景(有更优方案)
- 维护汇总数据(如部门总工资):用物化视图(Materialized View)更高效。
- 数据库复制:用数据库内置复制功能,而非触发器记录变更。
7. CHECK/Assertion/Trigger 对比
| 约束类型 | 作用范围 | 实现方式 | 性能 | 适用场景 |
| CHECK | 单表/单域 | 声明式(仅定义规则) | 高 | 简单的单表属性/元组约束 |
| Assertion | 全局(多表) | 声明式(仅定义规则) | 中(有开销) | 跨多表的通用约束,无需自定义动作 |
| Trigger | 任意(单/多表) | 过程式(定义规则+动作) | 低(行级触发开销大) | 复杂业务规则,需执行自定义动作(如插入/更新/外部联动) |
4.3 Authorization(授权)
核心定义
授权是数据库的安全机制,用于限制用户对数据库对象的访问和操作权限,防止恶意窃取/修改数据,分为数据库级/操作系统级/网络级/物理级/人员级安全,此处重点讲数据库级的SQL授权。
4.3.1 授权的类型
1. 对数据库数据的操作权限(DML权限)
- Read(SELECT):读取数据,不可修改。
- Insert:插入新数据,不可修改现有数据。
- Update:修改数据,不可删除。
- Delete:删除数据。
2. 对数据库模式的修改权限(DDL权限)
- Index:创建/删除索引。
- Resources:创建新关系(表)。
- Alteration:添加/修改表的属性。
- Drop:删除关系(表)/视图。
4.3.2 视图与授权
视图是实现细粒度授权的核心工具,通过视图隐藏敏感数据,仅将视图的访问权限授予用户,而不授予基表的权限:
- 优势:既简化用户操作,又提升安全性,实现数据最小访问原则。
- 规则:创建视图不需要Resources权限(视图是虚表);视图的创建者仅拥有基表已有的权限,无法通过视图获得额外权限。
示例:银行职员仅能查看客户-支行对应关系,不可查看贷款金额
| -- 定义视图,隐藏贷款敏感信息
create view cust-loan as
select branchname, customer-name
from borrower, loan
where borrower.loan-number = loan.loan-number;
-- 仅授予视图的SELECT权限,不授予基表权限
grant select on cust-loan to bank_clerk;
|
- 当用户查询视图时,DBMS会先检查用户对视图的权限,再将视图查询转换为基表查询执行(无需检查基表权限)。
4.3.3 权限的授予(GRANT)
1. 核心语法
| GRANT <权限列表> ON <表/视图名> TO <用户/角色/PUBLIC> [WITH GRANT OPTION];
|
- 权限列表:可指定单个权限(如SELECT)、多个权限(逗号分隔),或
ALL PRIVILEGES(所有可用权限)。 - 用户列表:
- 单个/多个用户ID(逗号分隔)。
PUBLIC:所有有效用户。
- Role(角色):一组用户的权限集合(推荐使用,简化权限管理)。
- WITH GRANT OPTION:被授权用户可将相同权限转授给其他用户(权限传递)。
- 规则:授权者必须已拥有该权限(或为DBA),否则授权失败。
2. 示例:基础授权
| -- 授予U1/U2/U3对instructor表的SELECT和INSERT权限
grant select, insert on instructor to U1, U2, U3;
-- 授予U1对department表的SELECT权限,且U1可转授该权限
grant select on department to U1 with grant option;
|
3. 角色(Role)
为同一类用户创建统一的角色,将权限授予角色,再将角色分配给用户,避免对单个用户重复授权,简化权限管理(SQL:1999支持)。
角色的核心操作
| -- 1. 创建角色
create role instructor;
create role dean;
-- 2. 为角色授予权限
grant select on takes to instructor;
-- 3. 角色嵌套:将instructor角色的权限授予dean角色
grant instructor to dean;
-- 4. 将角色分配给用户
grant dean to Amit, Satoshi;
|
4.3.4 权限的回收(REVOKE)
1. 核心语法
| REVOKE <权限列表> ON <表/视图名> FROM <用户/角色/PUBLIC> [RESTRICT | CASCADE];
|
- RESTRICT(限制回收):若回收的权限被用户转授给他人,回收失败(默认)。
- CASCADE(级联回收):若回收的权限被用户转授给他人,同时回收被转授的权限(沿授权链传播)。
- 规则:若同一权限被多个授权者授予,回收其中一个授权者的权限,用户仍保留该权限(需所有授权者都回收)。
2. 示例:权限回收
| -- 级联回收U1/U3对department表的SELECT权限(包括他们转授的权限)
revoke select on department from U1, U3 cascade;
-- 限制回收U1对instructor表的INSERT权限,若U1转授过则失败
revoke insert on instructor from U1 restrict;
-- 回收所有用户对course表的DELETE权限
revoke delete on course from PUBLIC;
|
4.3.5 授权图(Authorization Graph)
用有向图表示权限的传递关系,便于理解权限的回收规则:
- 节点:用户/角色/DBA(根节点)。
- 边:
Ui → Uj表示Ui将权限授予Uj(可带WITH GRANT OPTION)。 - 核心规则:所有边必须属于从DBA出发的路径,若DBA回收某用户的权限,所有从该用户出发的边都被删除(级联回收)。
示例:授权图的级联回收
| DBA → U1 → U4
DBA → U2 → U5
DBA → U3
|
- 若DBA回收U1的权限,U4的权限也被级联回收;U5的权限保留(因有DBA→U2的路径)。
4.3.6 SQL授权的局限性
- 不支持元组级授权:无法限制用户仅查看/修改表中的部分元组(如学生仅查看自己的成绩),需通过应用程序实现。
- Web应用的权限映射问题:Web应用的所有终端用户通常映射到单个数据库用户ID,数据库级授权失效,需通过应用程序的权限系统实现细粒度授权。
- 应用程序级授权的优缺点:
- 优点:可实现元组级/列级的细粒度授权。
- 缺点:授权逻辑分散在应用代码中,难以检查漏洞,维护成本高。
4.4 Audit Trails(审计跟踪)
核心定义
审计跟踪是数据库所有修改操作的日志,记录操作类型(插入/删除/更新)、操作用户、操作时间、操作对象等信息,用于追踪错误/欺诈性的数据库修改,是数据库安全的最后一道防线。 - 实现方式:可通过触发器自定义日志,多数数据库(Oracle/SQL Server)提供内置审计功能(更高效)。
Oracle中的审计实现
Oracle支持语句审计和对象审计,审计结果存储在系统数据字典中(仅DBA可查看)。
1. 语句审计(Audit Statement)
审计某类SQL语句的执行(如创建/删除表),语法:
| AUDIT <语句类型> [BY <用户>] [BY SESSION/ACCESS] [WHENEVER SUCCESSFUL/NOT SUCCESSFUL];
|
- 语句类型:
table(create/drop/alter table)、view、index、role等。 - BY SESSION:同一会话中,相同类型的语句仅记录一次(默认)。
- BY ACCESS:每执行一次语句就记录一次(详细日志)。
- WHENEVER SUCCESSFUL:仅审计执行成功的语句;
NOT SUCCESSFUL仅审计失败的语句。
示例:语句审计
| -- 审计用户scott每次成功执行的表相关语句(create/drop/alter)
audit table by scott by access whenever successful;
|
2. 对象审计(Audit Object)
审计对具体数据库对象的操作(如对student表的增删改查),语法:
| AUDIT <操作类型> ON <对象名/DEFAULT> [BY SESSION/ACCESS] [WHENEVER SUCCESSFUL/NOT SUCCESSFUL];
|
- 操作类型:
insert、delete、update、select、grant等。 - ON DEFAULT:对后续创建的所有对象生效(批量审计)。
- 规则:对象审计对所有用户生效,无需指定用户。
示例:对象审计
| -- 审计所有用户对student表的delete和update操作(无论成功与否)
audit delete, update on student;
|
3. 取消审计(NOAUDIT)
语法与AUDIT一致,将AUDIT替换为NOAUDIT即可:
| -- 取消对scott的表语句审计
noaudit table by scott;
-- 取消对student表的delete和update审计
noaudit delete, update on student;
|
4. 查看审计结果
审计结果存储在Oracle系统数据字典中,DBA可通过以下视图查询:
sys.aud$:原始审计日志表。 dba_audit_trail:所有审计记录。 dba_audit_statement:语句审计记录。 dba_audit_object:对象审计记录。
审计经典例题
题目
现有Person表定义:
| create table Person(
id char(10) primary key,
name varchar(12) not null,
age int,
gender char(1),
spouse char(10),
foreign key(spouse) references Person
on update cascade
on delete set NULL,
check(gender in ('f', 'm'))
);
|
执行以下操作: | delete from Person where name='Wang'; -- Wang的spouse为Ling
select count(spouse) from Person;
|
已知Person表原有数据中,Wang(id05)的spouse为Ling(id08),Ling的spouse为Wang,其他元组spouse非空。求count(spouse)的结果。 答案
7
- 解析:
delete Wang触发ON DELETE SET NULL,Ling的spouse被置为NULL;count(spouse)忽略NULL值,原有9个元组,删除1个(Wang),剩余8个,其中Ling的spouse为NULL,故计数为7。
4.5 Embedded SQL(嵌入式SQL)
核心定义
SQL是非过程化语言,缺乏计算、循环、分支等过程化能力,嵌入式SQL将SQL语句嵌入到高级过程化语言(C/Java/Pascal)中,结合SQL的数据操作能力和宿主语言的过程化能力,实现复杂的数据库应用。
- 宿主语言:嵌入SQL的过程化语言(如C)。
- 嵌入式SQL语句:以
EXEC SQL开头,END_EXEC结尾(不同语言略有差异),由预编译器转换为宿主语言可执行的代码。
核心概念
- 宿主变量:宿主语言中的变量,用于在宿主语言和SQL之间传递数据,SQL中引用时加冒号(:),宿主语言中直接使用。
- SQLCA(SQL Communication Area):SQL通讯区,存储SQL语句的执行状态(如成功/失败、错误码),核心变量
SQLSTATE/sqlcode。 - 游标(Cursor):用于处理SQL的多行查询结果,宿主语言一次只能处理一行,通过游标遍历结果集。
4.5.1 嵌入式SQL的基本流程
- 声明宿主变量:在
EXEC SQL BEGIN DECLARE SECTION和EXEC SQL END DECLARE SECTION之间声明。 - 执行SQL语句:单行操作直接执行,多行操作通过游标。
- 处理执行结果:通过SQLCA判断执行状态,处理宿主变量中的数据。
- 释放资源:关闭游标、释放连接。
4.5.2 单行查询(无游标)
适用于查询结果仅返回一行的场景,通过INTO将查询结果赋值给宿主变量。
示例(Oracle Pro*C)
| EXEC SQL BEGIN DECLARE SECTION;
char V_an[20], bn[20]; // 宿主变量:账号、支行名
float bal; // 宿主变量:余额
EXEC SQL END DECLARE SECTION;
scanf("%s", V_an); // 宿主语言读入账号
// 嵌入式SQL:根据账号查询支行名和余额,赋值给宿主变量
EXEC SQL SELECT branch_name, balance INTO :bn, :bal
FROM account WHERE account_number = :V_an;
END_EXEC
printf("账号:%s,支行:%s,余额:%f", V_an, bn, bal); // 宿主语言处理数据
|
4.5.3 多行查询(游标)
适用于查询结果返回多行的场景,通过游标逐行遍历结果集,核心步骤:声明游标→打开游标→获取数据→关闭游标。
示例(Oracle Pro*C):查询余额大于指定值的客户名和城市
| EXEC SQL BEGIN DECLARE SECTION;
char cn[50], ccity[50]; // 宿主变量:客户名、城市
float v_amount = 1000; // 宿主变量:指定余额
EXEC SQL END DECLARE SECTION;
// 步骤1:声明游标,关联查询语句
EXEC SQL DECLARE c CURSOR FOR
SELECT customer_name, customer_city
FROM depositor D, customer B, account A
WHERE D.customer_name = B.customer_name
and D.account_number = A.account_number
and A.balance > :v_amount;
END_EXEC
// 步骤2:打开游标,执行查询
EXEC SQL OPEN c END_EXEC;
// 步骤3:循环获取数据,直到结果集结束
while(1) {
EXEC SQL FETCH c INTO :cn, :ccity END_EXEC;
// SQLSTATE='02000'表示无更多数据
if (strcmp(SQLSTATE, "02000") == 0) break;
printf("客户名:%s,城市:%s\n", cn, ccity);
}
// 步骤4:关闭游标,释放资源
EXEC SQL CLOSE c END_EXEC;
|
4.5.4 数据库更新(插入/删除/更新)
1. 单行更新
直接执行嵌入式SQL的更新语句,通过宿主变量传递参数。
| EXEC SQL BEGIN DECLARE SECTION;
char an[20];
float bal;
EXEC SQL END DECLARE SECTION;
scanf("%s,%f", an, &bal); // 读入账号和增加的余额
// 嵌入式SQL:更新账号余额
EXEC SQL UPDATE account SET balance = balance + :bal
WHERE account_number = :an;
END_EXEC
|
2. 多行更新(带游标的定位更新)
通过游标定位到要更新的行,使用WHERE CURRENT OF <游标名>执行定位更新/删除,需在声明游标时加FOR UPDATE OF <属性>。
| // 声明游标,指定可更新balance属性
EXEC SQL DECLARE csr CURSOR FOR
SELECT * FROM account WHERE branch_name = 'Perryridge'
FOR UPDATE OF balance;
END_EXEC
EXEC SQL OPEN csr;
while(1) {
EXEC SQL FETCH csr INTO :an, :bn, :bal;
if (sqlca.sqlcode != SUCCESS) break;
// 定位更新:更新游标当前指向的行的余额
EXEC SQL UPDATE account SET balance = balance + 100
WHERE CURRENT OF csr;
// 定位删除:删除游标当前指向的行
// EXEC SQL DELETE FROM account WHERE CURRENT OF csr;
}
EXEC SQL CLOSE csr;
|
4.5.5 嵌入式SQL的编译执行流程(Pro*C)
| Pro*C源程序(.pc)→ 预编译器(pcc)→ C源程序(.c)→ C编译器(cc)→ 可执行文件(.exe)
|
- 预编译器的作用:将嵌入式SQL语句转换为C语言的函数调用,实现与数据库的交互。
4.6 Dynamic SQL(动态SQL)
核心定义
动态SQL允许在程序运行时动态构造SQL语句(字符串形式),并提交给DBMS执行,适用于SQL语句结构不确定的场景(如用户自定义查询条件、动态表名/列名)。
- 与嵌入式SQL的区别:嵌入式SQL的语句在编译时固定,动态SQL的语句在运行时生成。
核心语法(C语言)
| EXEC SQL PREPARE <动态语句名> FROM :<SQL字符串变量>; -- 预编译动态SQL
EXEC SQL EXECUTE <动态语句名> [USING :<宿主变量列表>]; -- 执行动态SQL,传递参数
|
- 占位符(?):动态SQL字符串中用
?表示参数位置,执行时通过USING将宿主变量赋值给占位符,避免SQL注入。
示例:动态更新账户余额
| char *sqlprog = "update account set balance = balance * 1.05 where account_number = ?";
char v_account[10] = "A_101";
// 预编译动态SQL语句
EXEC SQL PREPARE dynprog FROM :sqlprog;
// 执行动态SQL,将v_account赋值给占位符?
EXEC SQL EXECUTE dynprog USING :v_account;
|
4.7 数据库访问接口:ODBC & JDBC
嵌入式SQL和动态SQL依赖数据库专用的预编译器,跨数据库兼容性差;ODBC/JDBC是标准化的数据库访问API,实现应用程序与数据库的解耦,一套代码可访问不同数据库(MySQL/Oracle/SQL Server)。
4.7.1 ODBC(Open DataBase Connectivity,开放数据库连接)
核心定义
ODBC是微软推出的跨平台、跨数据库的C语言API,为应用程序提供统一的数据库访问接口,底层由驱动程序管理器和数据库专用驱动实现与不同数据库的交互。
- 优势:无需预编译,直接通过C函数调用执行SQL,跨数据库兼容性强。
- 架构:应用程序 → ODBC API → 驱动程序管理器 → 数据库驱动 → 数据源(数据库)。
ODBC的核心对象(句柄)
ODBC通过句柄(Handle)管理资源,所有操作都基于句柄,核心句柄: 1. 环境句柄(HENV):最高级句柄,管理数据库环境,一个应用程序一个。 2. 连接句柄(HDBC):管理与具体数据源的连接,一个环境句柄可对应多个连接句柄。 3. 语句句柄(HSTMT):管理SQL语句的执行,一个连接句柄可对应多个语句句柄。
ODBC编程的基本流程
| // 1. 分配环境句柄
HENV henv;
SQLAllocEnv(&henv);
// 2. 分配连接句柄
HDBC hdbc;
SQLAllocConnect(henv, &hdbc);
// 3. 连接数据源(DSN:数据源名,用户/密码)
SQLConnect(hdbc, "MySQLServer", SQL_NTS, "user", SQL_NTS, "password", SQL_NTS);
// 4. 分配语句句柄
HSTMT hstmt;
SQLAllocStmt(hdbc, &hstmt);
// 5. 执行SQL语句
// 5.1 直接执行(适合简单语句)
char *sql = "select dept_name, sum(salary) from instructor group by dept_name";
SQLExecDirect(hstmt, sql, SQL_NTS);
// 5.2 预编译执行(适合重复执行的语句,带占位符)
// SQLPrepare(hstmt, sql, SQL_NTS);
// SQLExecute(hstmt);
// 6. 绑定结果集到宿主变量(将查询结果赋值给C变量)
char deptname[80];
float salary;
SQLBindCol(hstmt, 1, SQL_C_CHAR, deptname, 80, NULL);
SQLBindCol(hstmt, 2, SQL_C_FLOAT, &salary, 0, NULL);
// 7. 遍历结果集
while (SQLFetch(hstmt) == SQL_SUCCESS) {
printf("院系:%s,总工资:%f\n", deptname, salary);
}
// 8. 释放资源
SQLFreeStmt(hstmt, SQL_DROP); // 释放语句句柄
SQLDisconnect(hdbc); // 断开连接
SQLFreeConnect(hdbc); // 释放连接句柄
SQLFreeEnv(henv); // 释放环境句柄
|
ODBC的一致性级别
ODBC定义了三级一致性级别,规定了不同级别支持的功能,便于驱动实现: 1. Core(核心级):支持基础SQL操作。 2. Level 1:增加元数据查询功能(如查询表结构)。 3. Level 2:增加批量参数传递、详细目录信息查询等功能。
4.7.2 JDBC(Java Database Connectivity)
核心定义
JDBC是Sun推出的Java语言专用的数据库访问API,基于Java的跨平台特性,实现一次编写,到处运行,底层由数据库驱动类实现与不同数据库的交互。 - 与ODBC的区别:ODBC是C语言API,有平台依赖;JDBC是Java API,纯跨平台,无需驱动程序管理器。 - 架构:Java应用程序 → JDBC API → 数据库驱动(JDBC Driver) → 数据库。
JDBC编程的基本流程
| import java.sql.*;
public class JDBCExample {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try {
// 1. 加载数据库驱动(Oracle驱动)
Class.forName("oracle.jdbc.driver.OracleDriver");
// 2. 建立数据库连接(URL:jdbc:数据库类型://地址:端口/数据库名,用户/密码)
conn = DriverManager.getConnection(
"jdbc:oracle:thin:@aura.bell_labs.com:2000:universitydb",
"userid",
"passwd"
);
// 3. 创建Statement对象,执行SQL语句
stmt = conn.createStatement();
// 4. 执行查询,获取结果集
String sql = "select dept_name, avg(salary) from instructor group by dept_name";
rset = stmt.executeQuery(sql);
// 5. 遍历结果集
while (rset.next()) {
// 通过列名/列索引获取数据
String deptName = rset.getString("dept_name");
float avgSalary = rset.getFloat(2);
System.out.println(deptName + " " + avgSalary);
}
// 执行更新操作(插入/删除/更新)
// stmt.executeUpdate("insert into instructor values('76766', 'Crick', 'Biology', 72000)");
} catch (ClassNotFoundException e) {
e.printStackTrace(); // 驱动加载失败
} catch (SQLException e) {
e.printStackTrace(); // SQL执行失败
} finally {
// 6. 释放资源(逆序释放)
try { if (rset != null) rset.close(); } catch (SQLException e) {}
try { if (stmt != null) stmt.close(); } catch (SQLException e) {}
try { if (conn != null) conn.close(); } catch (SQLException e) {}
}
}
}
|
JDBC的核心对象
- DriverManager:驱动管理器,加载驱动并建立数据库连接。
- Connection:数据库连接对象,管理与数据库的连接。
- Statement:执行静态SQL语句的对象,适合简单语句。
- PreparedStatement:执行预编译SQL语句的对象,支持占位符(?),避免SQL注入,适合重复执行的语句。
- ResultSet:查询结果集对象,遍历查询结果。
PreparedStatement示例(预编译,防SQL注入)
| // 预编译SQL语句,带4个占位符
String sql = "insert into instructor values(?, ?, ?, ?)";
PreparedStatement pStmt = conn.prepareStatement(sql);
// 为占位符赋值(索引从1开始)
pStmt.setString(1, "76766");
pStmt.setString(2, "Crick");
pStmt.setString(3, "Biology");
pStmt.setFloat(4, 72000);
// 执行更新
pStmt.executeUpdate();
// 重新赋值,执行第二次
pStmt.setString(1, "76767");
pStmt.executeUpdate();
|
JDBC处理NULL值
JDBC获取NULL值时,会返回对应数据类型的默认值(如int返回0,String返回null),需通过wasNull()判断是否为数据库的NULL值:
| int totCred = rset.getInt("tot_cred");
if (rset.wasNull()) {
System.out.println("总学分为NULL");
}
|
4.7.3 其他数据库访问技术
- OLE DB:微软推出的底层COM接口,支持关系型/非关系型数据库,速度快,编程量大。
- ADO:基于OLE DB的高层接口,简化数据库访问,适合VB/VC++开发。
- DAO:基于Microsoft Jet引擎,专为Access数据库设计,性能优异。
参考习题
7th Edition Ex. 4.7, 4.18, 4.20, 5.1, 5.7