Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on stored procedures in Python! ๐ Have you ever wondered how to supercharge your database operations while keeping your business logic secure and efficient? Thatโs exactly what stored procedures can do for you!
In this guide, weโll explore how to call stored procedures from Python, turning your database into a powerful ally. Whether youโre building e-commerce platforms ๐, financial systems ๐ฐ, or data processing pipelines ๐, mastering stored procedures will level up your database game!
By the end of this tutorial, youโll be confidently calling stored procedures like a database wizard! Letโs dive in! ๐โโ๏ธ
๐ Understanding Stored Procedures
๐ค What are Stored Procedures?
Think of stored procedures as pre-written recipes ๐ณ stored in your database. Just like how a restaurant chef prepares signature dishes using tested recipes, stored procedures are pre-compiled SQL scripts that perform specific database operations.
In Python terms, stored procedures are like functions that live inside your database. This means you can:
- โจ Execute complex operations with a single call
- ๐ Improve performance with pre-compiled SQL
- ๐ก๏ธ Enhance security by limiting direct table access
- ๐ฆ Package business logic in the database
๐ก Why Use Stored Procedures?
Hereโs why developers love stored procedures:
- Performance Boost ๐: Pre-compiled and optimized by the database
- Security Shield ๐ก๏ธ: Prevent SQL injection and control access
- Network Efficiency ๐ก: Reduce data transfer between app and database
- Code Reusability ๐: Share logic across multiple applications
Real-world example: Imagine an online banking system ๐ฆ. With stored procedures, you can handle complex transactions (checking balance, validating limits, transferring money) in one secure, atomic operation!
๐ง Basic Syntax and Usage
๐ Simple Example with MySQL
Letโs start with a friendly example using MySQL and PyMySQL:
# ๐ Hello, Stored Procedures!
import pymysql
# ๐จ Connect to database
connection = pymysql.connect(
host='localhost',
user='your_user',
password='your_password',
database='your_database'
)
try:
with connection.cursor() as cursor:
# ๐ฏ Call a simple stored procedure
cursor.callproc('get_user_count')
# ๐ Fetch the result
result = cursor.fetchone()
print(f"Total users: {result[0]} ๐")
finally:
connection.close()
๐ก Explanation: Notice how we use callproc()
to execute the stored procedure. Itโs that simple! The procedure runs on the database server and returns results.
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Calling with parameters
def get_user_orders(user_id):
with connection.cursor() as cursor:
# ๐ฆ Pass parameters to stored procedure
cursor.callproc('get_orders_by_user', [user_id])
# ๐จ Fetch all results
orders = cursor.fetchall()
return orders
# ๐จ Pattern 2: Getting output parameters
def calculate_discount(order_total):
with connection.cursor() as cursor:
# ๐ Call procedure with IN and OUT parameters
args = [order_total, 0] # Second param will receive output
cursor.callproc('calculate_customer_discount', args)
# ๐ฐ Get the output parameter
cursor.execute('SELECT @_calculate_customer_discount_1')
discount = cursor.fetchone()[0]
return discount
# ๐ Pattern 3: Handling multiple result sets
def get_dashboard_data():
with connection.cursor() as cursor:
# ๐ Call procedure that returns multiple datasets
cursor.callproc('get_dashboard_stats')
# ๐ Process multiple result sets
daily_sales = cursor.fetchall()
cursor.nextset()
top_products = cursor.fetchall()
cursor.nextset()
active_users = cursor.fetchall()
return daily_sales, top_products, active_users
๐ก Practical Examples
๐ Example 1: E-Commerce Order Processing
Letโs build something real - an order processing system:
# ๐๏ธ E-commerce order processor
import pymysql
from datetime import datetime
from decimal import Decimal
class OrderProcessor:
def __init__(self, db_config):
self.db_config = db_config
# ๐ Process a new order
def process_order(self, user_id, cart_items):
connection = pymysql.connect(**self.db_config)
try:
with connection.cursor() as cursor:
# ๐ณ Call order processing stored procedure
order_id = 0
total_amount = Decimal('0.00')
status = ''
# ๐ฆ Prepare parameters
args = [
user_id, # IN: user ID
str(cart_items), # IN: cart items as JSON
order_id, # OUT: new order ID
total_amount, # OUT: total amount
status # OUT: processing status
]
# ๐ Execute the stored procedure
cursor.callproc('process_new_order', args)
# ๐ Get output parameters
cursor.execute(
'SELECT @_process_new_order_2, ' +
'@_process_new_order_3, @_process_new_order_4'
)
result = cursor.fetchone()
order_id = result[0]
total_amount = result[1]
status = result[2]
# โ
Commit the transaction
connection.commit()
print(f"๐ Order #{order_id} created!")
print(f"๐ฐ Total: ${total_amount}")
print(f"๐ Status: {status}")
return {
'order_id': order_id,
'total': total_amount,
'status': status
}
except Exception as e:
# โ Rollback on error
connection.rollback()
print(f"๐ฑ Order processing failed: {e}")
raise
finally:
connection.close()
# ๐ฎ Let's use it!
processor = OrderProcessor({
'host': 'localhost',
'user': 'shop_user',
'password': 'secure_pass',
'database': 'ecommerce_db'
})
# ๐ Sample cart
cart = [
{'product_id': 101, 'quantity': 2, 'price': 29.99},
{'product_id': 205, 'quantity': 1, 'price': 49.99}
]
order = processor.process_order(user_id=12345, cart_items=cart)
๐ฏ Try it yourself: Add a method to cancel orders and apply discount codes!
๐ฎ Example 2: Game Leaderboard System
Letโs make it fun with a gaming leaderboard:
# ๐ Game leaderboard manager
import psycopg2 # Using PostgreSQL for this example
from contextlib import contextmanager
class GameLeaderboard:
def __init__(self, db_url):
self.db_url = db_url
@contextmanager
def get_db_cursor(self):
# ๐ Database connection manager
conn = psycopg2.connect(self.db_url)
try:
with conn.cursor() as cursor:
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
# ๐ฏ Submit a new score
def submit_score(self, player_name, score, level):
with self.get_db_cursor() as cursor:
# ๐ Call score submission procedure
cursor.callproc('submit_player_score', [
player_name, # ๐ค Player name
score, # ๐ฏ Score achieved
level # ๐ Level completed
])
# ๐ Get player's rank
result = cursor.fetchone()
if result:
rank = result[0]
print(f"๐ {player_name} is now rank #{rank}!")
# ๐ Check for achievements
cursor.callproc('check_achievements', [player_name])
achievements = cursor.fetchall()
if achievements:
print("๐ New achievements unlocked:")
for achievement in achievements:
print(f" ๐ {achievement[0]}")
return rank
# ๐ Get top players
def get_leaderboard(self, limit=10):
with self.get_db_cursor() as cursor:
# ๐
Call leaderboard procedure
cursor.callproc('get_top_players', [limit])
leaderboard = cursor.fetchall()
print("๐ LEADERBOARD ๐")
print("-" * 40)
for rank, (name, score, level) in enumerate(leaderboard, 1):
emoji = "๐ฅ" if rank == 1 else "๐ฅ" if rank == 2 else "๐ฅ" if rank == 3 else "๐ฎ"
print(f"{emoji} #{rank}: {name} - Score: {score} (Level {level})")
return leaderboard
# ๐ Get player statistics
def get_player_stats(self, player_name):
with self.get_db_cursor() as cursor:
# ๐ Call stats procedure
cursor.callproc('get_player_statistics', [player_name])
stats = cursor.fetchone()
if stats:
return {
'total_games': stats[0],
'high_score': stats[1],
'avg_score': stats[2],
'favorite_level': stats[3],
'play_time': stats[4]
}
# ๐ฎ Test the leaderboard!
leaderboard = GameLeaderboard('postgresql://user:pass@localhost/gamedb')
# ๐ Submit some scores
leaderboard.submit_score("SpeedyGonzales", 9500, 12)
leaderboard.submit_score("DragonSlayer", 8700, 10)
leaderboard.submit_score("PixelNinja", 10200, 15)
# ๐ Show the leaderboard
leaderboard.get_leaderboard()
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Handling Complex Data Types
When youโre ready to level up, work with complex return types:
# ๐ฏ Advanced stored procedure handling
import json
import cx_Oracle # Oracle example
class AdvancedProcedureHandler:
def __init__(self, connection_string):
self.connection_string = connection_string
# ๐ช Handle cursor output from procedures
def get_complex_report(self, report_type, date_range):
connection = cx_Oracle.connect(self.connection_string)
try:
cursor = connection.cursor()
# ๐ Create cursor for output
report_cursor = connection.cursor()
# ๐ Call procedure that returns a cursor
cursor.callproc('generate_complex_report', [
report_type,
date_range['start'],
date_range['end'],
report_cursor # OUT parameter as cursor
])
# ๐จ Process the cursor results
columns = [col[0] for col in report_cursor.description]
results = []
for row in report_cursor:
# โจ Convert to dictionary
results.append(dict(zip(columns, row)))
return results
finally:
connection.close()
# ๐๏ธ Handle JSON returns
def process_json_procedure(self, operation, data):
connection = cx_Oracle.connect(self.connection_string)
try:
cursor = connection.cursor()
# ๐ฆ Prepare JSON data
json_input = json.dumps(data)
json_output = cursor.var(cx_Oracle.STRING)
# ๐ซ Call JSON processing procedure
cursor.callproc('process_json_data', [
operation,
json_input,
json_output
])
# ๐ฏ Parse JSON result
result = json.loads(json_output.getvalue())
return result
finally:
connection.close()
๐๏ธ Advanced Topic 2: Async Stored Procedures
For the brave developers using async Python:
# ๐ Async stored procedure handling
import asyncpg # PostgreSQL async driver
import asyncio
class AsyncProcedureManager:
def __init__(self, db_url):
self.db_url = db_url
self.pool = None
async def initialize(self):
# ๐ Create connection pool
self.pool = await asyncpg.create_pool(self.db_url)
async def close(self):
# ๐ Close connection pool
await self.pool.close()
# โก Async procedure call
async def call_procedure_async(self, proc_name, *args):
async with self.pool.acquire() as connection:
# ๐ฏ Call stored procedure asynchronously
result = await connection.fetch(
f"SELECT * FROM {proc_name}($1, $2, $3)",
*args
)
return result
# ๐โโ๏ธ Batch processing with procedures
async def batch_process_orders(self, order_ids):
tasks = []
# ๐ฆ Create tasks for parallel execution
for order_id in order_ids:
task = self.call_procedure_async(
'process_order_async',
order_id,
'PROCESSING',
asyncio.get_event_loop().time()
)
tasks.append(task)
# ๐ Execute all procedures concurrently
results = await asyncio.gather(*tasks)
print(f"โจ Processed {len(results)} orders in parallel!")
return results
# ๐ฎ Test async procedures
async def main():
manager = AsyncProcedureManager('postgresql://user:pass@localhost/db')
await manager.initialize()
# ๐ Process multiple orders concurrently
order_ids = [1001, 1002, 1003, 1004, 1005]
results = await manager.batch_process_orders(order_ids)
await manager.close()
# ๐โโ๏ธ Run it!
# asyncio.run(main())
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Parameter Mismatch
# โ Wrong way - incorrect parameter count!
cursor.callproc('update_user_profile', ['John']) # ๐ฅ Procedure expects 3 params!
# โ
Correct way - match all parameters!
cursor.callproc('update_user_profile', [
'John', # username
'[email protected]', # email
25 # age
])
๐คฏ Pitfall 2: Forgetting to Handle Output Parameters
# โ Dangerous - losing output values!
def calculate_tax(amount):
cursor.callproc('calc_tax', [amount, 0])
# Output parameter is lost! ๐ฐ
# โ
Safe - properly retrieve output!
def calculate_tax(amount):
tax_amount = 0
cursor.callproc('calc_tax', [amount, tax_amount])
# ๐ Retrieve the output parameter
cursor.execute('SELECT @_calc_tax_1')
tax = cursor.fetchone()[0]
return tax # โ
Got it!
๐ Pitfall 3: Not Handling Multiple Result Sets
# โ Missing data - only gets first result set!
cursor.callproc('get_full_report')
data = cursor.fetchall() # ๐ฅ Missing other result sets!
# โ
Complete - get all result sets!
cursor.callproc('get_full_report')
results = []
while True:
data = cursor.fetchall()
results.append(data)
if not cursor.nextset(): # ๐ Check for more sets
break
print(f"๐ Retrieved {len(results)} result sets!")
๐ ๏ธ Best Practices
- ๐ฏ Use Connection Pooling: Donโt create new connections for each call
- ๐ Document Parameters: Clearly specify IN, OUT, and INOUT parameters
- ๐ก๏ธ Handle Exceptions: Always use try-except blocks with rollback
- ๐จ Use Prepared Statements: Combine with procedures for extra security
- โจ Keep It Simple: Donโt put entire application logic in procedures
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Banking Transaction System
Create a secure banking system with stored procedures:
๐ Requirements:
- โ Transfer money between accounts with validation
- ๐ท๏ธ Check account balance before transfers
- ๐ค Log all transactions for audit
- ๐ Generate monthly statements
- ๐จ Each transaction needs a unique reference!
๐ Bonus Points:
- Add fraud detection
- Implement transaction limits
- Create account interest calculations
๐ก Solution
๐ Click to see solution
# ๐ฏ Secure banking transaction system!
import pymysql
from datetime import datetime
from decimal import Decimal
import uuid
class BankingSystem:
def __init__(self, db_config):
self.db_config = db_config
# ๐ฐ Transfer money between accounts
def transfer_money(self, from_account, to_account, amount):
connection = pymysql.connect(**self.db_config)
try:
with connection.cursor() as cursor:
# ๐ฏ Generate unique transaction reference
transaction_ref = f"TXN-{uuid.uuid4().hex[:8].upper()}"
# ๐ Call transfer stored procedure
success = False
message = ''
new_balance = Decimal('0.00')
args = [
from_account, # IN: source account
to_account, # IN: destination account
amount, # IN: transfer amount
transaction_ref, # IN: unique reference
success, # OUT: success flag
message, # OUT: status message
new_balance # OUT: new balance
]
cursor.callproc('transfer_funds', args)
# ๐ Get output parameters
cursor.execute(
'SELECT @_transfer_funds_4, ' +
'@_transfer_funds_5, @_transfer_funds_6'
)
result = cursor.fetchone()
success = result[0]
message = result[1]
new_balance = result[2]
if success:
connection.commit()
print(f"โ
Transfer successful! Ref: {transaction_ref}")
print(f"๐ฐ New balance: ${new_balance}")
else:
connection.rollback()
print(f"โ Transfer failed: {message}")
return {
'success': success,
'reference': transaction_ref,
'message': message,
'new_balance': new_balance
}
except Exception as e:
connection.rollback()
print(f"๐ฑ Transaction error: {e}")
raise
finally:
connection.close()
# ๐ Check account balance
def check_balance(self, account_number):
connection = pymysql.connect(**self.db_config)
try:
with connection.cursor() as cursor:
cursor.callproc('get_account_balance', [account_number])
result = cursor.fetchone()
if result:
balance = result[0]
print(f"๐ณ Account {account_number}")
print(f"๐ฐ Balance: ${balance}")
return balance
finally:
connection.close()
# ๐ Generate statement
def generate_statement(self, account_number, month, year):
connection = pymysql.connect(**self.db_config)
try:
with connection.cursor() as cursor:
# ๐
Call statement generation procedure
cursor.callproc('generate_monthly_statement', [
account_number,
month,
year
])
# ๐ Get statement data
transactions = cursor.fetchall()
print(f"\n๐ STATEMENT FOR {month}/{year}")
print("=" * 50)
total_debit = Decimal('0.00')
total_credit = Decimal('0.00')
for txn in transactions:
date, desc, amount, type_ = txn
emoji = "โ" if type_ == 'DEBIT' else "โ"
print(f"{emoji} {date}: {desc} - ${amount}")
if type_ == 'DEBIT':
total_debit += amount
else:
total_credit += amount
print("=" * 50)
print(f"๐ Total Credits: ${total_credit}")
print(f"๐ Total Debits: ${total_debit}")
print(f"๐ฐ Net Change: ${total_credit - total_debit}")
return transactions
finally:
connection.close()
# ๐ฎ Test it out!
bank = BankingSystem({
'host': 'localhost',
'user': 'bank_user',
'password': 'secure_password',
'database': 'banking_db'
})
# ๐ธ Make a transfer
bank.transfer_money('ACC-12345', 'ACC-67890', Decimal('100.00'))
# ๐ฐ Check balance
bank.check_balance('ACC-12345')
# ๐ Generate statement
bank.generate_statement('ACC-12345', 3, 2024)
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Call stored procedures from Python with confidence ๐ช
- โ Handle parameters (IN, OUT, INOUT) like a pro ๐ก๏ธ
- โ Process multiple result sets efficiently ๐ฏ
- โ Implement secure database operations with procedures ๐
- โ Build high-performance database applications ๐
Remember: Stored procedures are powerful tools that can make your database operations faster, safer, and more maintainable! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered calling stored procedures from Python!
Hereโs what to do next:
- ๐ป Practice with the banking system exercise above
- ๐๏ธ Create stored procedures for your existing projects
- ๐ Learn about database-specific features (triggers, functions)
- ๐ Explore async database operations for high-performance apps
Remember: Every database expert started by calling their first stored procedure. Keep practicing, keep learning, and most importantly, have fun building amazing database-driven applications! ๐
Happy coding! ๐๐โจ