精通 Oracle+Python 事務和大型對象


通過 Python 管理數據事務、處理大型對象

事務包含一組 SQL 語句,這組 SQL 語句構成數據庫中的一個邏輯操作,如轉帳或信用卡支付操作。將 SQL 語句聚合到一個邏輯組中,其效果完全取決於事務的成敗,事務成功則提交更改,事務失敗則撤銷內部 SQL 的結果(整體撤消)。通過 Python,您可以利用 Oracle 數據庫所提供的原子性、一致性、孤立性和持久性優勢。

利用大型對象,可在一列中保存大量數據(從 Oracle Databaase 11g 起該數量可達到 128TB),但這種靈活性是要付出代價的 — 用於訪問和操作 LOB 的方法不同於常規查詢方法。

注意:Python 的 2.x 版本已升級到 2.6,cx_Oracle 模塊已發展到 5.0。從現在起,在 MO+P 中將使用這些版本。此外,本教程依舊基於可用於 Oracle Database 10g 第 2 版快捷版中的 HR 模式。

這就是 ACID

一個數據庫事務是一些語句組成的一個邏輯組,具有以下四個特征:

  • 原子性:所有操作要么全部成功,要么全部失敗
  • 一致性:提交事務不會導致數據損壞
  • 孤立性:其他事務始終不知道此事務的執行
  • 持久性:即使在數據庫崩潰的情況下,事務中提交的操作也將持續有效。

Python Oracle 數據庫 API 提供了一種處理事務的自然方式,可將對數據的邏輯操作存儲在數據庫回滾段中以等待最終的決定:是提交還是回滾整組語句。

當第一條 SQL 語句通過 cursor.execute() 方法傳給數據庫時,一個事務就啟動了。當沒有其他事務已從該會話啟動時,可以使用 db.begin() 方法顯式啟動一個新事務。為了獲得最高一致性,當連接對象被關閉或刪除時,cx_Oracle 會默認回滾所有事務。

cx_Oracle.Connection.autocommit 屬性仍可設置為 1,從而使 Oracle 可提交通過 cursor.execute* 系列方法發出的每條語句。開發人員還應知道,由於 Oracle 的 DDL 不是事務性的,所有 DDL 語句都會隱式提交。最后,與 SQL*Plus 相反,用 db.close() 關閉連接不會提交正在進行的事務。

有一些 Oracle 事務語句包裝在 cx_Oracle 中,如 db.begin()、db.commit() 和 db.rollback(),但您可通過顯式地調用 SET TRANSACTION 語句來使用其余事務語句。無論如何,新的事務從會話中的首個 DML 開始,因此沒必要顯式進行,除非您需要利用特定的事務屬性,如 SET TRANSACTION [ READ ONLY | ISOLATION LEVEL SERIALIZABLE ]。

我們來看一個執行具有事務特征的 HR 操作的類:

import cx_Oracle
 
class HR:
  def __enter__(self):
    self.__db = cx_Oracle.Connection("hr/hrpwd@localhost:1521/XE")
    self.__cursor = self.__db.cursor()
    return self 
  
  def __exit__(self, type, value, traceback): 
    self.__db.close()
  
  def swapDepartments(self, employee_id1, employee_id2):
    assert employee_id1!=employee_id2
    
    select_sql = """select employee_id, department_id from employees
      where employee_id in (:1, :2)"""
    update_sql = "update employees set department_id=:1 where employee_id=:2"

    self.__db.begin()
    self.__cursor.execute(select_sql, (employee_id1, employee_id2))
    D = dict(self.__cursor.fetchall())
    self.__cursor.execute(update_sql, (D[employee_id2], employee_id1))
    self.__cursor.execute(update_sql, (D[employee_id1], employee_id2))
    self.__db.commit()
 
  def raiseSalary(self, employee_ids, raise_pct):
    update_sql = "update employees set salary=salary*:1 where employee_id=:2"
    
    self.__db.begin()

    for employee_id in employee_ids:
      try:
        self.__cursor.execute(update_sql, [1+raise_pct/100, employee_id])
        assert self.__cursor.rowcount==1
      except AssertionError:
        self.__db.rollback()
        raise Warning, "invalid employee_id (%s)" % employee_id

    self.__db.commit()
 
if __name__ == "__main__":
  with HR() as hr:
    hr.swapDepartments(106, 116)
    hr.raiseSalary([102, 106, 116], 20)

上述代碼中定義了兩個方法:一個方法可在指定的員工之間互換部門,另一個方法可為任意數量的員工加薪。為了安全起見,此類操作只能通過事務實現。為確保不會發生其他事務,這兩個方法中顯式調用了 self.__db.begin()(否則會引發 DatabaseError ORA-01453 錯誤)。雙下划線前綴在 Python 中具有特殊含義,因為該語言實際上不允許您聲明私有變量(類中一切皆為公有),從而增加了變量的訪問難度:變量是通過 _Class__Member 語法(上例中的 _HR__db 和 _HR_cursor)公開的。同時,我們在該類中聲明 __enter__ 和 __exit__ 方法,這樣我們可以使用 WITH 語句,在 WITH 代碼塊結束時會自動關閉連接。

