PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Pemicu PostgreSQL dan Dasar-dasar Fungsi Tersimpan

Catatan dari Somenines:Blog ini diterbitkan secara anumerta saat Berend Tober meninggal pada 16 Juli 2018. Kami menghormati kontribusinya kepada komunitas PostgreSQL dan mendoakan kedamaian bagi teman dan penulis tamu kami.

Pada artikel sebelumnya kita telah membahas serial pseudo-type PostgreSQL, yang berguna untuk mengisi nilai kunci sintetik dengan bilangan bulat yang bertambah. Kami melihat bahwa menggunakan kata kunci tipe data serial dalam pernyataan bahasa definisi data tabel (DDL) diimplementasikan sebagai deklarasi kolom tipe integer yang diisi, pada penyisipan database, dengan nilai default yang diturunkan dari pemanggilan fungsi sederhana. Perilaku otomatis dalam menjalankan kode fungsional sebagai bagian dari respons integral terhadap aktivitas bahasa manipulasi data (DML) adalah fitur yang kuat dari sistem manajemen basis data relasional (RDBMS) yang canggih seperti PostgreSQL. Dalam artikel ini kita mempelajari lebih jauh aspek lain yang lebih mampu untuk memanggil kode kustom secara otomatis, yaitu penggunaan pemicu dan fungsi tersimpan.Pendahuluan

Gunakan Kasus untuk Pemicu dan Fungsi Tersimpan

Mari kita bicara tentang mengapa Anda mungkin ingin berinvestasi dalam memahami pemicu dan fungsi tersimpan. Dengan membangun kode DML ke dalam database itu sendiri, Anda dapat menghindari implementasi duplikat kode terkait data di beberapa aplikasi terpisah yang mungkin dibangun untuk berinteraksi dengan database. Ini memastikan eksekusi kode DML yang konsisten untuk validasi data, pembersihan data, atau fungsi lain seperti audit data (yaitu, mencatat perubahan) atau mempertahankan tabel ringkasan secara independen dari aplikasi panggilan apa pun. Penggunaan umum lainnya dari pemicu dan fungsi tersimpan adalah untuk membuat tampilan dapat ditulis, yaitu, untuk mengaktifkan penyisipan dan/atau pembaruan pada tampilan kompleks atau untuk melindungi data kolom tertentu dari modifikasi yang tidak sah. Selain itu, data yang diproses di server, bukan di kode aplikasi, tidak melintasi jaringan, sehingga risiko data terekspos ke penyadapan lebih rendah serta pengurangan kemacetan jaringan. Selain itu, dalam PostgreSQL, fungsi yang disimpan dapat dikonfigurasi untuk mengeksekusi kode pada tingkat hak istimewa yang lebih tinggi daripada pengguna sesi, yang mengakui beberapa kemampuan yang kuat. Kami akan melakukan beberapa contoh nanti.

Kasus Terhadap Pemicu dan Fungsi Tersimpan

Tinjauan komentar di milis Umum PostgreSQL mengungkapkan beberapa pendapat yang tidak mendukung penggunaan pemicu dan fungsi tersimpan yang saya sebutkan di sini untuk kelengkapan dan untuk mendorong Anda dan tim Anda mempertimbangkan pro dan kontra untuk implementasi Anda.

Di antara keberatan itu, misalnya, persepsi bahwa fungsi-fungsi yang disimpan tidak mudah untuk dipelihara, sehingga membutuhkan orang yang berpengalaman dengan keterampilan dan pengetahuan yang canggih dalam administrasi database untuk mengelolanya. Beberapa profesional perangkat lunak telah melaporkan bahwa kontrol perubahan perusahaan pada sistem basis data biasanya lebih kuat daripada pada kode aplikasi, sehingga jika aturan bisnis atau logika lain diimplementasikan dalam basis data, maka membuat perubahan saat persyaratan berkembang menjadi sangat rumit. Sudut pandang lain menganggap pemicu sebagai efek samping yang tidak terduga dari beberapa tindakan lain dan dengan demikian, mungkin tidak jelas, mudah terlewatkan, sulit untuk di-debug, dan membuat frustrasi untuk dipertahankan dan biasanya harus menjadi pilihan terakhir, bukan yang pertama.

