Appearance
MySQL存储过程
存储过程是一组预编译的SQL语句,存储在数据库中,可以通过名称调用执行。存储过程提供了模块化、可重用的数据库编程功能,能够提高性能、增强安全性并简化复杂操作。
存储过程基础
创建存储过程
sql
-- 基本语法
DELIMITER //
CREATE PROCEDURE procedure_name([parameter_list])
BEGIN
-- SQL语句
END //
DELIMITER ;
-- 简单示例:获取用户总数
DELIMITER //
CREATE PROCEDURE GetTotalUsers()
BEGIN
SELECT COUNT(*) AS total_users FROM users;
END //
DELIMITER ;
-- 调用存储过程
CALL GetTotalUsers();
存储过程参数
存储过程支持三种类型的参数:IN(输入)、OUT(输出)、INOUT(输入输出)。
sql
-- IN参数:输入参数
DELIMITER //
CREATE PROCEDURE GetUserById(IN user_id INT)
BEGIN
SELECT * FROM users WHERE id = user_id;
END //
DELIMITER ;
-- OUT参数:输出参数
DELIMITER //
CREATE PROCEDURE GetUserCount(OUT total_count INT)
BEGIN
SELECT COUNT(*) INTO total_count FROM users;
END //
DELIMITER ;
-- 调用并获取输出参数
CALL GetUserCount(@count);
SELECT @count;
-- INOUT参数:输入输出参数
DELIMITER //
CREATE PROCEDURE DoubleNumber(INOUT num INT)
BEGIN
SET num = num * 2;
END //
DELIMITER ;
-- 调用INOUT参数
SET @value = 5;
CALL DoubleNumber(@value);
SELECT @value; -- 结果为10
存储过程变量
局部变量
sql
DELIMITER //
CREATE PROCEDURE ProcessUser(IN user_id INT)
BEGIN
-- 声明局部变量
DECLARE user_name VARCHAR(100);
DECLARE user_email VARCHAR(100);
DECLARE created_date DATE;
-- 为变量赋值
SELECT username, email, DATE(created_at)
INTO user_name, user_email, created_date
FROM users
WHERE id = user_id;
-- 使用变量
SELECT user_name, user_email, created_date;
END //
DELIMITER ;
用户变量
sql
-- 用户变量在会话级别可用
SET @global_count = 0;
DELIMITER //
CREATE PROCEDURE UpdateGlobalCount()
BEGIN
SET @global_count = @global_count + 1;
END //
DELIMITER ;
控制结构
条件语句
sql
-- IF-ELSE语句
DELIMITER //
CREATE PROCEDURE CheckUserStatus(IN user_id INT)
BEGIN
DECLARE user_status VARCHAR(20);
DECLARE message VARCHAR(200);
SELECT status INTO user_status FROM users WHERE id = user_id;
IF user_status = 'active' THEN
SET message = '用户状态正常';
ELSEIF user_status = 'inactive' THEN
SET message = '用户已停用';
ELSE
SET message = '用户状态未知';
END IF;
SELECT message;
END //
DELIMITER ;
-- CASE语句
DELIMITER //
CREATE PROCEDURE GetUserCategory(IN user_id INT)
BEGIN
DECLARE user_age INT;
DECLARE category VARCHAR(20);
SELECT TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) INTO user_age
FROM users WHERE id = user_id;
CASE
WHEN user_age < 18 THEN SET category = '未成年';
WHEN user_age BETWEEN 18 AND 65 THEN SET category = '成年人';
ELSE SET category = '老年人';
END CASE;
SELECT category;
END //
DELIMITER ;
循环语句
sql
-- WHILE循环
DELIMITER //
CREATE PROCEDURE ProcessNumbers()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE result VARCHAR(1000) DEFAULT '';
WHILE counter <= 10 DO
SET result = CONCAT(result, counter, ',');
SET counter = counter + 1;
END WHILE;
SELECT result;
END //
DELIMITER ;
-- REPEAT循环
DELIMITER //
CREATE PROCEDURE ProcessWithRepeat()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE result VARCHAR(1000) DEFAULT '';
REPEAT
SET result = CONCAT(result, counter, ',');
SET counter = counter + 1;
UNTIL counter > 10 END REPEAT;
SELECT result;
END //
DELIMITER ;
-- LOOP循环
DELIMITER //
CREATE PROCEDURE ProcessWithLoop()
BEGIN
DECLARE counter INT DEFAULT 1;
DECLARE result VARCHAR(1000) DEFAULT '';
loop_label: LOOP
IF counter > 10 THEN
LEAVE loop_label;
END IF;
SET result = CONCAT(result, counter, ',');
SET counter = counter + 1;
END LOOP;
SELECT result;
END //
DELIMITER ;
游标使用
游标用于逐行处理结果集。
sql
DELIMITER //
CREATE PROCEDURE ProcessUserOrders(IN user_id INT)
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE order_id INT;
DECLARE order_amount DECIMAL(10,2);
DECLARE order_date DATE;
-- 声明游标
DECLARE order_cursor CURSOR FOR
SELECT id, amount, DATE(created_at)
FROM orders
WHERE user_id = user_id;
-- 声明继续处理程序
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 创建临时表存储结果
CREATE TEMPORARY TABLE temp_user_orders (
order_id INT,
amount DECIMAL(10,2),
order_date DATE
);
OPEN order_cursor;
read_loop: LOOP
FETCH order_cursor INTO order_id, order_amount, order_date;
IF done THEN
LEAVE read_loop;
END IF;
INSERT INTO temp_user_orders VALUES (order_id, order_amount, order_date);
END LOOP;
CLOSE order_cursor;
-- 返回结果
SELECT * FROM temp_user_orders;
DROP TEMPORARY TABLE temp_user_orders;
END //
DELIMITER ;
错误处理
异常处理
sql
DELIMITER //
CREATE PROCEDURE SafeTransfer(
IN from_user_id INT,
IN to_user_id INT,
IN transfer_amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL; -- 重新抛出异常
END;
START TRANSACTION;
-- 检查转出账户余额
SET @balance = (SELECT balance FROM accounts WHERE user_id = from_user_id FOR UPDATE);
IF @balance < transfer_amount THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
END IF;
-- 执行转账
UPDATE accounts SET balance = balance - transfer_amount WHERE user_id = from_user_id;
UPDATE accounts SET balance = balance + transfer_amount WHERE user_id = to_user_id;
-- 记录转账日志
INSERT INTO transfer_logs (from_user, to_user, amount, transfer_time)
VALUES (from_user_id, to_user_id, transfer_amount, NOW());
COMMIT;
END //
DELIMITER ;
自定义错误
sql
DELIMITER //
CREATE PROCEDURE ValidateUser(IN user_email VARCHAR(100))
BEGIN
DECLARE user_count INT DEFAULT 0;
SELECT COUNT(*) INTO user_count FROM users WHERE email = user_email;
IF user_count = 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户不存在';
ELSEIF user_count > 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '数据异常:存在重复邮箱';
END IF;
SELECT '用户验证通过' AS result;
END //
DELIMITER ;
复杂存储过程示例
用户注册存储过程
sql
DELIMITER //
CREATE PROCEDURE RegisterUser(
IN p_username VARCHAR(50),
IN p_email VARCHAR(100),
IN p_password VARCHAR(255),
OUT p_user_id INT,
OUT p_result VARCHAR(100)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_result = '注册失败:系统错误';
SET p_user_id = 0;
END;
START TRANSACTION;
-- 检查用户名是否已存在
IF EXISTS(SELECT 1 FROM users WHERE username = p_username) THEN
SET p_result = '用户名已存在';
SET p_user_id = 0;
ROLLBACK;
ELSEIF EXISTS(SELECT 1 FROM users WHERE email = p_email) THEN
-- 检查邮箱是否已存在
SET p_result = '邮箱已被注册';
SET p_user_id = 0;
ROLLBACK;
ELSE
-- 插入用户信息
INSERT INTO users (username, email, password, created_at)
VALUES (p_username, p_email, p_password, NOW());
SET p_user_id = LAST_INSERT_ID();
-- 创建用户资料
INSERT INTO user_profiles (user_id, created_at)
VALUES (p_user_id, NOW());
-- 分配默认角色
INSERT INTO user_roles (user_id, role_id)
VALUES (p_user_id, 1); -- 假设1是普通用户角色
SET p_result = '注册成功';
COMMIT;
END IF;
END //
DELIMITER ;
-- 调用示例
CALL RegisterUser('newuser', 'newuser@example.com', 'password123', @user_id, @result);
SELECT @user_id, @result;
订单处理存储过程
sql
DELIMITER //
CREATE PROCEDURE ProcessOrder(
IN p_user_id INT,
IN p_product_id INT,
IN p_quantity INT,
OUT p_order_id INT,
OUT p_result VARCHAR(200)
)
BEGIN
DECLARE v_product_price DECIMAL(10,2);
DECLARE v_product_stock INT;
DECLARE v_total_amount DECIMAL(10,2);
DECLARE v_user_balance DECIMAL(10,2);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_result = '订单处理失败:系统错误';
SET p_order_id = 0;
END;
START TRANSACTION;
-- 获取商品信息
SELECT price, stock INTO v_product_price, v_product_stock
FROM products
WHERE id = p_product_id AND status = 'active' FOR UPDATE;
-- 检查商品是否存在
IF v_product_price IS NULL THEN
SET p_result = '商品不存在或已下架';
SET p_order_id = 0;
ROLLBACK;
ELSEIF v_product_stock < p_quantity THEN
-- 检查库存是否充足
SET p_result = CONCAT('库存不足,当前库存:', v_product_stock);
SET p_order_id = 0;
ROLLBACK;
ELSE
-- 计算总金额
SET v_total_amount = v_product_price * p_quantity;
-- 获取用户余额
SELECT balance INTO v_user_balance
FROM accounts
WHERE user_id = p_user_id FOR UPDATE;
-- 检查余额是否充足
IF v_user_balance < v_total_amount THEN
SET p_result = CONCAT('余额不足,当前余额:', v_user_balance, ',需要:', v_total_amount);
SET p_order_id = 0;
ROLLBACK;
ELSE
-- 创建订单
INSERT INTO orders (user_id, total_amount, status, created_at)
VALUES (p_user_id, v_total_amount, 'pending', NOW());
SET p_order_id = LAST_INSERT_ID();
-- 创建订单详情
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (p_order_id, p_product_id, p_quantity, v_product_price);
-- 更新库存
UPDATE products SET stock = stock - p_quantity WHERE id = p_product_id;
-- 扣减用户余额
UPDATE accounts SET balance = balance - v_total_amount WHERE user_id = p_user_id;
-- 记录余额变动
INSERT INTO balance_logs (user_id, amount, type, related_id, description)
VALUES (p_user_id, -v_total_amount, 'order', p_order_id, '订单支付');
SET p_result = '订单创建成功';
UPDATE orders SET status = 'confirmed' WHERE id = p_order_id;
COMMIT;
END IF;
END IF;
END //
DELIMITER ;
存储过程管理
查看存储过程
sql
-- 查看所有存储过程
SHOW PROCEDURE STATUS;
-- 查看特定数据库的存储过程
SHOW PROCEDURE STATUS WHERE Db = 'your_database_name';
-- 查看存储过程定义
SHOW CREATE PROCEDURE procedure_name;
-- 从information_schema查看
SELECT routine_name, routine_type, created, last_altered
FROM information_schema.routines
WHERE routine_schema = 'your_database_name'
AND routine_type = 'PROCEDURE';
修改和删除存储过程
sql
-- 删除存储过程
DROP PROCEDURE IF EXISTS procedure_name;
-- 如果需要修改存储过程,需要先删除再重新创建
DROP PROCEDURE IF EXISTS GetUserById;
DELIMITER //
CREATE PROCEDURE GetUserById(IN user_id INT)
BEGIN
SELECT id, username, email, created_at
FROM users
WHERE id = user_id AND status = 'active';
END //
DELIMITER ;
存储函数
存储函数与存储过程类似,但必须返回一个值。
sql
DELIMITER //
CREATE FUNCTION GetUserOrderCount(user_id INT)
RETURNS INT
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE order_count INT;
SELECT COUNT(*) INTO order_count
FROM orders
WHERE user_id = user_id;
RETURN IFNULL(order_count, 0);
END //
DELIMITER ;
-- 使用存储函数
SELECT username, GetUserOrderCount(id) as order_count
FROM users
WHERE id IN (1, 2, 3);
性能优化
预编译优势
存储过程在创建时被预编译,执行时无需再次解析,提高性能。
减少网络传输
sql
-- 不使用存储过程:多次网络往返
SELECT balance FROM accounts WHERE id = 1;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
INSERT INTO transactions VALUES (...);
-- 使用存储过程:一次网络往返
CALL TransferMoney(1, 2, 100);
执行计划缓存
MySQL会缓存存储过程的执行计划,提高重复执行的效率。
安全考虑
权限管理
sql
-- 授予执行存储过程的权限
GRANT EXECUTE ON PROCEDURE database.procedure_name TO 'username'@'host';
-- 授予所有存储过程的执行权限
GRANT EXECUTE ON database.* TO 'username'@'host';
SQL注入防护
存储过程的参数化查询天然防止SQL注入:
sql
-- 安全的参数化查询
DELIMITER //
CREATE PROCEDURE SearchUsers(IN search_term VARCHAR(100))
BEGIN
SELECT * FROM users WHERE username LIKE CONCAT('%', search_term, '%');
END //
DELIMITER ;
最佳实践
1. 命名规范
sql
-- 推荐的命名约定
DELIMITER //
CREATE PROCEDURE sp_GetUserOrders(IN p_user_id INT) -- 前缀sp_表示存储过程
BEGIN
-- 过程体
END //
DELIMITER ;
2. 参数验证
sql
DELIMITER //
CREATE PROCEDURE ValidateAndProcess(IN p_input INT)
BEGIN
-- 参数验证
IF p_input IS NULL OR p_input <= 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '参数无效';
END IF;
-- 业务逻辑
-- ...
END //
DELIMITER ;
3. 事务管理
sql
DELIMITER //
CREATE PROCEDURE ComplexOperation()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- 多个相关操作
-- ...
COMMIT;
END //
DELIMITER ;
与应用程序集成
从应用程序调用
sql
-- 示例:从应用程序调用存储过程
-- 在Java中使用JDBC:
-- CallableStatement stmt = connection.prepareCall("{call ProcessOrder(?, ?, ?, ?, ?)}");
-- stmt.setInt(1, userId);
-- stmt.setInt(2, productId);
-- stmt.setInt(3, quantity);
-- stmt.registerOutParameter(4, Types.INTEGER);
-- stmt.registerOutParameter(5, Types.VARCHAR);
-- stmt.execute();
总结
存储过程是MySQL中强大的编程功能,提供了以下优势:
- 性能提升:预编译和执行计划缓存
- 安全性:参数化查询防止SQL注入,权限控制
- 模块化:可重用的代码单元
- 数据完整性:在数据库层面强制业务规则
- 减少网络流量:批量操作只需一次网络调用
在使用存储过程时,需要注意:
- 适当的错误处理和事务管理
- 合理的参数验证
- 性能优化和监控
- 安全性考虑
- 维护和文档
通过合理使用存储过程,可以构建高效、安全、可维护的数据库应用程序。