除了通過 db.commit() 和 db.rollback() 語句執行 DCL 語句的標准方式外,還可使用 cursor.execute() 方法運行原始的 SQL 命令(例如,為了能夠使用保存點)。使用 cursor.execute('ROLLBACK') 和 db.rollback() 除以下之外並無任何不同:前者可以帶額外的參數,如 cursor.execute('ROLLBACK TO SAVEPOINT some_savepoint')。SQL 方法還需要對命令像一般 SQL 語句一樣解析和執行,而 db.commit() 和 db.rollback() 方法映射到一個低級 API 調用並允許 cx_Oracle 驅動程序跟蹤事務狀態。

大型對象 (LOB)

提到 Oracle 數據庫的表列可用的數據類型,VARCHAR2 最多只能存儲 4000 個字節的值。大型對象以其存儲大型數據(如文本、圖像、視頻和其他多媒體格式)的能力而適用於大容量存儲的情況。並且以您幾乎不能稱之為有限的存儲能力而適用於這種情況 — 一個 LOB 的最大容量可高達 128 TB(自 11g 第 2 版開始)。

在 Oracle 數據庫中可以使用幾種類型的 LOB:二進制大型對象 (BLOB)、字符大型對象 (CLOB)、國家字符集大型對象 (NCLOB) 和外部二進制文件 (BFILE)。最后這種 LOB 用於以只讀模式訪問外部操作系統文件,而所有其他類型的 LOB 能以永久模式或臨時模式在數據庫中存儲大量數據。每個 LOB 包含一個實際值和一個指向該值的小型定位器。傳遞 LOB 通常只是意味着傳遞 LOB 定位器。

在任何給定時間,一個 LOB 只能處於以下三種已定義狀態之一:NULL、empty 或 populated。這類似於其他 RDBMS 引擎中常規 VARCHAR 列的行為(empty 字符串不等同於 NULL)。最后,對 LOB 有幾個限制,其中的主要限制是:

  • LOB 不能是主鍵
  • LOB 不能是集群的一部分
  • LOB 不能與 DISTINCT、ORDER BY 和 GROUP BY 子句一起使用

Oracle Database Application Developer's Guide 中提供了有關大型對象的大量文檔資料。

Python 的 cx_Oracle 模塊支持對所有類型 LOB 的訪問:

>>> [i for i in dir(cx_Oracle) if i.endswith('LOB') or i=='BFILE'] 
['BFILE', 'BLOB', 'CLOB', 'LOB', 'NCLOB'] 

cx_Oracle 中針對所有那些不能自動斷定其長度或類型的 Oracle 類型提供了一種特殊的數據類型。該數據類型專門用於處理存儲過程的 IN/OUT 參數。我們來看一個變量對象,它填充了 1 MB 的數據(將一個哈希字符重復 2 的 20 次方):

 
>>> db = cx_Oracle.Connection('hr/hrpwd@localhost:1521/XE') 
>>> cursor = db.cursor() 
>>> clob = cursor.var(cx_Oracle.CLOB) 
>>> clob.setvalue(0, '#'*2**20) 

為了在 Python 中創建一個 LOB 對象,我們將一個 cx_Oracle.CLOB 類型傳遞給了該 Variable 對象的構造函數,該對象提供兩個基本方法(另外還有別的方法):

  • getvalue(pos=0) 用於獲取給定位置(默認為 0)的值
  • setvalue(pos, value) 用於在給定位置設置值

為了進行一些 LOB 試驗,我們使用下面的 DDL 創建如下所列的一些對象:

 
CREATE TABLE lobs (
  c CLOB,
  nc NCLOB,
  b BLOB,
  bf BFILE
);

  
CREATE VIEW v_lobs AS
SELECT
  ROWID id,
  c,
  nc,
  b,
  bf,
  dbms_lob.getlength(c) c_len,
  dbms_lob.getlength(nc) nc_len,
  dbms_lob.getlength(b) b_len,
  dbms_lob.getlength(bf) bf_len
FROM lobs;

在本示例中,BFILE 定位器將指向 /tmp 目錄中的一個文件。以 SYSTEM 用戶身份運行:

 
create directory lob_dir AS '/tmp';
grant read on directory lob_dir to HR;

最后,使用編輯器創建 /tmp/example.txt 文件,其中包含您所選的任何虛擬文本。

為了深入了解用於 LOB 表的默認的表創建選項,試着用 DBMS_METADATA.GET_DDL 過程生成全部 DDL:

SET LONG 5000
SELECT 
  DBMS_METADATA.GET_DDL('TABLE', TABLE_NAME)
FROM USER_TABLES
WHERE TABLE_NAME = 'LOBS';

輸出結果中有兩個有趣的參數值得一瞧:

  • ENABLE STORAGE IN ROW 指示 Oracle 在 LOB 數據不超過 4000 個字節減去系統元數據這個大小時,嘗試將該 LOB 數據與表數據放在一起(相反的參數是 DISABLE STORAGE IN ROW)。
  • CHUNK 確定處理 LOB 時分配的字節數(在 Python 中可通過 cx_Oracle.LOB.getchunksize() 方法訪問該信息)。

