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

功能優(yōu)勢

  • 基于JavaScript,簡單易用。
  • 使用Google的V8引擎,性能強(qiáng)大。
  • 使SQL函數(shù)更加豐富。

安裝部署

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

    運行環(huán)境會話獨立,如果會話切換,則會初始化新的JS運行上下文,保證數(shù)據(jù)獨立。

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

使用指南

PLV8可以在PostgreSQL內(nèi)部執(zhí)行多種類型的函數(shù)調(diào)用,也可以使用多個綁定到PLV8對象的內(nèi)置函數(shù)。

  • 標(biāo)量函數(shù)調(diào)用
    在PLV8中,您通常可以使用CREATE FUNCTION語句在JavaScript中編寫您需要調(diào)用的函數(shù)。示例如下:
    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)
    在內(nèi)部,該函數(shù)定義如下:
    (function(keys, vals) {
    var o = {};
    for(var i=0; i<keys.length; i++){
    o[keys[i]] = vals[i];
    }
    return o;
    })
    說明 其中。
    • keysvals在PostgreSQL內(nèi)部進(jìn)行類型檢查和驗證,并作為函數(shù)的參數(shù)調(diào)用。
    • o作為JSON類型返回給PostgreSQL的對象。如果在創(chuàng)建函數(shù)時省略了參數(shù)名稱,它們將在函數(shù)中以$1$2等形式出現(xiàn)。
  • 集合返回函數(shù)調(diào)用
    PLV8支持從函數(shù)調(diào)用返回集合。
    CREATE TYPE rec AS (i integer, t text);
    CREATE FUNCTION set_of_records() RETURNS SETOF rec AS
    $$
    // plv8.return _next() 將記錄保存在內(nèi)部元組存儲中,
    // 并在函數(shù)終止時返回所有記錄。
    plv8.return_next( { "i": 1, "t": "a" } );
    plv8.return_next( { "i": 2, "t": "b" } );
    
    // 您還可以采用JSON數(shù)組形式返回記錄。
    return [ { "i": 3, "t": "c" }, { "i": 4, "t": "d" } ];
    $$
    LANGUAGE plv8;
    執(zhí)行結(jié)果如下:
    SELECT * FROM set_of_records();
    
    i | t
    ---+---
    1 | a
    2 | b
    3 | c
    4 | d
    (4 rows)
    說明
    • 如果函數(shù)聲明為RETURNS SETOF,每次調(diào)用函數(shù)時PLV8都會準(zhǔn)備一個元組存儲。您可以根據(jù)需要多次調(diào)用plv8.return_next()函數(shù)來返回一行。此外,您還可以通過返回一個數(shù)組來添加一組記錄。
    • 如果return_next()的參數(shù)對象具有參數(shù)未定義的額外屬性,則return_next()會引發(fā)錯誤。
  • 觸發(fā)器函數(shù)調(diào)用
    PLV8支持觸發(fā)器函數(shù)調(diào)用:
    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');
    如果觸發(fā)器類型是INSERTUPDATE,您可以制定NEW變量的屬性來更改此操作存儲的實際元組。
    PLV8觸發(fā)器函數(shù)包含以下特殊變量:
    • NEW
    • OLD
    • TG_NAME
    • TG_WHEN
    • TG_LEVEL
    • TG_OP
    • TG_RELID
    • TG_TABLE_NAME
    • TG_TABLE_SCHEMA
    • TG_ARGV
    更多信息,請參見PostgreSQL官方文檔觸發(fā)器函數(shù)
  • 內(nèi)聯(lián)語句調(diào)用
    PostgreSQL 9.0及以上版本,PLV8支持DO塊。
    DO $$ plv8.elog(NOTICE, 'this', 'is', 'inline', 'code') $$ LANGUAGE plv8;
  • JavaScript和數(shù)據(jù)庫內(nèi)置類型之間的自動映射
    對于結(jié)果和參數(shù),如果所需的數(shù)據(jù)庫列類型是以下之一,則會自動映射數(shù)據(jù)庫列類型和JavaScript數(shù)據(jù)類型。
    • oid
    • bool
    • int2
    • int4
    • int8
    • float4
    • float8
    • numeric
    • date
    • timestamp
    • timestamptz
    • bytea
    • json (>= 9.2)
    • jsonb (>= 9.4)
    如果以上類型是JavaScript兼容的,則可以轉(zhuǎn)換成功。 否則,PLV8會嘗試通過cstring表示形式對其進(jìn)行轉(zhuǎn)換。 僅當(dāng)維度為1時才支持?jǐn)?shù)組類型。 JavaScript對象在使用時將映射到元組。 除這些類型外,PLV8還支持ANYELEMENTANYARRAY等復(fù)合類型。 BYTEA的轉(zhuǎn)換略有不同,詳情請參見類型化數(shù)組
  • 類型化數(shù)組
    PLV8提供了一種類型化數(shù)組,允許快速訪問本機(jī)內(nèi)存,主要是為了在瀏覽器中支持canvas。 PLV8通過它將BYTEA和各種數(shù)組類型映射到JavaScript數(shù)組。 對于BYTEA,您可以將每個字節(jié)作為無符號字節(jié)數(shù)組訪問。 對于int2 /int4 /float4 /float8數(shù)組類型,PLV8通過使用PLV8域類型完成對每個元素的直接訪問。
    • plv8_int2array映射int2[]
    • plv8_int4array映射 int4[]
    • plv8_float4array映射float4[]
    • plv8_float8array映射float8[]
    類型化數(shù)組僅僅是說明PLV8使用快速訪問方法而不是常規(guī)方法的注解。對于這些類型化數(shù)組,只有一維數(shù)組沒有任何NULL元素。目前沒有辦法在PLV8函數(shù)中創(chuàng)建這樣的類型化數(shù)組,只有參數(shù)可以是類型化數(shù)組。您可以修改元素并返回值。類型化數(shù)組的示例如下:
    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)
  • 內(nèi)置函數(shù)
    PLV8提供了以下內(nèi)置函數(shù):
    • plv8.elog:向客戶端或PostgreSQL日志文件發(fā)送消息。錯誤級別如下所示:
      • 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類的函數(shù)都與同名的內(nèi)置SQL函數(shù)相同。
    • plv8.find_function:提供一個函數(shù)來訪問已在數(shù)據(jù)庫中注冊、定義PLV8函數(shù)的其他函數(shù)。
      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函數(shù)。 如果查找結(jié)果不是PLV8函數(shù),則會引發(fā)錯誤。 plv8.find_function()的函數(shù)簽名參數(shù)是regproc(僅函數(shù)名稱)或regprocedure(具有參數(shù)類型的函數(shù)名稱)。 對于純JavaScript函數(shù),您可以將內(nèi)部類型作為參數(shù),使用void類型作為返回類型,以確保不會發(fā)生來自SQL語句的任何調(diào)用。
    • plv8.version:PLV8對象提供的版本字符串。該字符串對應(yīng)PLV8模塊版本。
  • 通過SPI訪問數(shù)據(jù)庫

    PLV8提供了用于數(shù)據(jù)庫訪問的函數(shù),包括預(yù)編譯語句和游標(biāo)。

    • plv8.execute( sql [, args] ):執(zhí)行SQL語句并檢索結(jié)果。其中,sql參數(shù)必選,args是一個可選數(shù)組包含sql查詢中傳遞的任何參數(shù)。 對于SELECT查詢,返回值是一個對象數(shù)組。 每個對象代表一行,對象屬性映射為列名。 對于非SELECT命令,返回值是一個整數(shù),表示受影響的行數(shù)。
      var json_result = plv8.execute('SELECT * FROM tbl');
      var num_affected = plv8.execute('DELETE FROM tbl WHERE price > $1', [ 1000 ]);
    • plv8.prepare( sql [, typenames] ):打開或創(chuàng)建預(yù)編譯語句。typename參數(shù)是一個數(shù)組,其中每個元素都是一個字符串,對應(yīng)于每個綁定參數(shù)的數(shù)據(jù)庫類型名稱。 返回值是PreparedPlan類型的對象。 在退出函數(shù)之前,該對象必須通過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] ):執(zhí)行預(yù)編譯語句。其中,args參數(shù)和plv8.execute()所需的參數(shù)相同。如果語句沒有任何參數(shù),則args參數(shù)可以省略,其返回結(jié)果也和plv8.execute()相同。
    • PreparedPlan.cursor( [args] ):從預(yù)編譯語句中打開一個游標(biāo)。args參數(shù)和plv8.execute()以及PreparedPlan.execute()所需參數(shù)相同。返回值是游標(biāo)類型的對象。在退出函數(shù)之前,必須由Cursor.close()關(guān)閉。
      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():釋放預(yù)編譯語句。
    • Cursor.fetch( [nrows] ):如果省略了nrows參數(shù),則從游標(biāo)中獲取一行并將其作為對象返回(不是數(shù)組)。如果指定nrows參數(shù),則獲取與nrows參數(shù)相同的行數(shù),直至超過該行,并返回一個對象數(shù)組。 如果是負(fù)值,則向后獲取。
    • Cursor.move( [nrows] ):將游標(biāo)移動到nrows指定的值。如果是負(fù)值則向后移動。
    • Cursor.close():關(guān)閉游標(biāo)。
    • plv8.subtransaction( func ):每次執(zhí)行時,plv8.execute()會創(chuàng)建一個子事務(wù)。 如果需要原子操作,則需要調(diào)用plv8.subtransaction()來創(chuàng)建子事務(wù)塊。
      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 ...
      }
      說明 如果子事務(wù)塊中的一條SQL執(zhí)行失敗,則該事務(wù)塊中的所有操作都將回滾。 如果事務(wù)塊中的進(jìn)程引發(fā)JavaScript異常,則將繼續(xù)執(zhí)行。 因此,使用try ... catch塊來捕獲異常,并在異常發(fā)生時執(zhí)行替代操作。
  • 窗口函數(shù)

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

    說明 有關(guān)用戶自定義窗口函數(shù)的更多信息,請參見PostgreSQL官方文檔創(chuàng)建函數(shù)
    • WindowObject.get_current_position():返回分區(qū)中的當(dāng)前位置,從0開始。
    • WindowObject.get_partition_row_count():返回分區(qū)中的行數(shù)。
    • WindowObject.set_mark_position( pos ):在指定行設(shè)置標(biāo)記。 此位置上方的行將消失,后續(xù)將無法訪問。
    • 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中的參數(shù)值(從0開始)返回給該函數(shù),位置在距離當(dāng)前分區(qū)或幀中seektyperelpos行處。 seektype可以是WindowObject.SEEK_HEADWindowObject.SEEK_CURRENTWindowObject.SEEK_TAIL。 如果mark_postrue,則標(biāo)記從中獲取參數(shù)的行。 如果指定的行不在分區(qū)/幀中,則返回的值為undefined

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