~/icsd.ir — bash
SYSTEM_ONLINE

Stored Procedures و Functions

Stored Procedure یک تکه کد SQL از پیش کامپایل شده است که در سرور ذخیره می‌شود. مزایا و معایب:

۶.۱ مقدمه: کِی از Stored Procedure استفاده کنیم؟

Stored Procedure یک تکه کد SQL از پیش کامپایل شده است که در سرور ذخیره می‌شود. مزایا و معایب:

مزایا

  • کاهش traffic شبکه (یک call به‌جای ده‌ها query)
  • اجرای یکسان منطق در همه اپلیکیشن‌ها
  • امنیت (دسترسی فقط به procedure، نه جدول)
  • Performance بهتر برای عملیات پیچیده

معایب

  • سخت‌تر version control
  • دیباگ سخت‌تر از کد اپلیکیشن
  • وابستگی به vendor (انتقال به دیتابیس دیگر سخت می‌شود)
  • logic در دو جا (اپ و DB) دشوار است
💡 توصیه عملی: Stored Procedure را برای کارهایی که صرفاً دیتابیسی هستند استفاده کنید: cleanup شبانه، انتقال داده، گزارش‌گیری پیچیده. منطق business را در اپلیکیشن نگه دارید.

۶.۲ ساختار اولیه Stored Procedure

SQL
DELIMITER $$ CREATE PROCEDURE get_user_orders(IN p_user_id INT) BEGIN SELECT id, total, status, created_at FROM orders WHERE user_id = p_user_id ORDER BY created_at DESC; END$$ DELIMITER ; -- فراخوانی CALL get_user_orders(5); -- حذف DROP PROCEDURE IF EXISTS get_user_orders;
📌 چرا DELIMITER؟ چون کد procedure شامل ; است، باید DELIMITER را موقتاً عوض کنیم تا MySQL فکر نکند procedure تمام شده. $$ یک انتخاب رایج است.

۶.۳ Parameters: IN، OUT، INOUT

SQL
DELIMITER $$ -- IN: ورودی فقط -- OUT: خروجی فقط -- INOUT: هر دو CREATE PROCEDURE order_summary( IN p_user_id INT, OUT p_total_count INT, OUT p_total_amount BIGINT ) BEGIN SELECT COUNT(*), COALESCE(SUM(total), 0) INTO p_total_count, p_total_amount FROM orders WHERE user_id = p_user_id; END$$ DELIMITER ; -- استفاده CALL order_summary(5, @count, @amount); SELECT @count, @amount;

۶.۴ متغیرها و کنترل جریان

SQL
DELIMITER $$ CREATE PROCEDURE apply_discount(IN p_user_id INT) BEGIN -- متغیر محلی DECLARE v_total BIGINT DEFAULT 0; DECLARE v_discount_pct INT DEFAULT 0; DECLARE v_status VARCHAR(20); -- محاسبه مجموع خرید SELECT COALESCE(SUM(total), 0) INTO v_total FROM orders WHERE user_id = p_user_id AND status = 'paid'; -- IF / ELSEIF / ELSE IF v_total >= 10000000 THEN SET v_discount_pct = 20; SET v_status = 'VIP'; ELSEIF v_total >= 5000000 THEN SET v_discount_pct = 10; SET v_status = 'Gold'; ELSEIF v_total >= 1000000 THEN SET v_discount_pct = 5; SET v_status = 'Silver'; ELSE SET v_discount_pct = 0; SET v_status = 'Regular'; END IF; -- ذخیره UPDATE users SET discount_pct = v_discount_pct, membership_level = v_status WHERE id = p_user_id; SELECT v_total AS total_purchases, v_discount_pct AS discount, v_status AS level; END$$ DELIMITER ; -- CASE WHEN هم پشتیبانی می‌شود CREATE PROCEDURE classify_order(IN p_total BIGINT, OUT p_class VARCHAR(20)) BEGIN SET p_class = CASE WHEN p_total >= 5000000 THEN 'large' WHEN p_total >= 1000000 THEN 'medium' ELSE 'small' END; END$$