Keberatan ini mungkin memiliki beberapa kelebihan, tetapi jika Anda memikirkannya, data adalah aset yang berharga dan jadi Anda mungkin sebenarnya menginginkan orang atau tim yang terampil dan berpengalaman yang bertanggung jawab atas RDBMS di organisasi perusahaan atau pemerintah, dan demikian pula, Ubah Papan Kontrol adalah komponen yang terbukti dari pemeliharaan berkelanjutan untuk sistem informasi catatan, dan efek samping satu orang sama baiknya dengan kenyamanan orang lain, yang merupakan sudut pandang yang diadopsi untuk keseimbangan artikel ini.

Mendeklarasikan Pemicu

Mari kita belajar tentang mur dan baut. Ada banyak opsi yang tersedia dalam sintaks DDL umum untuk mendeklarasikan pemicu, dan akan membutuhkan waktu yang signifikan untuk menangani semua kemungkinan permutasi, jadi untuk singkatnya, kita hanya akan membahas sebagian kecil dari mereka dalam contoh yang ikuti menggunakan sintaks singkat ini:

CREATE TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
    ON table_name
    FOR EACH ROW EXECUTE PROCEDURE function_name()

where event can be one of:

    INSERT
    UPDATE [ OF column_name [, ... ] ]
    DELETE
    TRUNCATE

Elemen yang dapat dikonfigurasi yang diperlukan selain nama adalah kapan , mengapa , di mana , dan apa , yaitu, waktu untuk kode pemicu dipanggil relatif terhadap tindakan pemicu (kapan), jenis spesifik dari pernyataan DML pemicu (mengapa), tabel atau tabel yang ditindaklanjuti (di mana), dan kode fungsi yang disimpan untuk dieksekusi (apa).

Mendeklarasikan Fungsi

Deklarasi pemicu di atas memerlukan spesifikasi nama fungsi, jadi secara teknis deklarasi pemicu DDL tidak dapat dijalankan sampai setelah fungsi pemicu didefinisikan sebelumnya. Sintaks DDL umum untuk deklarasi fungsi juga memiliki banyak opsi sehingga untuk pengelolaan, kami akan menggunakan sintaks yang cukup minimal untuk tujuan kami di sini:

CREATE [ OR REPLACE ] FUNCTION
    name () RETURNS TRIGGER
  { LANGUAGE lang_name
    | SECURITY DEFINER
    | SET configuration_parameter { TO value | = value | FROM CURRENT }
    | AS 'definition'
  }...

Fungsi pemicu tidak membutuhkan parameter, dan tipe yang dikembalikan harus TRIGGER. Kita akan berbicara tentang pengubah opsional seperti yang kita temui dalam contoh di bawah ini.

Skema Penamaan untuk Pemicu dan Fungsi

Ilmuwan komputer terhormat Phil Karlton telah dikaitkan sebagai menyatakan (dalam bentuk parafrase di sini) bahwa penamaan sesuatu adalah salah satu tantangan terbesar bagi tim perangkat lunak. Saya akan menyajikan di sini pemicu yang mudah digunakan dan konvensi penamaan fungsi tersimpan yang telah membantu saya dengan baik dan mendorong Anda untuk mempertimbangkan untuk mengadopsinya untuk proyek RDBMS Anda sendiri. Skema penamaan dalam contoh untuk artikel ini mengikuti pola penggunaan nama tabel terkait yang diakhiri dengan singkatan yang menunjukkan pemicu yang dideklarasikan kapan dan mengapa atribut:Huruf sufiks pertama akan berupa "b", "a", atau "i" (untuk "sebelum", "setelah", atau "bukan"), selanjutnya akan menjadi satu atau lebih dari "i" , “u”, “d”, atau “t” (untuk “insert”, “update”, “delete”, atau “truncate”), dan huruf terakhir hanyalah “t” untuk trigger. (Saya menggunakan konvensi penamaan yang serupa untuk aturan, dan dalam hal ini huruf terakhir adalah "r"). Jadi misalnya, berbagai kombinasi atribut deklarasi pemicu minimal untuk tabel bernama "my_table" adalah:

|-------------+-------------+-----------+---------------+-----------------|
|  TABLE NAME |  WHEN       |  WHY      |  TRIGGER NAME |  FUNCTION NAME  |
|-------------+-------------+-----------+---------------+-----------------|
|  my_table   |  BEFORE     |  INSERT   |  my_table_bit |  my_table_bit   |
|  my_table   |  BEFORE     |  UPDATE   |  my_table_but |  my_table_but   |
|  my_table   |  BEFORE     |  DELETE   |  my_table_bdt |  my_table_bdt   |
|  my_table   |  BEFORE     |  TRUNCATE |  my_table_btt |  my_table_btt   |
|  my_table   |  AFTER      |  INSERT   |  my_table_ait |  my_table_ait   |
|  my_table   |  AFTER      |  UPDATE   |  my_table_aut |  my_table_aut   |
|  my_table   |  AFTER      |  DELETE   |  my_table_adt |  my_table_adt   |
|  my_table   |  AFTER      |  TRUNCATE |  my_table_att |  my_table_att   |
|  my_table   |  INSTEAD OF |  INSERT   |  my_table_iit |  my_table_iit   |
|  my_table   |  INSTEAD OF |  UPDATE   |  my_table_iut |  my_table_iut   |
|  my_table   |  INSTEAD OF |  DELETE   |  my_table_idt |  my_table_idt   |
|  my_table   |  INSTEAD OF |  TRUNCATE |  my_table_itt |  my_table_itt   |
|-------------+-------------+-----------+---------------+-----------------|

Nama yang sama persis dapat digunakan untuk pemicu dan fungsi tersimpan terkait, yang sepenuhnya diizinkan di PostgreSQL karena RDBMS melacak pemicu dan fungsi tersimpan secara terpisah menurut tujuan masing-masing, dan konteks di mana nama item digunakan membuat jelas item mana yang dirujuk oleh nama tersebut.

Jadi misalnya, deklarasi pemicu yang sesuai dengan skenario baris pertama dari tabel di atas akan terlihat diimplementasikan sebagai

CREATE TRIGGER my_table_bit 
    BEFORE INSERT
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_bit();

Jika pemicu dideklarasikan dengan beberapa mengapa atribut, cukup perluas sufiks dengan tepat, mis., untuk masukkan atau perbarui pemicu, di atas akan menjadi

CREATE TRIGGER my_table_biut 
    BEFORE INSERT OR UPDATE
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_biut();

Tunjukkan Beberapa Kode Sudah!

Mari kita membuatnya nyata. Kami akan mulai dengan contoh sederhana dan kemudian memperluasnya untuk mengilustrasikan fitur lebih lanjut. Pernyataan DDL pemicu memerlukan fungsi yang sudah ada sebelumnya, seperti yang disebutkan, dan juga tabel untuk bertindak, jadi pertama-tama kita membutuhkan tabel untuk dikerjakan. Sebagai contoh tujuan misalkan kita perlu menyimpan data dasar identitas akun

CREATE TABLE person (
    login_name varchar(9) not null primary key,
    display_name text
);

Beberapa penegakan integritas data dapat ditangani hanya dengan DDL kolom yang tepat, seperti dalam hal ini persyaratan bahwa nama_login ada dan panjangnya tidak lebih dari sembilan karakter. Upaya untuk memasukkan nilai NULL atau nilai login_name yang terlalu panjang gagal dan melaporkan pesan kesalahan yang berarti:

INSERT INTO person VALUES (NULL, 'Felonious Erroneous');
ERROR:  null value in column "login_name" violates not-null constraint
DETAIL:  Failing row contains (null, Felonious Erroneous).

INSERT INTO person VALUES ('atoolongusername', 'Felonious Erroneous');
ERROR:  value too long for type character varying(9)

Penegakan lain dapat ditangani dengan batasan pemeriksaan, seperti membutuhkan panjang minimum dan menolak karakter tertentu:

ALTER TABLE person 
    ADD CONSTRAINT PERSON_LOGIN_NAME_NON_NULL 
    CHECK (LENGTH(login_name) > 0);

ALTER TABLE person 
    ADD CONSTRAINT person_login_name_no_space 
    CHECK (POSITION(' ' IN login_name) = 0);

INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  new row for relation "person" violates check constraint "person_login_name_non_null"
DETAIL:  Failing row contains (, Felonious Erroneous).

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  new row for relation "person" violates check constraint "person_login_name_no_space"
DETAIL:  Failing row contains (space man, Major Tom).

tetapi perhatikan bahwa pesan kesalahan tidak sepenuhnya informatif seperti sebelumnya, hanya menyampaikan sebanyak yang dikodekan dalam nama pemicu daripada pesan teks penjelasan yang bermakna. Dengan menerapkan logika pemeriksaan dalam fungsi tersimpan, Anda dapat menggunakan pengecualian untuk mengirimkan pesan teks yang lebih bermanfaat. Juga, periksa ekspresi batasan tidak boleh berisi subkueri atau merujuk ke variabel selain kolom dari baris saat ini atau tabel database lainnya.

Jadi mari kita hilangkan batasan pemeriksaan

ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;

dan lanjutkan dengan pemicu dan fungsi tersimpan.

Tunjukkan Beberapa Kode Lainnya

Kami punya meja. Pindah ke fungsi DDL, kita mendefinisikan fungsi bertubuh kosong, yang dapat kita isi nanti dengan kode tertentu:

CREATE OR REPLACE FUNCTION person_bit() 
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    SET search_path = public
    AS '
    BEGIN
    END;
    ';

Hal ini memungkinkan kita untuk akhirnya sampai ke DDL pemicu yang menghubungkan tabel dan fungsi sehingga kita dapat melakukan beberapa contoh:

CREATE TRIGGER person_bit 
    BEFORE INSERT ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

PostgreSQL memungkinkan fungsi yang disimpan untuk ditulis dalam berbagai bahasa yang berbeda. Dalam kasus ini dan contoh berikut, kami menyusun fungsi dalam bahasa PL/pgSQL yang dirancang khusus untuk PostgreSQL dan mendukung penggunaan semua tipe data, operator, dan fungsi RDBMS PostgreSQL. Opsi SET SCHEMA mengatur jalur pencarian skema yang akan digunakan selama eksekusi fungsi. Menyetel jalur pencarian untuk setiap fungsi adalah praktik yang baik, karena tidak perlu menambahkan awalan objek database dengan nama skema dan melindungi dari kerentanan tertentu yang terkait dengan jalur pencarian.

CONTOH 0 - Validasi Data

Sebagai contoh pertama, mari kita terapkan pemeriksaan sebelumnya, tetapi dengan pesan yang lebih ramah manusia.

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;
    RETURN NEW;
    END;
    $$;

Kualifikasi "BARU" adalah referensi ke baris data yang akan dimasukkan. Ini adalah salah satu dari sejumlah variabel khusus yang tersedia dalam fungsi pemicu. Kami akan memperkenalkan beberapa lainnya di bawah ini. Perhatikan juga, PostgreSQL mengizinkan penggantian tanda kutip tunggal yang membatasi badan fungsi dengan pembatas lain, dalam hal ini mengikuti konvensi umum menggunakan tanda dolar ganda sebagai pembatas, karena badan fungsi itu sendiri menyertakan karakter kutip tunggal. Fungsi pemicu harus keluar dengan mengembalikan baris BARU yang akan dimasukkan atau NULL untuk membatalkan tindakan secara diam-diam.

Upaya penyisipan yang sama gagal seperti yang diharapkan, tetapi sekarang dengan pesan ramah:

INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  Login name must not be empty.

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  Login name must not include white space.

CONTOH 1 - Pencatatan Audit

Dengan fungsi yang disimpan, kami memiliki kebebasan yang luas tentang apa yang dilakukan kode yang dipanggil, termasuk mereferensikan tabel lain (yang tidak mungkin dilakukan dengan batasan pemeriksaan). Sebagai contoh yang lebih kompleks, kami akan membahas implementasi tabel audit, yaitu, memelihara catatan, dalam tabel terpisah, dari penyisipan, pembaruan, dan penghapusan ke tabel utama. Tabel audit biasanya berisi atribut yang sama dengan tabel utama, yang digunakan untuk mencatat nilai yang diubah, ditambah atribut tambahan untuk mencatat operasi yang dijalankan untuk membuat perubahan, serta stempel waktu transaksi, dan catatan pengguna yang membuat perubahan. ubah:

CREATE TABLE person_audit (
    login_name varchar(9) not null,
    display_name text,
    operation varchar,
    effective_at timestamp not null default now(),
    userid name not null default session_user
);

Dalam hal ini, mengimplementasikan audit sangat mudah, kami cukup memodifikasi fungsi pemicu yang ada untuk menyertakan DML untuk memengaruhi penyisipan tabel audit, lalu mendefinisikan ulang pemicu untuk mengaktifkan pembaruan serta sisipan. Perhatikan bahwa kami telah memilih untuk tidak mengubah akhiran nama fungsi pemicu menjadi “biut”, tetapi jika fungsi audit telah menjadi persyaratan yang diketahui pada waktu desain awal, nama tersebut akan digunakan:

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- New code to record audits

    INSERT INTO person_audit (login_name, display_name, operation) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP);

    RETURN NEW;
    END;
    $$;


DROP TRIGGER person_bit ON person;

CREATE TRIGGER person_biut 
    BEFORE INSERT OR UPDATE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

Perhatikan bahwa kami telah memperkenalkan variabel khusus lain "TG_OP" yang ditetapkan sistem untuk mengidentifikasi operasi DML yang memicu pemicu sebagai "INSERT", "UPDATE", "DELETE", dari "TRUNCATE", masing-masing.

Kami perlu menangani penghapusan secara terpisah dari penyisipan dan pembaruan karena tes validasi atribut berlebihan dan karena nilai khusus BARU tidak ditentukan saat masuk ke sebelum menghapus fungsi pemicu dan tentukan fungsi dan pemicu tersimpan yang sesuai:

CREATE OR REPLACE FUNCTION person_bdt()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN

    -- Record deletion in audit table

    INSERT INTO person_audit (login_name, display_name, operation) 
      VALUES (OLD.login_name, OLD.display_name, TG_OP);

    RETURN OLD;
    END;
    $$;
        
CREATE TRIGGER person_bdt 
    BEFORE DELETE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bdt();

Perhatikan penggunaan nilai khusus OLD sebagai referensi ke baris yang akan dihapus, yaitu baris seperti yang ada sebelum penghapusan terjadi.

Kami membuat beberapa sisipan untuk menguji fungsionalitas dan mengonfirmasi bahwa tabel audit menyertakan catatan sisipan:

INSERT INTO person VALUES ('dfunny', 'Doug Funny');
INSERT INTO person VALUES ('pmayo', 'Patti Mayonnaise');

SELECT * FROM person;
 login_name |   display_name   
------------+------------------
 dfunny     | Doug Funny
 pmayo      | Patti Mayonnaise
(2 rows)

SELECT * FROM person_audit;
 login_name |   display_name   | operation |        effective_at        |  userid  
------------+------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny       | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise | INSERT    | 2018-05-26 18:48:07.698623 | postgres
(2 rows)

Kemudian kami melakukan pembaruan ke satu baris dan mengonfirmasi bahwa tabel audit menyertakan catatan perubahan menambahkan nama tengah ke salah satu nama tampilan catatan data:

UPDATE person SET display_name = 'Doug Yancey Funny' WHERE login_name = 'dfunny';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 pmayo      | Patti Mayonnaise
 dfunny     | Doug Yancey Funny
(2 rows)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-26 18:48:07.698623 | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-26 18:48:07.707284 | postgres
(3 rows)

Dan terakhir kami menjalankan fungsi hapus dan mengonfirmasi bahwa tabel audit menyertakan catatan itu juga:

DELETE FROM person WHERE login_name = 'pmayo';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 dfunny     | Doug Yancey Funny
(1 row)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-27 08:13:22.747226 | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-27 08:13:22.74839  | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-27 08:13:22.749495 | postgres
 pmayo      | Patti Mayonnaise  | DELETE    | 2018-05-27 08:13:22.753425 | postgres
(4 rows)

CONTOH 2 - Nilai Turunan

Mari kita melangkah lebih jauh dan bayangkan kita ingin menyimpan beberapa dokumen teks bentuk bebas di dalam setiap baris, katakanlah resume berformat teks biasa atau makalah konferensi atau abstrak karakter hiburan, dan kita ingin mendukung penggunaan pencarian teks lengkap yang kuat kemampuan PostgreSQL pada dokumen teks bentuk bebas ini.

Kami pertama-tama menambahkan dua atribut untuk mendukung penyimpanan dokumen dan vektor pencarian teks terkait ke tabel utama. Karena vektor pencarian teks diturunkan pada basis per baris, tidak ada gunanya menyimpannya di tabel audit, jika kita menambahkan kolom penyimpanan dokumen ke tabel audit terkait:

ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Kemudian kami memodifikasi fungsi pemicu untuk memproses atribut baru ini. Kolom teks biasa ditangani dengan cara yang sama seperti data yang dimasukkan pengguna lainnya, tetapi vektor pencarian teks adalah nilai turunan dan ditangani oleh panggilan fungsi yang mereduksi teks dokumen menjadi tipe data tsvector untuk pencarian yang efisien.

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- Modified audit code to include text abstract

    INSERT INTO person_audit (login_name, display_name, operation, abstract) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP, NEW.abstract);

    -- New code to reduce text to text-search vector

    SELECT to_tsvector(NEW.abstract) INTO NEW.ts_abstract;

    RETURN NEW;
    END;
    $$;

Sebagai pengujian, kami memperbarui baris yang ada dengan beberapa teks detail dari Wikipedia:

UPDATE person SET abstract = 'Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd.' WHERE login_name = 'dfunny';

lalu konfirmasikan bahwa pemrosesan vektor pencarian teks berhasil:

SELECT login_name, ts_abstract  FROM person;
 login_name |                                                                                                                ts_abstract                                                                                                                
------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 dfunny     | '11':11 '12':13 'an':5 'and':9 'as':4 'boy':16 'crowd':24 'depicted':3 'doug':1 'fit':20 'gullible':10 'in':21 'insecure':8 'introverted':6 'is':2 'later':12 'old':15 'quiet':7 'the':23 'to':19 'wants':18 'who':17 'with':22 'year':14
(1 row)

CONTOH 3 - Pemicu &Tampilan

Vektor pencarian teks turunan dari contoh di atas tidak dimaksudkan untuk konsumsi manusia, yaitu, tidak dimasukkan oleh pengguna, dan kami tidak pernah berharap untuk menyajikan nilai tersebut kepada pengguna akhir. Jika pengguna mencoba memasukkan nilai untuk kolom ts_abstract, apa pun yang diberikan akan dibuang dan diganti dengan nilai yang diturunkan secara internal ke fungsi pemicu, jadi kami memiliki perlindungan terhadap keracunan korpus pencarian. Untuk menyembunyikan kolom sepenuhnya, kita dapat mendefinisikan tampilan ringkasan yang tidak menyertakan atribut tersebut, namun kita tetap mendapatkan manfaat dari aktivitas pemicu pada tabel yang mendasarinya:

CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;

Untuk tampilan sederhana, PostgreSQL secara otomatis membuatnya dapat ditulis sehingga kita tidak perlu melakukan hal lain untuk berhasil memasukkan atau memperbarui data. Ketika DML berlaku pada tabel yang mendasarinya, pemicu diaktifkan seolah-olah pernyataan itu diterapkan langsung ke tabel sehingga kami masih mendapatkan dukungan pencarian teks yang dieksekusi di latar belakang yang mengisi kolom vektor pencarian dari tabel orang serta menambahkan ubah informasi ke tabel audit:

INSERT INTO abridged_person VALUES ('skeeter', 'Mosquito Valentine', 'Skeeter is Doug''s best friend. He is famous in both series for the honking sounds he frequently makes.');


SELECT login_name, ts_abstract FROM person WHERE login_name = 'skeeter';
 login_name |                                                                                   ts_abstract                                                                                    
------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 skeeter    | 'best':5 'both':11 'doug':3 'famous':9 'for':13 'frequently':18 'friend':6 'he':7,17 'honking':15 'in':10 'is':2,8 'makes':19 's':4 'series':12 'skeeter':1 'sounds':16 'the':14
(1 row)


SELECT login_name, display_name, operation, userid FROM person_audit ORDER BY effective_at;
 login_name |    display_name    | operation |  userid  
------------+--------------------+-----------+----------
 dfunny     | Doug Funny         | INSERT    | postgres
 pmayo      | Patti Mayonnaise   | INSERT    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 pmayo      | Patti Mayonnaise   | DELETE    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 skeeter    | Mosquito Valentine | INSERT    | postgres
(6 rows)

Untuk tampilan yang lebih rumit yang tidak memenuhi persyaratan untuk dapat ditulis secara otomatis, baik sistem aturan atau bukan pemicu dapat melakukan pekerjaan untuk mendukung penulisan dan penghapusan.

CONTOH 4 - Nilai Ringkasan

Mari kita memperindah lebih jauh dan memperlakukan skenario di mana ada beberapa jenis tabel transaksi. Ini mungkin catatan jam kerja, penambahan dan pengurangan inventaris gudang atau stok eceran, atau mungkin daftar cek dengan debit dan kredit untuk setiap orang:

CREATE TABLE transaction (
    login_name character varying(9) NOT NULL,
    post_date date,
    description character varying,
    debit money,
    credit money,
    FOREIGN KEY (login_name) REFERENCES person (login_name)
);

Dan katakanlah meskipun penting untuk mempertahankan riwayat transaksi, aturan bisnis memerlukan penggunaan saldo bersih dalam pemrosesan aplikasi daripada detail transaksi apa pun. Untuk menghindari keharusan sering menghitung ulang saldo dengan menjumlahkan semua transaksi setiap kali saldo diperlukan, kita dapat mendenormalisasi dan menyimpan nilai saldo saat ini di sana di tabel orang dengan menambahkan kolom baru dan menggunakan pemicu dan fungsi tersimpan untuk mempertahankan saldo bersih saat transaksi dimasukkan:

ALTER TABLE person ADD COLUMN balance MONEY DEFAULT 0;

CREATE FUNCTION transaction_bit() RETURNS trigger
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    DECLARE
    newbalance money;
    BEGIN

    -- Update person account balance

    UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name
                RETURNING balance INTO newbalance;

    -- Data validation

    IF COALESCE(NEW.debit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Debit value must be non-negative';
    END IF;

    IF COALESCE(NEW.credit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Credit value must be non-negative';
    END IF;

    IF newbalance < 0::money THEN
        RAISE EXCEPTION 'Insufficient funds: %', NEW;
    END IF;

    RETURN NEW;
    END;
    $$;



CREATE TRIGGER transaction_bit 
      BEFORE INSERT ON transaction 
      FOR EACH ROW EXECUTE PROCEDURE transaction_bit();

Tampaknya aneh untuk melakukan pembaruan terlebih dahulu dalam fungsi yang disimpan sebelum memvalidasi non-negatif dari nilai debit, kredit, dan saldo, tetapi dalam hal validasi data, urutannya tidak masalah karena isi dari fungsi pemicu dijalankan sebagai transaksi basis data, jadi jika pemeriksaan validasi tersebut gagal, maka seluruh transaksi dibatalkan saat pengecualian dinaikkan. Keuntungan melakukan pembaruan terlebih dahulu adalah pembaruan mengunci baris yang terpengaruh selama durasi transaksi dan sesi lain yang mencoba memperbarui baris yang sama diblokir hingga transaksi saat ini selesai. Uji validasi lebih lanjut memastikan bahwa saldo yang dihasilkan adalah non-negatif, dan pesan informasi pengecualian dapat menyertakan variabel, yang dalam hal ini akan mengembalikan baris transaksi penyisipan yang melanggar untuk debugging.

Untuk menunjukkan bahwa ini benar-benar berfungsi, berikut adalah beberapa contoh entri dan cek yang menunjukkan saldo yang diperbarui di setiap langkah:

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name | balance 
------------+---------
 dfunny     |   $0.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-11', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$2780.52', NULL);
ERROR:  Insufficient funds: (dfunny,2018-01-17,"FOR:BGE PAYMENT ACH Withdrawal",,"$2,780.52")

Perhatikan bagaimana transaksi di atas gagal pada dana yang tidak mencukupi, yaitu akan menghasilkan saldo negatif dan berhasil dikembalikan. Perhatikan juga bahwa kami mengembalikan seluruh baris dengan variabel khusus BARU sebagai detail tambahan dalam pesan kesalahan untuk debugging.

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$278.52', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,721.48
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal', '$35.29', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

CONTOH 5 - Pemicu dan Redux Tampilan

Namun, ada masalah dengan implementasi di atas, yaitu tidak ada yang mencegah pengguna jahat mencetak uang:

BEGIN;
UPDATE person SET balance = '1000000000.00';

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

Kami telah menghentikan pencurian di atas untuk saat ini dan akan menunjukkan cara untuk membangun perlindungan terhadapnya dengan menggunakan pemicu pada tampilan untuk mencegah pembaruan nilai saldo.

Kami pertama-tama menambah tampilan ringkasan dari sebelumnya untuk mengekspos kolom saldo:

CREATE OR REPLACE VIEW abridged_person AS
  SELECT login_name, display_name, abstract, balance FROM person;

Ini jelas memungkinkan akses baca ke saldo, tetapi tetap tidak menyelesaikan masalah karena untuk tampilan sederhana seperti ini berdasarkan satu tabel, PostgreSQL secara otomatis membuat tampilan dapat ditulisi:

BEGIN;
UPDATE abridged_person SET balance = '1000000000.00';
SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

We could use a rule, but to illustrate that triggers can be defined on views as well as tables, we will take the latter route and use an instead of update trigger on the view to block unwanted DML, preventing non-transactional changes to the balance value:

CREATE FUNCTION abridged_person_iut() RETURNS TRIGGER
    LANGUAGE plpgsql
    SET search_path TO public
    AS $$
    BEGIN

    -- Disallow non-transactional changes to balance

      NEW.balance = OLD.balance;
    RETURN NEW;
    END;
    $$;

CREATE TRIGGER abridged_person_iut
    INSTEAD OF UPDATE ON abridged_person
    FOR EACH ROW EXECUTE PROCEDURE abridged_person_iut();

The above instead of update trigger and stored procedure discards any attempted updates to the balance value and instead forces use of the value present in the database prior to the triggering update statement:

UPDATE abridged_person SET balance = '1000000000.00';

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

which affords protection against un-auditable changes to the balance value.

Unduh Whitepaper Hari Ini Pengelolaan &Otomatisasi PostgreSQL dengan ClusterControlPelajari tentang apa yang perlu Anda ketahui untuk menerapkan, memantau, mengelola, dan menskalakan PostgreSQLUnduh Whitepaper

EXAMPLE 6 - Elevated Privileges

So far all the example code above has been executed at the database owner level by the postgres login role, so any of our anti-tampering efforts could be obviated… that’s just a fact of the database owner super-user privileges.

Our final example illustrates how triggers and stored functions can be used to allow the execution of code by a non-privileged user at a higher privilege than the logged in session user normally has by employing the SECURITY DEFINER attribute associated with stored functions.

First, we define a non-privileged login role, eve and confirm that upon instantiation there are no privileges:

CREATE USER eve;
\dp
                                  Access privileges
 Schema |      Name       | Type  | Access privileges | Column privileges | Policies 
--------+-----------------+-------+-------------------+-------------------+----------
 public | abridged_person | view  |                   |                   | 
 public | person          | table |                   |                   | 
 public | person_audit    | table |                   |                   | 
 public | transaction     | table |                   |                   | 
(4 rows)

We grant read, update, and create privileges on the abridged person view and read and create to the transaction table:

GRANT SELECT,INSERT, UPDATE ON abridged_person TO eve;
GRANT SELECT,INSERT ON transaction TO eve;
\dp
                                      Access privileges
 Schema |      Name       | Type  |     Access privileges     | Column privileges | Policies 
--------+-----------------+-------+---------------------------+-------------------+----------
 public | abridged_person | view  | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=arw/postgres          |                   | 
 public | person          | table |                           |                   | 
 public | person_audit    | table |                           |                   | 
 public | transaction     | table | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=ar/postgres           |                   | 
(4 rows)

By way of confirmation we see that eve is denied access to the person and person_audit tables:

SET SESSION AUTHORIZATION eve;

SELECT * FROM person;
ERROR:  permission denied for relation person

SELECT * from person_audit;
ERROR:  permission denied for relation person_audit

and that she does have appropriate read access to the abridged_person and transaction tables:

SELECT * FROM abridged_person;
 login_name |    display_name    |                                                            abstract                                                             |  balance  
------------+--------------------+---------------------------------------------------------------------------------------------------------------------------------+-----------
 skeeter    | Mosquito Valentine | Skeeter is Doug's best friend. He is famous in both series for the honking sounds he frequently makes.                          |     $0.00
 dfunny     | Doug Yancey Funny  | Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd. | $1,686.19
(2 rows)

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
(3 rows)

However, even though she has write privilege on the transaction table, a transaction insert attempt fails due to lack of privilege on the person table.

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
ERROR:  permission denied for relation person
CONTEXT:  SQL statement "UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name"
PL/pgSQL function transaction_bit() line 6 at SQL statement

The error message context shows this hold up occurs when inside the trigger function DML to update the balance is invoked. The way around this need to deny Eve direct write access to the person table but still effect updates to the person balance in a controlled manner is to add the SECURITY DEFINER attribute to the stored function:

RESET SESSION AUTHORIZATION;
ALTER FUNCTION transaction_bit() SECURITY DEFINER;

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
 dfunny     | 2018-01-23 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
(4 rows)

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $3,686.19
(1 row)

Now the transaction insert succeeds because the stored function is executed with privilege level of its definer, i.e., the postgres user, which does have the appropriate write privilege on the person table.

Kesimpulan

As lengthy as this article is, there’s still a lot more to say about triggers and stored functions. What we covered here is a basic introduction with a consideration of pros and cons of triggers and stored functions. We illustrated six use-case examples showing data validation, change logging, deriving values from inserted data, data hiding with simple updatable views, maintaining summary data in separate tables, and allowing safe invocation of code at elevated privilege. Look for a future article on using triggers and stored functions to prevent missing values in sequentially-incrementing (serial) columns.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Bagaimana Fungsi Radian() Bekerja di PostgreSQL

  2. Fungsi Escape untuk ekspresi reguler atau pola LIKE

  3. di postgres, dapatkah Anda mengatur pemformatan default untuk stempel waktu, berdasarkan sesi atau secara global?

  4. Sisipan multi-baris dengan pg-promise

  5. Pengaturan dan Penggunaan pgmemcache