PLV8是PostgreSQL數據庫受信任的JavaScript語言擴展。可以使用JavaScript來編寫PostgreSQL數據庫函數。本文介紹了如何安裝和使用PLV8插件。

功能優勢

  • 基于JavaScript,簡單易用。
  • 使用Google的V8引擎,性能強大。
  • 使SQL函數更加豐富。

安裝部署

  • 安裝PLV8到數據庫。
    CREATE EXTENSION plv8;
  • 驗證安裝結果。
    SELECT plv8_version();
    結果返回PLV8版本,表示安裝成功。
  • 運行環境。

    運行環境會話獨立,如果會話切換,則會初始化新的JS運行上下文,保證數據獨立。

  • 初始化設置。
    SET plv8.start_proc = 'xxx';
    說明
    • 只有管理員可以進行初始化設置。
    • xxx表示初始化設置的函數或變量。示例如下:
      //參考回歸文件startup.sql和startup_pre.sql
      set plv8.start_proc = startup;
      do $$ plv8.elog(NOTICE, 'foo = ' + foo) $$ language plv8;

使用指南

PLV8可以在PostgreSQL內部執行多種類型的函數調用,也可以使用多個綁定到PLV8對象的內置函數。

  • 標量函數調用
    在PLV8中,您通??梢允褂?code class="ph codeph" id="codeph-29w-rj3-cfa">CREATE FUNCTION語句在JavaScript中編寫您需要調用的函數。示例如下:
    CREATE FUNCTION plv8_test(keys TEXT[], vals TEXT[]) RETURNS JSON AS $$
    var o = {};
    for(var i=0; i<keys.length; i++){
    o[keys[i]] = vals[i];
    }
    return o;
    $$ LANGUAGE plv8 IMMUTABLE STRICT;
    
    =# SELECT plv8_test(ARRAY['name', 'age'], ARRAY['Tom', '29']);
    
    plv8_test
    ---------------------------
    {"name":"Tom","age":"29"}
    (1 row)
    在內部,該函數定義如下:
    (function(keys, vals) {
    var o = {};
    for(var i=0; i<keys.length; i++){
    o[keys[i]] = vals[i];
    }
    return o;
    })
    說明 其中。
    • keysvals在PostgreSQL內部進行類型檢查和驗證,并作為函數的參數調用。
    • o作為JSON類型返回給PostgreSQL的對象。如果在創建函數時省略了參數名稱,它們將在函數中以$1$2等形式出現。
  • 集合返回函數調用
    PLV8支持從函數調用返回集合。
    CREATE TYPE rec AS (i integer, t text);
    CREATE FUNCTION set_of_records() RETURNS SETOF rec AS
    $$
    // plv8.return _next() 將記錄保存在內部元組存儲中,
    // 并在函數終止時返回所有記錄。
    plv8.return_next( { "i": 1, "t": "a" } );
    plv8.return_next( { "i": 2, "t": "b" } );
    
    // 您還可以采用JSON數組形式返回記錄。
    return [ { "i": 3, "t": "c" }, { "i": 4, "t": "d" } ];
    $$
    LANGUAGE plv8;
    執行結果如下:
    SELECT * FROM set_of_records();
    
    i | t
    ---+---
    1 | a
    2 | b
    3 | c
    4 | d
    (4 rows)
    說明
    • 如果函數聲明為RETURNS SETOF,每次調用函數時PLV8都會準備一個元組存儲。您可以根據需要多次調用plv8.return_next()函數來返回一行。此外,您還可以通過返回一個數組來添加一組記錄。
    • 如果return_next()的參數對象具有參數未定義的額外屬性,則return_next()會引發錯誤。
  • 觸發器函數調用
    PLV8支持觸發器函數調用:
    CREATE FUNCTION test_trigger() RETURNS TRIGGER AS
    $$
        plv8.elog(NOTICE, "NEW = ", JSON.stringify(NEW));
        plv8.elog(NOTICE, "OLD = ", JSON.stringify(OLD));
        plv8.elog(NOTICE, "TG_OP = ", TG_OP);
        plv8.elog(NOTICE, "TG_ARGV = ", TG_ARGV);
        if (TG_OP == "UPDATE") {
            NEW.i = 102;
            return NEW;
        }
    $$
    LANGUAGE "plv8";
    
    CREATE TRIGGER test_trigger
        BEFORE INSERT OR UPDATE OR DELETE
        ON test_tbl FOR EACH ROW
        EXECUTE PROCEDURE test_trigger('foo', 'bar');
    如果觸發器類型是INSERTUPDATE,您可以制定NEW變量的屬性來更改此操作存儲的實際元組。
    PLV8觸發器函數包含以下特殊變量:
    • NEW
    • OLD
    • TG_NAME
    • TG_WHEN
    • TG_LEVEL
    • TG_OP
    • TG_RELID
    • TG_TABLE_NAME
    • TG_TABLE_SCHEMA
    • TG_ARGV
    更多信息,請參見PostgreSQL官方文檔觸發器函數
  • 內聯語句調用
    PostgreSQL 9.0及以上版本,PLV8支持DO塊。
    DO $$ plv8.elog(NOTICE, 'this', 'is', 'inline', 'code') $$ LANGUAGE plv8;
  • JavaScript和數據庫內置類型之間的自動映射
    對于結果和參數,如果所需的數據庫列類型是以下之一,則會自動映射數據庫列類型和JavaScript數據類型。
    • oid
    • bool
    • int2
    • int4
    • int8
    • float4
    • float8
    • numeric
    • date
    • timestamp
    • timestamptz
    • bytea
    • json (>= 9.2)
    • jsonb (>= 9.4)
    如果以上類型是JavaScript兼容的,則可以轉換成功。 否則,PLV8會嘗試通過cstring表示形式對其進行轉換。 僅當維度為1時才支持數組類型。 JavaScript對象在使用時將映射到元組。 除這些類型外,PLV8還支持ANYELEMENTANYARRAY等復合類型。 BYTEA的轉換略有不同,詳情請參見類型化數組。
  • 類型化數組
    PLV8提供了一種類型化數組,允許快速訪問本機內存,主要是為了在瀏覽器中支持canvas。 PLV8通過它將BYTEA和各種數組類型映射到JavaScript數組。 對于BYTEA,您可以將每個字節作為無符號字節數組訪問。 對于int2 /int4 /float4 /float8數組類型,PLV8通過使用PLV8域類型完成對每個元素的直接訪問。
    • plv8_int2array映射int2[]
    • plv8_int4array映射 int4[]
    • plv8_float4array映射float4[]
    • plv8_float8array映射float8[]
    類型化數組僅僅是說明PLV8使用快速訪問方法而不是常規方法的注解。對于這些類型化數組,只有一維數組沒有任何NULL元素。目前沒有辦法在PLV8函數中創建這樣的類型化數組,只有參數可以是類型化數組。您可以修改元素并返回值。類型化數組的示例如下:
    CREATE FUNCTION int4sum(ary plv8_int4array) RETURNS int8 AS $$
      var sum = 0;
      for (var i = 0; i < ary.length; i++) {
        sum += ary[i];
      }
      return sum;
    $$ LANGUAGE plv8 IMMUTABLE STRICT;
    
    SELECT int4sum(ARRAY[1, 2, 3, 4, 5]);
     int4sum
    ---------
          15
    (1 row)
  • 內置函數
    PLV8提供了以下內置函數:
    • plv8.elog:向客戶端或PostgreSQL日志文件發送消息。錯誤級別如下所示:
      • DEBUG5
      • DEBUG4
      • DEBUG3
      • DEBUG2
      • DEBUG1
      • LOG
      • INFO
      • NOTICE
      • WARNING
      • ERROR
      示例:
      var msg = 'world';
      
      plv8.elog(DEBUG1, 'Hello', `${msg}!`);
    • plv8.quote_literal、plv8.nullable、plv8.quote_ident:每個quote類的函數都與同名的內置SQL函數相同。
    • plv8.find_function:提供一個函數來訪問已在數據庫中注冊、定義PLV8函數的其他函數。
      CREATE FUNCTION callee(a int) RETURNS int AS $$ return a * a $$ LANGUAGE plv8;
      CREATE FUNCTION caller(a int, t int) RETURNS int AS $$
        var func = plv8.find_function("callee");
        return func(a);
      $$ LANGUAGE plv8;
      使用plv8.find_function(),您可以查找其他PLV8函數。 如果查找結果不是PLV8函數,則會引發錯誤。 plv8.find_function()的函數簽名參數是regproc(僅函數名稱)或regprocedure(具有參數類型的函數名稱)。 對于純JavaScript函數,您可以將內部類型作為參數,使用void類型作為返回類型,以確保不會發生來自SQL語句的任何調用。
    • plv8.version:PLV8對象提供的版本字符串。該字符串對應PLV8模塊版本。
  • 通過SPI訪問數據庫

    PLV8提供了用于數據庫訪問的函數,包括預編譯語句和游標。

    • plv8.execute( sql [, args] ):執行SQL語句并檢索結果。其中,sql參數必選,args是一個可選數組包含sql查詢中傳遞的任何參數。 對于SELECT查詢,返回值是一個對象數組。 每個對象代表一行,對象屬性映射為列名。 對于非SELECT命令,返回值是一個整數,表示受影響的行數。
      var json_result = plv8.execute('SELECT * FROM tbl');
      var num_affected = plv8.execute('DELETE FROM tbl WHERE price > $1', [ 1000 ]);
    • plv8.prepare( sql [, typenames] ):打開或創建預編譯語句。typename參數是一個數組,其中每個元素都是一個字符串,對應于每個綁定參數的數據庫類型名稱。 返回值是PreparedPlan類型的對象。 在退出函數之前,該對象必須通過plan.free()釋放。
      var plan = plv8.prepare( 'SELECT * FROM tbl WHERE col = $1', ['int'] );
      var rows = plan.execute( [1] );
      var sum = 0;
      for (var i = 0; i < rows.length; i++) {
        sum += rows[i].num;
      }
      plan.free();
      
      return sum;
    • PreparedPlan.execute( [args] ):執行預編譯語句。其中,args參數和plv8.execute()所需的參數相同。如果語句沒有任何參數,則args參數可以省略,其返回結果也和plv8.execute()相同。
    • PreparedPlan.cursor( [args] ):從預編譯語句中打開一個游標。args參數和plv8.execute()以及PreparedPlan.execute()所需參數相同。返回值是游標類型的對象。在退出函數之前,必須由Cursor.close()關閉。
      var plan = plv8.prepare( 'SELECT * FROM tbl WHERE col = $1', ['int'] );
      var cursor = plan.cursor( [1] );
      var sum = 0, row;
      while (row = cursor.fetch()) {
          sum += row.num;
      }
      cursor.close();
      plan.free();
      
      return sum;
    • PreparedPlan.free():釋放預編譯語句。
    • Cursor.fetch( [nrows] ):如果省略了nrows參數,則從游標中獲取一行并將其作為對象返回(不是數組)。如果指定nrows參數,則獲取與nrows參數相同的行數,直至超過該行,并返回一個對象數組。 如果是負值,則向后獲取。
    • Cursor.move( [nrows] ):將游標移動到nrows指定的值。如果是負值則向后移動。
    • Cursor.close():關閉游標。
    • plv8.subtransaction( func ):每次執行時,plv8.execute()會創建一個子事務。 如果需要原子操作,則需要調用plv8.subtransaction()來創建子事務塊。
      try{
        plv8.subtransaction(function(){
          plv8.execute("INSERT INTO tbl VALUES(1)"); // should be rolled back!
          plv8.execute("INSERT INTO tbl VALUES(1/0)"); // occurs an exception
        });
      } catch(e) {
        ... do fall back plan ...
      }
      說明 如果子事務塊中的一條SQL執行失敗,則該事務塊中的所有操作都將回滾。 如果事務塊中的進程引發JavaScript異常,則將繼續執行。 因此,使用try ... catch塊來捕獲異常,并在異常發生時執行替代操作。
  • 窗口函數

    您可以使用PLV8創建自定義的窗口函數。 PLV8通過封裝C-level窗口函數API以支持全部功能。 為創建窗口函數,首先需要調用plv8.get_window_object()來創建窗口對象,plv8.get_window_object()提供以下接口:

    說明 有關用戶自定義窗口函數的更多信息,請參見PostgreSQL官方文檔創建函數。
    • WindowObject.get_current_position():返回分區中的當前位置,從0開始。
    • WindowObject.get_partition_row_count():返回分區中的行數。
    • WindowObject.set_mark_position( pos ):在指定行設置標記。 此位置上方的行將消失,后續將無法訪問。
    • WindowObject.rows_are_peers( pos1, pos2 ):如果pos1和pos2的行是對等的,則返回true
    • WindowObject.get_func_arg_in_partition( argno, relpos, seektype, mark_pos )、WindowObject.get_func_arg_in_frame( argno, relpos, seektype, mark_pos ):

      argno中的參數值(從0開始)返回給該函數,位置在距離當前分區或幀中seektyperelpos行處。 seektype可以是WindowObject.SEEK_HEAD,WindowObject.SEEK_CURRENTWindowObject.SEEK_TAIL。 如果mark_postrue,則標記從中獲取參數的行。 如果指定的行不在分區/幀中,則返回的值為undefined。

    • WindowObject.get_func_arg_in_current( argno ):將argno中的參數值(從0開始)返回給該函數的當前行處。
      說明 返回值與該函數的參數變量相同。
    • WindowObject.get_partition_local( [size] ):返回分區本地值,該值在當前分區終止時釋放。 如果沒有存儲任何內容,則返回undefinedsize參數(默認為1000)是第一次調用中分配的內存大小。 一旦分配內存,size值就不會改變。
    • WindowObject.set_partition_local( obj ):存儲分區本地值,您可以通過get_partition_local()檢索該值。 此函數在內部使用JSON.stringify()來序列化對象,因此,如果傳入一個無法序列化的值,則最終可能會是一個異常的值。 如果序列化值的大小超過分配的內存,將引發異常。