۶.۵ Loops: WHILE، REPEAT، LOOP

SQL
DELIMITER $$ -- WHILE CREATE PROCEDURE generate_test_data(IN n INT) BEGIN DECLARE i INT DEFAULT 1; WHILE i <= n DO INSERT INTO test_users (name, email) VALUES (CONCAT('User ', i), CONCAT('user', i, '@example.com')); SET i = i + 1; END WHILE; END$$ -- REPEAT (مثل do-while) CREATE PROCEDURE example_repeat() BEGIN DECLARE i INT DEFAULT 1; REPEAT SELECT i; SET i = i + 1; UNTIL i > 5 END REPEAT; END$$ -- LOOP با LEAVE (break) و ITERATE (continue) CREATE PROCEDURE example_loop() BEGIN DECLARE i INT DEFAULT 0; my_loop: LOOP SET i = i + 1; IF i > 10 THEN LEAVE my_loop; END IF; IF i % 2 = 0 THEN ITERATE my_loop; -- skip even END IF; SELECT i; -- only odd END LOOP my_loop; END$$ DELIMITER ;

۶.۶ Cursors – پیمایش نتایج

وقتی نیاز داشتید روی نتیجه یک SELECT ردیف به ردیف کار کنید:

SQL
DELIMITER $$ CREATE PROCEDURE recalculate_user_totals() BEGIN DECLARE v_user_id INT; DECLARE v_done INT DEFAULT 0; -- تعریف cursor DECLARE user_cursor CURSOR FOR SELECT id FROM users WHERE is_active = 1; -- handler برای پایان داده‌ها DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1; OPEN user_cursor; read_loop: LOOP FETCH user_cursor INTO v_user_id; IF v_done THEN LEAVE read_loop; END IF; -- پردازش هر کاربر UPDATE users SET total_spent = ( SELECT COALESCE(SUM(total), 0) FROM orders WHERE user_id = v_user_id AND status = 'paid' ) WHERE id = v_user_id; END LOOP; CLOSE user_cursor; END$$ DELIMITER ; CALL recalculate_user_totals();
⚠️ Performance: Cursor در MySQL بسیار کندتر از یک UPDATE با JOIN است. تا حد امکان با set-based operations کار کنید:

UPDATE users u
LEFT JOIN (
    SELECT user_id, SUM(total) AS s
    FROM orders WHERE status = 'paid'
    GROUP BY user_id
) o ON o.user_id = u.id
SET u.total_spent = COALESCE(o.s, 0)
WHERE u.is_active = 1;

۶.۷ Exception Handling

SQL
DELIMITER $$ CREATE PROCEDURE transfer_money( IN p_from_user INT, IN p_to_user INT, IN p_amount BIGINT ) BEGIN DECLARE v_from_balance BIGINT; -- handler برای هر خطای SQL DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; -- خطا را به caller برگردان END; -- handler برای SQLWARNING DECLARE CONTINUE HANDLER FOR SQLWARNING SELECT 'هشدار رخ داد' AS warning; -- handler برای کد خطای خاص (Duplicate key) DECLARE CONTINUE HANDLER FOR 1062 SELECT 'این رکورد قبلاً وجود دارد' AS warning; START TRANSACTION; SELECT balance INTO v_from_balance FROM users WHERE id = p_from_user FOR UPDATE; IF v_from_balance < p_amount THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'موجودی کافی نیست'; END IF; UPDATE users SET balance = balance - p_amount WHERE id = p_from_user; UPDATE users SET balance = balance + p_amount WHERE id = p_to_user; COMMIT; END$$ DELIMITER ;

۶.۸ Stored Functions - تفاوت با Procedure

Function یک مقدار برمی‌گرداند و در SELECT قابل استفاده است:

SQL
DELIMITER $$ -- function ساده CREATE FUNCTION calculate_tax(p_amount BIGINT, p_rate DECIMAL(4,2)) RETURNS BIGINT DETERMINISTIC BEGIN RETURN p_amount * p_rate / 100; END$$ -- function برای فرمت قیمت فارسی CREATE FUNCTION format_price_persian(p_amount BIGINT) RETURNS VARCHAR(50) DETERMINISTIC BEGIN -- ساده‌ترین حالت RETURN CONCAT(FORMAT(p_amount / 10, 0), ' تومان'); END$$ -- function برای محاسبه سن CREATE FUNCTION calculate_age(p_birthdate DATE) RETURNS INT DETERMINISTIC BEGIN RETURN TIMESTAMPDIFF(YEAR, p_birthdate, CURDATE()); END$$ DELIMITER ; -- استفاده در SELECT SELECT title, format_price_persian(price) AS price_formatted, calculate_tax(price, 9.0) AS tax FROM products; -- استفاده در WHERE (با احتیاط!) SELECT * FROM users WHERE calculate_age(birthdate) >= 18;

کلمات کلیدی مهم

  • DETERMINISTIC: با ورودی یکسان، خروجی همیشه یکسان است (می‌تواند cache شود)
  • NOT DETERMINISTIC: خروجی می‌تواند متفاوت باشد (مثل NOW())
  • READS SQL DATA: فقط SELECT انجام می‌دهد
  • MODIFIES SQL DATA: INSERT/UPDATE/DELETE می‌کند
  • NO SQL: هیچ SQL ندارد (فقط محاسبه)
⚠️ Performance: استفاده از Function در WHERE معمولاً ایندکس را غیرفعال می‌کند (مگر Functional Index در MySQL 8). برای داده‌های بزرگ، function در SELECT مشکل‌ساز است.

۶.۹ مثال جامع: ثبت سفارش با تراکنش

SQL
DELIMITER $$ CREATE PROCEDURE create_order( IN p_user_id INT, IN p_product_id INT, IN p_quantity INT, OUT p_order_id INT, OUT p_message VARCHAR(200) ) BEGIN DECLARE v_price BIGINT; DECLARE v_stock INT; DECLARE v_total BIGINT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_order_id = 0; SET p_message = 'خطا در ثبت سفارش'; END; START TRANSACTION; -- قفل کردن ردیف محصول SELECT price, stock INTO v_price, v_stock FROM products WHERE id = p_product_id FOR UPDATE; IF v_stock < p_quantity THEN SET p_order_id = 0; SET p_message = CONCAT('موجودی کافی نیست. موجودی: ', v_stock); ROLLBACK; ELSE SET v_total = v_price * p_quantity; INSERT INTO orders (user_id, total, status, created_at) VALUES (p_user_id, v_total, 'pending', NOW()); SET p_order_id = LAST_INSERT_ID(); INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (p_order_id, p_product_id, p_quantity, v_price); UPDATE products SET stock = stock - p_quantity WHERE id = p_product_id; COMMIT; SET p_message = 'سفارش با موفقیت ثبت شد'; END IF; END$$ DELIMITER ; -- استفاده CALL create_order(5, 12, 2, @order_id, @message); SELECT @order_id, @message;

۶.۱۰ مدیریت Stored Procedures موجود

SQL
-- لیست همه procedures SHOW PROCEDURE STATUS WHERE Db = 'my_database'; -- نمایش کد یک procedure SHOW CREATE PROCEDURE get_user_orders; -- یا از information_schema SELECT routine_name, routine_type, created FROM information_schema.routines WHERE routine_schema = 'my_database'; -- backup همه procedures mysqldump --routines --no-data --no-create-info my_database > procs.sql

۶.۱۱ خلاصه فصل

  • Procedure برای عملیات پیچیده، Function برای محاسبه و استفاده در SELECT
  • IN/OUT/INOUT برای ارسال و دریافت پارامتر
  • Cursor برای پیمایش ردیف به ردیف (اما set-based معمولاً سریع‌تر است)
  • EXIT HANDLER برای exception، SIGNAL برای throw کردن خطا
  • DETERMINISTIC را برای function‌های ثابت تعیین کنید
  • Logic اصلی را در اپلیکیشن نگه دارید؛ DB فقط برای کارهای مرتبط با داده

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.