當應用程式的商業邏輯變得複雜時,將部分邏輯封裝在資料庫層的預存程序中可以帶來諸多優勢。預存程序在資料庫伺服器上預先編譯與最佳化,相較於每次都傳送 SQL 語句到伺服器解析執行,可以顯著提升效能。此外預存程序提供了一個清晰的介面來封裝複雜的資料操作,多個應用程式可以共用相同的邏輯而不需要重複實作,這大幅提升了程式碼的可維護性與一致性。

然而在實務開發中,不同的應用程式可能使用不同的程式語言。前端服務可能使用 Python 的 Flask 或 Django 框架,後端的批次處理可能使用 Java 的 Spring Boot,資料分析腳本可能使用 Python 的科學運算函式庫。這些不同語言的應用程式都需要存取相同的 MySQL 資料庫,並呼叫相同的預存程序來執行資料操作。理解如何在不同程式語言中正確呼叫預存程序,對於建構多語言的應用架構至關重要。

本文將深入探討 Python 與 Java 這兩種廣泛使用的程式語言如何呼叫 MySQL 預存程序。我們將從基礎的連線建立開始,逐步深入到參數傳遞、結果集處理與錯誤處理等進階主題。透過完整的程式碼範例與詳細的註解,讀者將能夠掌握跨語言資料庫整合的核心技術,並理解兩種語言在實作上的差異與共通原則。

MySQL 預存程序基礎

在開始實作應用程式之前,需要先在 MySQL 資料庫中建立測試用的預存程序。預存程序本質上是一組預先定義的 SQL 語句集合,可以接受輸入參數並回傳結果集或輸出參數。以下範例建立了一個簡單的預存程序,用於查詢特定地理位置的山峰資訊。

-- 建立資料庫與資料表
-- 這個資料庫將儲存全球主要山峰的資訊
CREATE DATABASE IF NOT EXISTS topography
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;

USE topography;

-- 建立山峰資料表
-- 記錄山峰名稱、所在位置與海拔高度
CREATE TABLE IF NOT EXISTS mountain (
    -- 山峰唯一識別碼
    mountain_id INT AUTO_INCREMENT PRIMARY KEY,
    
    -- 山峰名稱
    mountain_name VARCHAR(100) NOT NULL,
    
    -- 所在位置(洲或國家)
    location VARCHAR(100) NOT NULL,
    
    -- 海拔高度(單位:公尺)
    height INT NOT NULL,
    
    -- 建立時間
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    -- 建立索引以加速按位置查詢
    INDEX idx_location (location)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 插入測試資料
-- 包含各洲的代表性高峰
INSERT INTO mountain (mountain_name, location, height) VALUES
    ('聖母峰', 'Asia', 8848),
    ('K2', 'Asia', 8611),
    ('干城章嘉峰', 'Asia', 8586),
    ('吉力馬札羅山', 'Africa', 5895),
    ('阿空加瓜山', 'South America', 6961),
    ('迪納利峰', 'North America', 6190),
    ('厄爾布魯士峰', 'Europe', 5642);

-- 變更分隔符號
-- 因為預存程序內部會使用分號,需要臨時變更分隔符號
DELIMITER //

-- 建立預存程序:根據位置查詢山峰
-- 這個預存程序接受一個位置參數,回傳該位置的所有山峰資訊
CREATE PROCEDURE p_get_mountain_by_loc(
    IN p_location VARCHAR(100)
)
BEGIN
    -- 查詢指定位置的山峰
    -- 按照高度降序排列
    SELECT 
        mountain_name,
        location,
        height
    FROM mountain
    WHERE location = p_location
    ORDER BY height DESC;
END//

-- 恢復預設分隔符號
DELIMITER ;

這個預存程序展示了最基本的模式:接受輸入參數並回傳結果集。在實務中預存程序可能包含更複雜的邏輯,例如多個查詢、條件判斷、迴圈處理或呼叫其他預存程序。無論邏輯多複雜,從應用程式呼叫的基本模式都是相同的。

Python 呼叫 MySQL 預存程序

Python 透過 mysql-connector-python 函式庫提供了完整的 MySQL 資料庫存取能力。這個官方維護的函式庫實作了 Python 資料庫 API 規範(PEP 249),提供了一致的介面來操作資料庫。呼叫預存程序時,可以使用特殊的 callproc 方法,或是直接執行 CALL 語句。

"""
Python 呼叫 MySQL 預存程序範例

展示如何使用 mysql-connector-python 建立連線
呼叫預存程序並處理結果集

作者:玄貓(BlackCat)
"""

import mysql.connector
from mysql.connector import Error
from typing import List, Tuple, Optional

class MountainDatabase:
    """
    山峰資料庫存取類別
    
    封裝資料庫連線管理與預存程序呼叫邏輯
    提供乾淨的介面給應用程式使用
    """
    
    def __init__(self, host: str, database: str, user: str, password: str):
        """
        初始化資料庫連線參數
        
        參數:
            host: 資料庫主機位址
            database: 資料庫名稱
            user: 使用者名稱
            password: 密碼
        """
        self.config = {
            'host': host,
            'database': database,
            'user': user,
            'password': password,
            # 使用 utf8mb4 字元集以支援完整的 Unicode
            'charset': 'utf8mb4',
            # 啟用自動重新連線
            'autocommit': True,
            # 設定連線逾時時間(秒)
            'connection_timeout': 10
        }
        self.connection = None
    
    def connect(self) -> bool:
        """
        建立資料庫連線
        
        回傳:
            連線成功回傳 True,失敗回傳 False
        """
        try:
            # 使用配置字典建立連線
            self.connection = mysql.connector.connect(**self.config)
            
            if self.connection.is_connected():
                # 取得資料庫版本資訊
                db_info = self.connection.get_server_info()
                print(f'成功連線到 MySQL 伺服器版本 {db_info}')
                return True
            
        except Error as error:
            print(f'連線失敗:{error}')
            return False
    
    def get_mountains_by_location_v1(self, location: str) -> List[Tuple]:
        """
        使用 callproc 方法呼叫預存程序
        
        這是呼叫預存程序的標準方式
        適用於需要處理 IN/OUT 參數的複雜情況
        
        參數:
            location: 要查詢的位置
            
        回傳:
            山峰資料的元組串列
        """
        if not self.connection or not self.connection.is_connected():
            print('錯誤:資料庫未連線')
            return []
        
        try:
            # 建立遊標物件
            # buffered=True 會立即取得所有結果,避免未讀取結果的錯誤
            cursor = self.connection.cursor(buffered=True)
            
            # 使用 callproc 方法呼叫預存程序
            # 參數以串列形式傳遞
            cursor.callproc('p_get_mountain_by_loc', [location])
            
            # 預存程序可能回傳多個結果集
            # stored_results() 方法取得所有結果集
            results = []
            for result_cursor in cursor.stored_results():
                # fetchall() 取得結果集的所有列
                results.extend(result_cursor.fetchall())
            
            # 關閉遊標以釋放資源
            cursor.close()
            
            return results
            
        except Error as error:
            print(f'執行預存程序時發生錯誤:{error}')
            return []
    
    def get_mountains_by_location_v2(self, location: str) -> List[Tuple]:
        """
        使用 execute 方法直接執行 CALL 語句
        
        這是另一種呼叫預存程序的方式
        語法更直覺,但需要手動處理參數化查詢
        
        參數:
            location: 要查詢的位置
            
        回傳:
            山峰資料的元組串列
        """
        if not self.connection or not self.connection.is_connected():
            print('錯誤:資料庫未連線')
            return []
        
        try:
            cursor = self.connection.cursor(buffered=True)
            
            # 直接執行 CALL 語句
            # 使用參數化查詢防止 SQL 注入
            # %s 是參數佔位符,實際值透過第二個參數傳遞
            sql = "CALL p_get_mountain_by_loc(%s)"
            cursor.execute(sql, (location,))
            
            # 取得結果集
            results = cursor.fetchall()
            
            cursor.close()
            
            return results
            
        except Error as error:
            print(f'執行預存程序時發生錯誤:{error}')
            return []
    
    def display_results(self, results: List[Tuple], location: str):
        """
        格式化顯示查詢結果
        
        參數:
            results: 查詢結果
            location: 查詢的位置
        """
        if not results:
            print(f'\n{location} 找不到任何山峰資料')
            return
        
        print(f'\n{location} 的山峰資訊:')
        print(f'{"山峰名稱":<20} {"位置":<20} {"高度(公尺)":<15}')
        print('-' * 55)
        
        for mountain_name, loc, height in results:
            print(f'{mountain_name:<20} {loc:<20} {height:<15}')
    
    def close(self):
        """
        關閉資料庫連線
        """
        if self.connection and self.connection.is_connected():
            self.connection.close()
            print('\n資料庫連線已關閉')

# 使用範例
if __name__ == '__main__':
    # 資料庫連線參數
    # 實務上應該從環境變數或設定檔讀取,避免寫死在程式碼中
    db = MountainDatabase(
        host='localhost',
        database='topography',
        user='top_app',
        password='your_password_here'
    )
    
    # 建立連線
    if db.connect():
        # 測試兩種呼叫方式
        print('\n=== 使用 callproc 方法 ===')
        results = db.get_mountains_by_location_v1('Asia')
        db.display_results(results, 'Asia')
        
        print('\n=== 使用 execute 方法 ===')
        results = db.get_mountains_by_location_v2('South America')
        db.display_results(results, 'South America')
        
        # 關閉連線
        db.close()
@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100

|Python 應用程式|
start

:載入連線參數;
note right
  從設定檔或環境變數
  讀取敏感資訊
end note

:建立 MountainDatabase 物件;

:呼叫 connect() 方法;

|MySQL 伺服器|
:驗證使用者認證;

if (認證成功?) then (是)
  :建立連線;
  
  |Python 應用程式|
  :連線成功;
  
  :準備查詢參數\n(location = 'Asia');
  
  :呼叫 callproc() 或 execute();
  note right
    callproc('p_get_mountain_by_loc', ['Asia'])
    或
    execute("CALL p_get_mountain_by_loc(%s)", ('Asia',))
  end note
  
  |MySQL 伺服器|
  :執行預存程序;
  
  :查詢 mountain 資料表;
  
  :回傳結果集;
  
  |Python 應用程式|
  :使用 fetchall() 取得結果;
  
  :處理並顯示資料;
  
  :關閉遊標;
  
  :關閉連線;
  
  stop
else (否)
  :連線失敗;
  
  |Python 應用程式|
  :顯示錯誤訊息;
  
  stop
endif

@enduml

Python 的實作展示了物件導向的設計方式,將資料庫操作封裝在類別中。這種設計提供了更好的程式碼組織與可維護性,連線管理與錯誤處理都被適當地封裝,應用程式只需要呼叫簡單的方法就能完成複雜的資料庫操作。

Java 呼叫 MySQL 預存程序

Java 透過 JDBC(Java Database Connectivity)API 提供了標準化的資料庫存取介面。雖然 JDBC 是一個標準 API,但實際的資料庫驅動程式由各資料庫廠商提供。MySQL 的官方 JDBC 驅動程式是 MySQL Connector/J,需要在專案中加入對應的相依套件。

/**
 * Java 呼叫 MySQL 預存程序範例
 * 
 * 展示如何使用 JDBC 與 CallableStatement 呼叫預存程序
 * 包含連線管理、參數傳遞與結果集處理
 * 
 * @author 玄貓(BlackCat)
 */

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * 山峰資料類別
 * 
 * 用於儲存從資料庫查詢到的山峰資訊
 */
class Mountain {
    private String name;
    private String location;
    private int height;
    
    public Mountain(String name, String location, int height) {
        this.name = name;
        this.location = location;
        this.height = height;
    }
    
    public String getName() {
        return name;
    }
    
    public String getLocation() {
        return location;
    }
    
    public int getHeight() {
        return height;
    }
    
    @Override
    public String toString() {
        return String.format("%-20s %-20s %-15d", name, location, height);
    }
}

/**
 * 山峰資料庫存取類別
 * 
 * 封裝所有資料庫操作邏輯
 * 提供類型安全的介面給應用程式使用
 */
class MountainDatabase {
    // JDBC 連線 URL
    // jdbc:mysql:// 是 MySQL 的協定前綴
    // localhost:3306 是主機與埠號
    // topography 是資料庫名稱
    // useSSL=false 在開發環境中關閉 SSL(生產環境應啟用)
    // serverTimezone=UTC 設定時區
    private static final String DB_URL = 
        "jdbc:mysql://localhost:3306/topography?useSSL=false&serverTimezone=UTC";
    
    // 資料庫使用者認證資訊
    private static final String DB_USER = "top_app";
    private static final String DB_PASSWORD = "your_password_here";
    
    // 連線物件
    private Connection connection;
    
    /**
     * 建立資料庫連線
     * 
     * 使用 DriverManager 建立連線
     * MySQL Connector/J 8.0 之後不需要手動載入驅動程式
     * 
     * @return 連線成功回傳 true,失敗回傳 false
     */
    public boolean connect() {
        try {
            // 建立連線
            // DriverManager 會自動載入 JDBC 驅動程式
            connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
            
            // 取得資料庫中繼資料
            String dbProduct = connection.getMetaData().getDatabaseProductName();
            String dbVersion = connection.getMetaData().getDatabaseProductVersion();
            
            System.out.println("成功連線到 " + dbProduct + " 版本 " + dbVersion);
            
            return true;
            
        } catch (SQLException e) {
            System.err.println("連線失敗:" + e.getMessage());
            return false;
        }
    }
    
    /**
     * 呼叫預存程序查詢山峰資料
     * 
     * 使用 CallableStatement 呼叫預存程序
     * 這是 JDBC 中呼叫預存程序的標準方式
     * 
     * @param location 要查詢的位置
     * @return 山峰資料串列
     */
    public List<Mountain> getMountainsByLocation(String location) {
        List<Mountain> mountains = new ArrayList<>();
        
        // 檢查連線狀態
        if (connection == null) {
            System.err.println("錯誤:資料庫未連線");
            return mountains;
        }
        
        // SQL CALL 語句
        // ? 是參數佔位符,會透過 setString 方法設定實際值
        String sql = "CALL p_get_mountain_by_loc(?)";
        
        // try-with-resources 語法
        // 自動關閉 CallableStatement 與 ResultSet
        // 即使發生例外也會確保資源被釋放
        try (CallableStatement stmt = connection.prepareCall(sql)) {
            
            // 設定 IN 參數
            // 第一個參數索引為 1(JDBC 使用 1-based 索引)
            // 第二個參數是實際的值
            stmt.setString(1, location);
            
            // 執行預存程序並取得結果集
            // executeQuery() 用於回傳結果集的查詢
            // executeUpdate() 用於 INSERT/UPDATE/DELETE
            // execute() 可以處理任何類型的 SQL 語句
            ResultSet rs = stmt.executeQuery();
            
            // 遍歷結果集
            // next() 方法將遊標移動到下一列
            // 當沒有更多列時回傳 false
            while (rs.next()) {
                // 從結果集取得資料
                // getString() 取得字串類型的欄位
                // getInt() 取得整數類型的欄位
                // 可以使用欄位名稱或索引(1-based)
                String mountainName = rs.getString("mountain_name");
                String loc = rs.getString("location");
                int height = rs.getInt("height");
                
                // 建立 Mountain 物件並加入串列
                mountains.add(new Mountain(mountainName, loc, height));
            }
            
            // ResultSet 會在 try-with-resources 區塊結束時自動關閉
            
        } catch (SQLException e) {
            System.err.println("執行預存程序時發生錯誤:" + e.getMessage());
            e.printStackTrace();
        }
        
        return mountains;
    }
    
    /**
     * 顯示查詢結果
     * 
     * @param mountains 山峰資料串列
     * @param location 查詢的位置
     */
    public void displayResults(List<Mountain> mountains, String location) {
        if (mountains.isEmpty()) {
            System.out.println("\n在 " + location + " 找不到任何山峰資料");
            return;
        }
        
        System.out.println("\n" + location + " 的山峰資訊:");
        System.out.println(String.format("%-20s %-20s %-15s", "山峰名稱", "位置", "高度(公尺)"));
        System.out.println("-".repeat(55));
        
        for (Mountain mountain : mountains) {
            System.out.println(mountain);
        }
    }
    
    /**
     * 關閉資料庫連線
     */
    public void close() {
        if (connection != null) {
            try {
                connection.close();
                System.out.println("\n資料庫連線已關閉");
            } catch (SQLException e) {
                System.err.println("關閉連線時發生錯誤:" + e.getMessage());
            }
        }
    }
}

/**
 * 主程式類別
 */
public class MountainList {
    public static void main(String[] args) {
        // 建立資料庫存取物件
        MountainDatabase db = new MountainDatabase();
        
        // 建立連線
        if (db.connect()) {
            // 查詢亞洲的山峰
            List<Mountain> asianMountains = db.getMountainsByLocation("Asia");
            db.displayResults(asianMountains, "Asia");
            
            // 查詢南美洲的山峰
            List<Mountain> southAmericanMountains = db.getMountainsByLocation("South America");
            db.displayResults(southAmericanMountains, "South America");
            
            // 關閉連線
            db.close();
        }
    }
}
@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100

|Java 應用程式|
start

:定義連線參數\n(URL, USER, PASSWORD);

:建立 MountainDatabase 物件;

:呼叫 connect() 方法;

|JDBC DriverManager|
:載入 MySQL Connector/J;

:解析 JDBC URL;

|MySQL 伺服器|
:驗證使用者認證;

if (認證成功?) then (是)
  :建立連線;
  
  |Java 應用程式|
  :取得 Connection 物件;
  
  :準備 CALL 語句\n"CALL p_get_mountain_by_loc(?)";
  
  :建立 CallableStatement;
  
  :設定參數\nstmt.setString(1, "Asia");
  
  :執行 executeQuery();
  
  |MySQL 伺服器|
  :執行預存程序;
  
  :查詢資料表;
  
  :回傳 ResultSet;
  
  |Java 應用程式|
  :遍歷 ResultSet;
  
  while (還有資料?) is (是)
    :取得欄位值;
    
    :建立 Mountain 物件;
    
    :加入串列;
  endwhile (否)
  
  :關閉 ResultSet;
  
  :關閉 CallableStatement;
  
  :顯示結果;
  
  :關閉 Connection;
  
  stop
else (否)
  :連線失敗;
  
  |Java 應用程式|
  :顯示錯誤訊息;
  
  stop
endif

@enduml

Java 的實作展示了強型別語言的特性,透過 Mountain 類別提供了類型安全的資料封裝。try-with-resources 語法確保了資源的正確釋放,即使在發生例外的情況下也能避免資源洩漏。這種嚴謹的資源管理在企業級應用中特別重要。

連線管理與安全性最佳實務

無論使用哪種程式語言,在操作資料庫時都需要注意幾個關鍵的安全與效能議題。首先是連線資訊的安全管理,絕對不應該將資料庫密碼硬編碼在程式碼中。在 Python 中可以使用 python-dotenv 函式庫從環境變數讀取配置,在 Java 中可以使用 Properties 檔案或 Spring 的配置管理機制。

# Python 使用環境變數管理敏感資訊
from dotenv import load_dotenv
import os

# 載入 .env 檔案中的環境變數
load_dotenv()

# 從環境變數讀取連線參數
db = MountainDatabase(
    host=os.getenv('DB_HOST', 'localhost'),
    database=os.getenv('DB_NAME', 'topography'),
    user=os.getenv('DB_USER'),
    password=os.getenv('DB_PASSWORD')
)

其次是參數化查詢的重要性,所有傳遞給預存程序的參數都應該透過參數綁定機制而非字串拼接。這不僅能防止 SQL 注入攻擊,也能讓資料庫更有效地快取執行計畫。Python 的 execute 方法與 Java 的 setString 方法都實作了參數化查詢。

第三是連線池的使用,在高並發的應用中,每次請求都建立新連線會造成顯著的效能開銷。Python 可以使用 mysql-connector-python 的連線池功能,Java 則可以使用 HikariCP 或 Apache Commons DBCP 等連線池函式庫。連線池會維護一組預先建立的連線,應用程式從池中借用連線使用完後歸還,大幅提升了效能。

從實務角度來看,Python 與 Java 在呼叫預存程序的核心概念上是一致的:建立連線、準備語句、綁定參數、執行查詢、處理結果、釋放資源。兩種語言的主要差異在於語法細節與類型系統。Python 的動態型別與簡潔語法讓程式碼更短,但缺少編譯時的型別檢查。Java 的強型別系統提供了更好的型別安全與 IDE 支援,但需要更多的樣板程式碼。

選擇哪種語言往往取決於專案的整體架構與團隊的技能。Python 適合快速開發與資料分析場景,Java 適合需要高效能與嚴格型別安全的企業應用。無論選擇哪種語言,理解預存程序的呼叫機制與最佳實務都是建構可靠資料庫應用的基礎。透過適當的錯誤處理、資源管理與安全措施,可以確保應用程式在各種情況下都能穩定運作。