存储过程与触发器
本章将详细介绍PostgreSQL中的存储过程和触发器,包括如何创建、使用和管理这些数据库对象来实现复杂的业务逻辑。
8.1 存储过程概述
存储过程概念
存储过程是存储在数据库中的可执行代码块,具有以下特点:
- 预编译:存储过程在创建时被编译,执行效率高
- 模块化:可以将复杂的业务逻辑封装在存储过程中
- 安全性:通过存储过程控制数据访问权限
- 减少网络流量:在服务器端执行,减少客户端和服务器间的数据传输
PostgreSQL过程语言
PostgreSQL支持多种过程语言:
- PL/pgSQL:PostgreSQL的过程语言,类似Oracle的PL/SQL
- PL/Python:使用Python编写存储过程
- PL/Perl:使用Perl编写存储过程
- PL/Tcl:使用Tcl编写存储过程
8.2 PL/pgSQL基础
基本语法结构
-- PL/pgSQL函数基本结构
CREATE OR REPLACE FUNCTION function_name(parameter_name parameter_type)
RETURNS return_type AS $$
DECLARE
-- 变量声明
variable_name variable_type;
BEGIN
-- 函数体
-- SQL语句和控制结构
RETURN return_value;
EXCEPTION
-- 异常处理
WHEN condition THEN
-- 处理逻辑
END;
$$ LANGUAGE plpgsql;
变量声明和使用
CREATE OR REPLACE FUNCTION example_function()
RETURNS VOID AS $$
DECLARE
user_count INTEGER;
user_name VARCHAR(100);
current_time TIMESTAMP := CURRENT_TIMESTAMP;
avg_price NUMERIC(10,2);
BEGIN
-- 查询赋值
SELECT COUNT(*) INTO user_count FROM users;
-- 表达式赋值
user_name := 'John Doe';
-- 执行SQL语句
INSERT INTO logs (message, created_at)
VALUES ('Function executed', current_time);
RAISE NOTICE 'User count: %, Current time: %', user_count, current_time;
END;
$$ LANGUAGE plpgsql;
8.3 控制结构
条件语句
CREATE OR REPLACE FUNCTION check_user_status(user_id INTEGER)
RETURNS VARCHAR(20) AS $$
DECLARE
user_active BOOLEAN;
result VARCHAR(20);
BEGIN
SELECT is_active INTO user_active FROM users WHERE id = user_id;
IF user_active THEN
result := 'Active';
ELSE
result := 'Inactive';
END IF;
RETURN result;
END;
$$ LANGUAGE plpgsql;
-- 多条件判断
CREATE OR REPLACE FUNCTION get_user_level(score INTEGER)
RETURNS VARCHAR(20) AS $$
DECLARE
level VARCHAR(20);
BEGIN
IF score >= 90 THEN
level := 'Excellent';
ELSIF score >= 80 THEN
level := 'Good';
ELSIF score >= 60 THEN
level := 'Average';
ELSE
level := 'Poor';
END IF;
RETURN level;
END;
$$ LANGUAGE plpgsql;
循环语句
-- LOOP循环
CREATE OR REPLACE FUNCTION loop_example(max_count INTEGER)
RETURNS INTEGER AS $$
DECLARE
counter INTEGER := 1;
total INTEGER := 0;
BEGIN
LOOP
total := total + counter;
counter := counter + 1;
EXIT WHEN counter > max_count;
END LOOP;
RETURN total;
END;
$$ LANGUAGE plpgsql;
-- WHILE循环
CREATE OR REPLACE FUNCTION while_example(max_count INTEGER)
RETURNS INTEGER AS $$
DECLARE
counter INTEGER := 1;
total INTEGER := 0;
BEGIN
WHILE counter <= max_count LOOP
total := total + counter;
counter := counter + 1;
END LOOP;
RETURN total;
END;
$$ LANGUAGE plpgsql;
-- FOR循环
CREATE OR REPLACE FUNCTION for_example(max_count INTEGER)
RETURNS INTEGER AS $$
DECLARE
total INTEGER := 0;
i INTEGER;
BEGIN
FOR i IN 1..max_count LOOP
total := total + i;
END LOOP;
RETURN total;
END;
$$ LANGUAGE plpgsql;
-- FOR查询循环
CREATE OR REPLACE FUNCTION process_users()
RETURNS INTEGER AS $$
DECLARE
user_record RECORD;
processed_count INTEGER := 0;
BEGIN
FOR user_record IN SELECT id, username FROM users WHERE is_active = true LOOP
-- 处理每个用户
INSERT INTO user_logs (user_id, action, created_at)
VALUES (user_record.id, 'processed', CURRENT_TIMESTAMP);
processed_count := processed_count + 1;
END LOOP;
RETURN processed_count;
END;
$$ LANGUAGE plpgsql;
8.4 游标操作
游标基础
CREATE OR REPLACE FUNCTION cursor_example()
RETURNS TABLE(user_id INTEGER, username VARCHAR(100)) AS $$
DECLARE
user_cursor CURSOR FOR
SELECT id, username FROM users WHERE is_active = true;
user_record RECORD;
BEGIN
OPEN user_cursor;
LOOP
FETCH user_cursor INTO user_record;
EXIT WHEN NOT FOUND;
-- 返回结果
user_id := user_record.id;
username := user_record.username;
RETURN NEXT;
END LOOP;
CLOSE user_cursor;
END;
$$ LANGUAGE plpgsql;
游标FOR循环
CREATE OR REPLACE FUNCTION cursor_for_example()
RETURNS INTEGER AS $$
DECLARE
processed_count INTEGER := 0;
BEGIN
FOR user_record IN
SELECT id, username FROM users WHERE is_active = true
LOOP
-- 处理每个用户
INSERT INTO user_logs (user_id, action, created_at)
VALUES (user_record.id, 'processed', CURRENT_TIMESTAMP);
processed_count := processed_count + 1;
END LOOP;
RETURN processed_count;
END;
$$ LANGUAGE plpgsql;
8.5 异常处理
基本异常处理
CREATE OR REPLACE FUNCTION safe_divide(dividend NUMERIC, divisor NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
result NUMERIC;
BEGIN
IF divisor = 0 THEN
RAISE EXCEPTION 'Division by zero error';
END IF;
result := dividend / divisor;
RETURN result;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Cannot divide by zero';
RETURN NULL;
WHEN OTHERS THEN
RAISE NOTICE 'An error occurred: %', SQLERRM;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
自定义异常
CREATE OR REPLACE FUNCTION validate_user_age(user_id INTEGER, age INTEGER)
RETURNS VOID AS $$
BEGIN
IF age < 0 OR age > 150 THEN
RAISE EXCEPTION 'Invalid age: %', age USING ERRCODE = 'invalid_parameter_value';
END IF;
UPDATE users SET age = age WHERE id = user_id;
EXCEPTION
WHEN OTHERS THEN
RAISE LOG 'Error updating user age: %', SQLERRM;
RAISE EXCEPTION 'Failed to update user age';
END;
$$ LANGUAGE plpgsql;
8.6 触发器
触发器函数
-- 创建触发器函数
CREATE OR REPLACE FUNCTION update_user_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 创建BEFORE触发器
CREATE TRIGGER update_user_timestamp_trigger
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_user_timestamp();
不同类型的触发器
-- BEFORE INSERT触发器
CREATE OR REPLACE FUNCTION set_default_values()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.created_at IS NULL THEN
NEW.created_at := CURRENT_TIMESTAMP;
END IF;
IF NEW.is_active IS NULL THEN
NEW.is_active := true;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_default_values_trigger
BEFORE INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION set_default_values();
-- AFTER INSERT触发器
CREATE OR REPLACE FUNCTION log_user_creation()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_logs (user_id, action, created_at)
VALUES (NEW.id, 'created', CURRENT_TIMESTAMP);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER log_user_creation_trigger
AFTER INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION log_user_creation();
-- AFTER UPDATE触发器
CREATE OR REPLACE FUNCTION log_user_update()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.email != NEW.email THEN
INSERT INTO user_logs (user_id, action, details, created_at)
VALUES (NEW.id, 'email_changed',
'From: ' || OLD.email || ' To: ' || NEW.email,
CURRENT_TIMESTAMP);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER log_user_update_trigger
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION log_user_update();
8.7 存储过程
创建存储过程
-- PostgreSQL 11+支持CREATE PROCEDURE
CREATE OR REPLACE PROCEDURE create_user(
p_username VARCHAR(50),
p_email VARCHAR(255),
p_first_name VARCHAR(50),
p_last_name VARCHAR(50)
)
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO users (username, email, first_name, last_name, created_at)
VALUES (p_username, p_email, p_first_name, p_last_name, CURRENT_TIMESTAMP);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
$$;
-- 调用存储过程
CALL create_user('newuser', 'newuser@example.com', 'New', 'User');
复杂存储过程示例
CREATE OR REPLACE PROCEDURE process_monthly_orders(p_month DATE)
LANGUAGE plpgsql
AS $$
DECLARE
user_record RECORD;
total_sales NUMERIC(10,2) := 0;
order_count INTEGER := 0;
BEGIN
-- 创建临时表存储结果
CREATE TEMP TABLE monthly_summary (
user_id INTEGER,
username VARCHAR(100),
order_count INTEGER,
total_amount NUMERIC(10,2)
);
-- 处理每个用户的订单
FOR user_record IN
SELECT u.id, u.username, COUNT(o.id) as cnt, SUM(o.total_amount) as amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE DATE_TRUNC('month', o.order_date) = DATE_TRUNC('month', p_month)
GROUP BY u.id, u.username
LOOP
INSERT INTO monthly_summary (user_id, username, order_count, total_amount)
VALUES (user_record.id, user_record.username, user_record.cnt, user_record.amount);
total_sales := total_sales + COALESCE(user_record.amount, 0);
order_count := order_count + user_record.cnt;
END LOOP;
-- 生成汇总报告
INSERT INTO monthly_reports (report_month, total_sales, order_count, created_at)
VALUES (p_month, total_sales, order_count, CURRENT_TIMESTAMP);
RAISE NOTICE 'Processed % orders with total sales of % for month %',
order_count, total_sales, p_month;
-- 清理临时表
DROP TABLE monthly_summary;
END;
$$;
-- 调用存储过程
CALL process_monthly_orders('2023-10-01');
8.8 函数与存储过程的区别
主要区别
特性 | 函数 | 存储过程 |
---|---|---|
返回值 | 必须返回值 | 可选返回值 |
调用方式 | SELECT function_name() | CALL procedure_name() |
事务控制 | 不能包含事务控制语句 | 可以包含事务控制语句 |
SQL语句中使用 | 可以在SQL语句中调用 | 不能在SQL语句中直接调用 |
版本支持 | 所有版本支持 | PostgreSQL 11+支持 |
8.9 管理存储过程和函数
查看存储过程和函数
-- 查看所有函数
SELECT
proname AS function_name,
pg_get_function_arguments(p.oid) AS arguments,
pg_get_function_result(p.oid) AS return_type,
prosecdef AS security_definer
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname = 'public';
-- 查看特定函数的定义
SELECT pg_get_functiondef('update_user_timestamp'::regproc);
-- 查看触发器
SELECT
tgname AS trigger_name,
relname AS table_name,
tgtype AS trigger_type,
tgfoid::regproc AS function_name
FROM pg_trigger t
JOIN pg_class c ON t.tgrelid = c.oid
WHERE NOT tgisinternal;
修改和删除
-- 修改函数
CREATE OR REPLACE FUNCTION function_name()
RETURNS return_type AS $$
-- 新的函数体
$$ LANGUAGE plpgsql;
-- 删除函数
DROP FUNCTION IF EXISTS function_name(argument_types);
-- 删除触发器
DROP TRIGGER IF EXISTS trigger_name ON table_name;
-- 删除触发器函数
DROP FUNCTION IF EXISTS trigger_function_name();
8.10 最佳实践
性能优化
- 避免在循环中执行SQL:尽量使用集合操作代替循环
- 合理使用游标:只在必要时使用游标,优先考虑FOR循环
- 异常处理:适当使用异常处理,但不要过度使用
- 事务管理:在存储过程中合理管理事务
安全性
- 输入验证:对所有输入参数进行验证
- 权限控制:使用适当的权限设置
- SQL注入防护:使用参数化查询
- 日志记录:记录重要的操作日志
实际应用示例
-- 创建订单处理函数
CREATE OR REPLACE FUNCTION process_order(
p_user_id INTEGER,
p_products JSONB
)
RETURNS INTEGER AS $$
DECLARE
order_id INTEGER;
product_item JSONB;
product_id INTEGER;
quantity INTEGER;
price NUMERIC(10,2);
total_amount NUMERIC(10,2) := 0;
BEGIN
-- 开始事务
BEGIN
-- 创建订单
INSERT INTO orders (user_id, total_amount, status, created_at)
VALUES (p_user_id, 0, 'pending', CURRENT_TIMESTAMP)
RETURNING id INTO order_id;
-- 处理每个产品
FOR product_item IN SELECT jsonb_array_elements(p_products)
LOOP
product_id := (product_item->>'product_id')::INTEGER;
quantity := (product_item->>'quantity')::INTEGER;
-- 获取产品价格
SELECT price INTO price FROM products WHERE id = product_id;
-- 检查库存
IF (SELECT stock_quantity FROM products WHERE id = product_id) < quantity THEN
RAISE EXCEPTION 'Insufficient stock for product %', product_id;
END IF;
-- 添加订单项
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (order_id, product_id, quantity, price);
-- 更新库存
UPDATE products
SET stock_quantity = stock_quantity - quantity
WHERE id = product_id;
-- 计算总金额
total_amount := total_amount + (price * quantity);
END LOOP;
-- 更新订单总金额
UPDATE orders
SET total_amount = total_amount
WHERE id = order_id;
-- 记录日志
INSERT INTO order_logs (order_id, action, details, created_at)
VALUES (order_id, 'created', 'Order processed successfully', CURRENT_TIMESTAMP);
-- 提交事务
RETURN order_id;
EXCEPTION
WHEN OTHERS THEN
-- 回滚事务
RAISE NOTICE 'Order processing failed: %', SQLERRM;
RAISE;
END;
END;
$$ LANGUAGE plpgsql;
-- 调用函数
SELECT process_order(1, '[{"product_id": 1, "quantity": 2}, {"product_id": 2, "quantity": 1}]');
提示:存储过程和触发器虽然功能强大,但也要谨慎使用。过度依赖数据库逻辑会增加维护难度,建议将复杂的业务逻辑放在应用程序层。