創建表時可使用這些選項對其行為和性能進行調優。

以下代碼是使用大型對象的一個更為完整的示例。其中顯示了四種不同類型的 LOB 以及用於將 LOB 插入數據庫或從數據庫選擇 LOB 的四個方法。

# -*- coding: utf8 -*- 
import cx_Oracle
import operator
import os
from hashlib import md5
from random import randint 
  
class LobExample:
  def __enter__(self):
    self.__db = cx_Oracle.Connection("hr/hrpwd@localhost:1521/XE")
    self.__cursor = self.__db.cursor()
    return self 
  
  def __exit__(self, type, value, traceback):
    # calling close methods on cursor and connection -
    # this technique can be used to close arbitrary number of cursors
    map(operator.methodcaller("close"), (self.__cursor, self.__db)) 
  
  def clob(self):
    # populate the table with large data (1MB per insert) and then
    # select the data including dbms_lob.getlength for validating assertion
    self.__cursor.execute("INSERT INTO lobs(c) VALUES(:1)", ["~"*2**20])
    self.__cursor.execute("SELECT c, c_len FROM v_lobs WHERE c IS NOT NULL") 
    c, c_len = self.__cursor.fetchone()
    clob_data = c.read()
    assert len(clob_data)==c_len
    self.__db.rollback()
     
  def nclob(self):
    unicode_data = u"€"*2**20
    # define variable object holding the nclob unicode data
    nclob_var = self.__cursor.var(cx_Oracle.NCLOB)
    nclob_var.setvalue(0, unicode_data) 
    self.__cursor.execute("INSERT INTO lobs(nc) VALUES(:1)", [nclob_var])
    self.__cursor.execute("SELECT nc, nc_len FROM v_lobs WHERE nc IS NOT NULL") 
    nc, nc_len = self.__cursor.fetchone()
    # reading only the first character just to check if encoding is right
    nclob_substr = nc.read(1, 1)
    assert nclob_substr==u"€"
    self.__db.rollback() 
  
  def blob(self):
    # preparing the sample binary data with random 0-255 int and chr function
    binary_data = "".join(chr(randint(0, 255)) for c in xrange(2**2))
    binary_md5 = md5(binary_data).hexdigest()
    binary_var = self.__cursor.var(cx_Oracle.BLOB)
    binary_var.setvalue(0, binary_data) 
    self.__cursor.execute("INSERT INTO lobs(b) VALUES(:1)", [binary_var])
    self.__cursor.execute("SELECT b FROM v_lobs WHERE b IS NOT NULL") 
    b, = self.__cursor.fetchone()
    blob_data = b.read()
    blob_md5 = md5(blob_data).hexdigest()
    # data par is measured in hashes equality, what comes in must come out
    assert binary_md5==blob_md5
    self.__db.rollback() 
  
  def bfile(self):
    # to insert bfile we need to use the bfilename function
    self.__cursor.execute("INSERT INTO lobs(bf) VALUES(BFILENAME(:1, :2))",
      ["LOB_DIR", "example.txt"])
    self.__cursor.execute("SELECT bf FROM v_lobs WHERE bf IS NOT NULL") 
    # selecting is as simple as reading other types of large objects
    bf, = self.__cursor.fetchone()
    bfile_data = bf.read()
    assert bfile_data
    self.__db.rollback() 
  
if __name__ == "__main__":
  with LobExample() as eg:
    eg.clob()
    eg.nclob()
    eg.blob()
    eg.bfile()

該文件包含 UTF-8 字符,因此第一行包含 Python 的源代碼編碼聲明 (PEP-0263)。為確保以該編碼方式將數據傳輸給數據庫,應將環境變量 NLS_LANG 設置為“.AL32UTF8”。這向 Oracle Client 庫指明了應使用哪個字符集。該變量的設置應在 Oracle Client 初始化其內部數據結構之前進行,但不能保證發生在程序的哪個特定點。為安全起見,最好在調用該程序的 shell 環境中設置該變量。源代碼中包含了其他一些解釋和注釋。 

這里要注意幾點。就 NCLOB 示例而言,unicode_data 不能用作綁定變量,因為如果它超過 4000 個字符,會引發“ValueError:unicode data too large”異常。BLOB 示例中將出現類似的問題。如果我們不使用 binary_var,則會引發“DatabaseError:ORA-01465:invalid hex number”異常,因為必須顯式地聲明綁定的內容。

通過 LOB 參數(IN 或者 OUT)調用存儲過程還需要使用 cx_Oracle 的 Variable 對象,但這是本系列另一部分要討論的內容。

總結

在本教程中,您已經了解 Python 環境中有關事務處理和大型對象處理的各個方面。現在您應該已經熟悉了 cx_Oracle 模塊在事務封裝和訪問所有四種 LOB 以及應對 UTF-8 編碼需求這些方面的特點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM