簡體   English   中英

MySQL存儲過程還是php代碼?

[英]MySQL stored procedures or php code?

一個普遍的問題,沒有特定的情況 - 通常首選使用MySQL存儲過程而不是編寫執行相同計算和查詢的PHP腳本?

每種方法有什么好處?

點對點與傑夫阿特伍德“誰需要存儲過程,無論如何?” 從2004年開始

1)存儲過程用大型數據庫“語言”編寫,如PL / SQL(Oracle)或T-SQL(Microsoft)。 這些所謂的語言是陳舊的,充滿了瘋狂,不連貫的設計選擇,這些選擇總是源於十年向后兼容性的曲折演變。 你真的不想在這些東西中寫很多代碼。 對於上下文,JavaScript是PL / SQL或T-SQL的一大進步。

響應 :“SQL”中的“S”表示“結構化”,而不是“標准化” - PLSQL和TSQL都是SQL的自定義擴展 ,它也使ANSI SQL發揮作用,因為幾乎沒有SQL與數據庫無關。 通常,如果您希望查詢運行良好, 則不能依賴ANSI SQL。

ORM不是銀彈 - 由於數據庫抽象,大多數支持運行本機存儲過程/函數以獲得性能良好的查詢。 這很好,但完全違背了ORM的目的......

我永遠不會理解為什么網絡開發,無數技術的混合(HTML,Javascript / AJAX,Flash ......)總是將SQL作為家庭的黑羊分開。 像所有其他人一樣,你必須學習它以獲得一些東西。 必須是使用其他技術時獲得的即時滿足感......

2)存儲過程通常無法在您編寫UI的同一IDE中進行調試。 每當我在procs中隔離異常時,我必須停止我正在做的事情,破壞我的Toad副本,並加載數據庫包以查看出錯的地方。 經常在兩個完全不同的IDE之間進行轉換,具有完全不同的接口和語言,並不是完全有效的。

回復 :Eclipse或Visual Studio中最初是否有Javascript調試器? 不,他們允許使用插件,以便將產品推向市場,並激活以前不存在的市場。 大多數在Visual Studio / Eclipse之外使用Firebug沒有問題,為什么SQL調試會有所不同?

3)當出現問題時,存儲過程不會提供太多反饋。 除非proc通過奇怪的T-SQL或PL / SQL異常處理進行編碼,否則我們會根據失敗的proc中的特定行返回神秘的“錯誤”,例如Table沒有行。 呃,好嗎?

回應 :你缺乏熟悉並不是一種糟糕的語言。 就像你從來沒有用你選擇的語言谷歌那樣奇怪的錯誤...至少Oracle和MySQL會給你錯誤的參考號。

4)存儲過程不能傳遞對象。 所以,如果你不小心,你最終可能會得到很多參數。 如果你必須使用proc填充20多個字段的表行,請向20+參數問好。 最糟糕的是,如果我傳遞一個錯誤的參數 - 太多,不夠或不好的數據類型 - 我得到一個通用的“錯誤調用”錯誤。 Oracle無法告訴我哪些參數出錯! 因此,我必須手工填寫20多個參數,以確定哪一個是罪魁禍首。

響應 :SQL基於SET,完全不同於過程/ OO編程。 類型接近於對象,但在某些時候,需要在過程/ OO對象和數據庫實體之間進行映射。

5)存儲過程隱藏業務邏輯。 我不知道proc正在做什么,或者它將返回給我什么樣的游標(DataSet)或值。 我無法查看proc的源代碼(至少,如果我有適當的訪問權限,不使用#2)來驗證它實際上是在做我認為的那樣 - 或者設計者打算做什么。 內聯SQL可能不是很漂亮,但至少我可以在上下文中看到它,以及其他業務邏輯。

響應 :這是一件好事(tm) - 這就是你如何獲得模型 - 視圖 - 控制器(MVC),這樣你就可以擁有多種語言的前端,而無需每次處理每種語言時都復制邏輯'怪癖要復制那種邏輯。 或者,如果有人直接連接到數據庫,數據庫是否允許添加錯誤數據? 在應用程序和數據庫之間來回跳閘浪費時間和資源,您的應用程序永遠無法收回。

我認為傑夫阿特伍德在2004年關於存儲過程觸及了頭部:

無論如何,誰需要存儲過程?

廣泛使用存儲過程和動態SQL我絕對更喜歡后者:更容易管理,更好的封裝,數據訪問層沒有BL,更大的靈活性等等。 事實上,每個主要的開源PHP項目都使用動態SQL而不是存儲過程(參見:Drupal,Wordpress,Magento等等)。

這個對話幾乎看起來很古老:給自己一個好的ORM ,不要為了數據訪問而煩惱,並開始構建出色的應用程序。

對我們來說,使用存儲過程絕對是至關重 我們有一個相當大的.net應用程序。 要重新部署整個應用,可以讓我們的用戶離線一小段時間,這是不允許的。

但是,在應用程序運行時,我們有時必須對查詢進行微小的更正。 簡單的事情,如添加或刪除NOLOCK,甚至可能更改所涉及的連接。 這幾乎總是出於性能原因。 就在今天,我們遇到了一個由無關的NOLOCK引起的錯誤。 2分鍾找到問題,確定解決方案,並部署新的proc:零停機時間。 要在代碼中進行查詢,至少會造成輕微的中斷,這可能會讓很多人感到沮喪。

另一個原因是安全。 使用proc,我們將用戶ID(非順序,不可猜測)傳遞到每個proc調用中。 我們驗證用戶是否有權在Web應用程序中運行該功能,並再次在數據庫內部運行。 如果我們的網絡應用受到損害,這將徹底增加黑客的障礙。 他們不僅不能運行他們想要的任何sql,而且即使運行proc他們也必須擁有特定的授權密鑰。這將很難獲得。 (不,這不是我們唯一的防守)

我們在源代碼管理下有我們的proc,所以這不是問題。 此外,我不必擔心我如何命名(某些ORM討厭某些命名方案),我不必擔心飛行性能。 您必須了解的不僅僅是SQL才能正確調整ORM。您必須了解ORM的特定行為。

100個存儲過程99次。如果我是topick 1的原因,那么如果你的php web應用程序通過存儲過程完成所有數據庫訪問並且你的數據庫用戶只有permision來執行所述存儲過程那么你將100%受到保護SQL注入攻擊。

對我來說,與數據庫中的數據庫保持任何關系的優點是調試。 如果您在存儲過程中完成了計算(至少大部分),並且需要進行更改,那么您只需修改它,測試它,保存它。 您的PHP代碼不會發生任何變化。

如果要在PHP代碼中存儲主要計算,則需要從代碼中獲取SQL語句,清理它,然后修改它,測試它,然后將其復制回來再次測試。

通過保持獨立性,可以輕松實現易於維護。 代碼看起來更干凈,並且如果使用存儲過程則更容易閱讀,因為我們都知道SQL腳本會變得非常大。 將所有數據庫邏輯保留在數據庫中。

如果對數據庫進行了適當的調整,那么執行查詢的時間可能會稍微快一點,因為不是讓PHP解析字符串,而是將其發送到數據庫,然后數據庫執行它並將其發回,你可以使用存儲過程將參數推送到數據庫中,它將具有存儲過程的緩存執行計划,並且事情會稍微快一些。 一些精心布置的索引可以幫助加速任何數據檢索,因為實際上 - Web服務器只是一個管道,PHP腳本不會加載它。

出於多種原因,我盡可能多地使用存儲過程。

減少到數據庫的往返

如果需要一次更改多個相關表,則可以使用單個存儲過程,以便只對數據庫進行一次調用。

明確定義業務邏輯

如果關於查詢的某些事情必須是正確的,那么存儲過程可以讓知道SQL(一種相當簡單的語言)的人確保事情正確完成。

為其他程序員創建簡單的接口

你的非sql主管隊友可以使用更簡單的數據庫接口,你可以確定他們不會在事故中將關系置於糟糕狀態。

考慮:

SELECT a.first_name, IFNULL( b.plan_id, 0 ) AS plan_id
FROM account AS a
   LEFT JOIN subscription AS s ON s.account_id = a.id
WHERE a.id = 23

相比:

CALL account_get_current_plan_id( 23 );

給它們寫一個很好的小包裝器來處理存儲過程調用,它們都在業務中。

立即更新系統中的所有用法

如果每個人都使用存儲過程來查詢數據庫,並且您需要更改某些內容的工作方式,則可以更新存儲過程,只要您不更改接口,它就會在任何地方更新。

強制安全

如果您只能使用存儲過程來執行系統中的所有操作,則可以對訪問數據的用戶帳戶授予嚴格限制的權限。 無需為其提供UPDATE,DELETE或甚至SELECT權限。

易於錯誤處理

許多人沒有意識到這一點,但您可以創建存儲過程,以便跟蹤大多數問題變得非常容易。

如果使用良好的結構,您甚至可以集成代碼庫以正確處理返回的錯誤。

以下是執行以下操作的示例:

  • 使用退出處理程序來解決嚴重問題
  • 對不太嚴重的問題使用繼續處理程序
  • 是否預先進行非表掃描驗證
  • 如果驗證沒有失敗,表格是否會掃描驗證
  • 如果事情有效,則在事務中處理
  • 如果出現問題,請回滾所有內容
  • 報告發現的任何問題
  • 避免不必要的更新

這是一個組成存儲過程的內部接受帳戶ID,結束帳戶ID和IP地址,然后使用它們進行適當更新。 分隔符已設置為$$:

BEGIN

        # Helper variables

        DECLARE r_code INT UNSIGNED;
        DECLARE r_message VARCHAR(128);
        DECLARE it_exists INT UNSIGNED;
        DECLARE n_affected INT UNSIGNED;

        # Exception handler - for when you have written bad code
        # - or something really bad happens to the server
        DECLARE EXIT HANDLER FOR SQLEXCEPTION
        BEGIN
            ROLLBACK;
            SELECT 0 as `id`, 10001 as `code`, CONCAT(r_message, ' Failed with exception') as `message`;
        END;

        # Warning handler - to tell you exactly where problems are
        DECLARE CONTINUE HANDLER FOR SQLWARNING
        BEGIN
            SET r_code = 20001, r_message = CONCAT( r_message, 'WARNING' );
        END;

        SET r_code = 0, r_message = '', it_exists = 0, n_affected = 0;

        # STEP 1 - Obvious basic sanity checking (no table scans needed)
        IF ( 0 = i_account_id ) THEN
            SET r_code = 40001, r_message = 'You must specify an account to close';
        ELSEIF ( 0 = i_updated_by_id ) THEN
            SET r_code = 40002, r_message = 'You must specify the account doing the closing';
        END IF;

        # STEP 2 - Any checks requiring table scans

        # Given account must exist in system
        IF ( 0 = r_code ) THEN
            SELECT COUNT(id) INTO it_exists
            FROM account 
            WHERE id = i_account_id;

            IF ( 0 = it_exists ) THEN
                SET r_code = 40001, r_message = 'Account to close does not exist in the system';
            END IF;
        END IF;

        # Given account must not already be closed
        # - if already closed, we simply treat the call as a success
        # - and don't bother with further processing
        IF ( 0 = r_code ) THEN
            SELECT COUNT(id) INTO it_exists 
            FROM account 
            WHERE id = i_account_id AND status_id = 2;

            IF ( 0 < it_exists ) THEN
                SET r_code = 1, r_message = 'already closed';
            END IF;
        END IF;

        # Given closer account must be valid
        IF ( 0 = r_code ) THEN
            SELECT COUNT(id) INTO it_exists 
            FROM account
            WHERE id = i_updated_by_id;
        END IF;

        # STEP 3 - The actual update and related updates
        # r-message stages are used in case of warnings to tell exactly where a problem occurred
        IF ( 0 = r_code ) THEN
            SET r_message = CONCAT(r_message, 'a');

            START TRANSACTION;

            # Add the unmodified account record to our log
            INSERT INTO account_log ( field_list )
            SELECT field_list 
            FROM account 
            WHERE id = i_account_id;

            IF ( 0 = r_code ) THEN
                SET n_affected = ROW_COUNT();
                IF ( 0 = n_affected ) THEN
                    SET r_code = 20002, r_message = 'Failed to create account log record';
                END IF;
            END IF;

            # Update the account now that we have backed it up
            IF ( 0 = r_code ) THEN
                SET r_message = CONCAT( r_message, 'b' );

                UPDATE account 
                SET 
                    status_id = 2,
                    updated_by_id = i_updated_by_id,
                    updated_by_ip = i_updated_by_ip
                WHERE id = i_account_id;

                IF ( 0 = r_code ) THEN
                    SET n_affected = ROW_COUNT();
                    IF ( 0 = n_affected ) THEN
                        SET r_code = 20003, r_message = 'Failed to update account status';
                    END IF;
                END IF;
            END IF;

            # Delete some related data
            IF ( 0 = r_code ) THEN
                SET r_message = CONCAT( r_message, 'c' );

                DELETE FROM something 
                WHERE account_id = i_account_id;
            END IF;

            # Commit or roll back our transaction based on our current code
            IF ( 0 = r_code ) THEN
                SET r_code = 1, r_message = 'success';
                COMMIT;
            ELSE 
                ROLLBACK;
            END IF;
        END IF;

        SELECT 
            r_code as `code`,
            r_message as `message`,
            n_affected as `affected`;

    END$$

狀態代碼含義:

  • 0:永遠不應該發生 - 結果不好
  • 1:成功 - 帳戶已關閉或已正確關閉
  • 2XXXX - 邏輯或語法問題
  • 3XXXX - 系統中意外數據值的問題
  • 4XXXX - 缺少必填字段

而不是信任不熟悉數據庫(或者根本不熟悉該方案)的程序員,提供它們接口要簡單得多。

他們可以簡單地使用:而不是進行所有上述檢查。

CALL account_close_by_id( 23 );

然后檢查結果代碼並采取適當的措施。

就個人而言,我相信如果你有權訪問存儲過程並且你沒有使用它們,那么你真的應該考慮使用它們。

我會說“不要對數據庫做太多魔術”。 最糟糕的情況是項目中的新開發人員注意到**操作**已完成,但他無法看到代碼中的位置。 所以他一直在尋找它。 但它是在數據庫中完成的。

因此,如果您執行一些“隱形”數據庫操作(我正在考慮觸發器),只需在一些代碼文檔中編寫它。

// add a new user
$user = new User("john", "doe");
$user->save();
// The id is computed by the database see MYPROC_ID_COMPUTATION
print $user->getId();

另一方面,為DB編寫函數是個好主意,並為開發人員提供了一個很好的抽象層。

// Computes an ID for the given user
DB->execute("SELECT COMPUTE_ID(" . $user->getLogin() . ") FROM DUAL");

當然這都是偽代碼,但我希望你理解我晦澀難懂的想法。

好吧,我很少聽到這個論點的一面,所以我會在這里寫一下......

代碼是版本控制的。 數據庫不是。 因此,如果您有多個代碼實例,則需要某種方式在更新時自動執行遷移,否則您將面臨破壞風險的風險。 即便如此,您仍然會面臨“忘記”將更新的SP添加到遷移腳本,然后打破構建的問題(如果您沒有測試REALLY idepth,可能甚至沒有意識到)。

從調試和維護,我發現SP的100x與原始SQL一樣難以剖析。 原因是它至少需要三個步驟。 首先,查看PHP代碼以查看調用的代碼。 然后進入數據庫並找到該過程。 然后最后看一下程序的代碼。

另一個論點(沿着版本控制的方向)是SP沒有svn st命令。 因此,如果您有一個手動修改SP的開發人員,那么您將會花費一些時間來解決這個問題(假設它們並非全部由單個DBA管理)。

SP的真正亮點在於您有多個應用程序與同一數據庫模式進行通信。 然后,您只有一個存儲DDL和DML的位置,並且兩個應用程序都可以共享它,而無需在一個或多個庫中添加交叉依賴。

總之,我的觀點如下:

使用存儲過程:

  1. 當您有多個應用程序處理相同的數據集時
  2. 當您需要循環查詢並執行其他查詢時(避免TCP層丟失可以極大地提高效率)
  3. 如果你有一個非常好的DBA,因為它將強制執行他/她正在處理的所有SQL。

在任何其他情況下使用原始SQL / ORM /生成的SQL(差不多,因為肯定會有邊緣情況我不會考慮)...

再說一次,那只是我0.02美元......

在可能的情況下,最終用戶將受益於UI中數據的抽象。 因此,您應該盡可能地嘗試利用存儲過程。

如果在數據庫上執行計算,則不一定需要基礎值,然后讓數據庫執行它們。 這有助於將數據庫與PHP腳本之間的數據傳輸量保持在最低水平; 但通常使用數據庫數據進行計算最好由數據庫本身執行。

我聽說有人說“讓數據庫盡可能多地做”,而其他人則喊道:“wtf,你對我的數據庫性能做了什么”。

所以我想它應該主要是使用率的決定(存儲過程會強調MySQL進程,PHP代碼會強調Web服務器進程)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM