PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

Kiến thức cơ bản về kích hoạt PostgreSQL và hàm được lưu trữ

Lưu ý từ Somenines:Blog này đang được đăng sau khi Berend Tober qua đời vào ngày 16 tháng 7 năm 2018. Chúng tôi tôn vinh những đóng góp của anh ấy cho cộng đồng PostgreSQL và cầu chúc bình an cho người bạn và người viết khách của chúng tôi.

Trong bài viết trước, chúng ta đã thảo luận về kiểu giả nối tiếp PostgreSQL, hữu ích để điền các giá trị khóa tổng hợp với các số nguyên tăng dần. Chúng tôi đã thấy rằng việc sử dụng từ khóa kiểu dữ liệu nối tiếp trong câu lệnh ngôn ngữ định nghĩa dữ liệu bảng (DDL) được triển khai dưới dạng khai báo cột kiểu số nguyên được điền vào, dựa trên chèn cơ sở dữ liệu, với giá trị mặc định bắt nguồn từ một lệnh gọi hàm đơn giản. Hành vi tự động gọi mã chức năng này như một phần của phản ứng tích hợp đối với hoạt động của ngôn ngữ thao tác dữ liệu (DML) là một tính năng mạnh mẽ của các hệ thống quản lý cơ sở dữ liệu quan hệ phức tạp (RDBMS) như PostgreSQL. Trong bài viết này, chúng tôi đi sâu hơn vào một khía cạnh khác có khả năng hơn để tự động gọi mã tùy chỉnh, cụ thể là việc sử dụng trình kích hoạt và các hàm được lưu trữ.

Các trường hợp sử dụng cho trình kích hoạt và hàm được lưu trữ

Hãy nói về lý do tại sao bạn có thể muốn đầu tư vào việc hiểu các trình kích hoạt và các chức năng được lưu trữ. Bằng cách xây dựng mã DML vào chính cơ sở dữ liệu, bạn có thể tránh triển khai trùng lặp mã liên quan đến dữ liệu trong nhiều ứng dụng riêng biệt có thể được xây dựng để giao diện với cơ sở dữ liệu. Điều này đảm bảo thực thi nhất quán mã DML để xác thực dữ liệu, làm sạch dữ liệu hoặc các chức năng khác như kiểm tra dữ liệu (tức là ghi nhật ký thay đổi) hoặc duy trì bảng tóm tắt độc lập với bất kỳ ứng dụng gọi điện nào. Một cách sử dụng phổ biến khác của trình kích hoạt và các chức năng được lưu trữ là làm cho các chế độ xem có thể ghi, tức là cho phép chèn và / hoặc cập nhật trên các chế độ xem phức tạp hoặc để bảo vệ dữ liệu cột nhất định khỏi sửa đổi trái phép. Ngoài ra, dữ liệu được xử lý trên máy chủ chứ không phải trong mã ứng dụng không qua mạng, do đó, ít có nguy cơ dữ liệu bị nghe trộm hơn cũng như giảm tắc nghẽn mạng. Ngoài ra, trong PostgreSQL, các hàm được lưu trữ có thể được cấu hình để thực thi mã ở mức đặc quyền cao hơn người dùng phiên, điều này thừa nhận một số khả năng mạnh mẽ. Chúng tôi sẽ làm một số ví dụ sau.

Trường hợp chống lại các trình kích hoạt và các chức năng được lưu trữ

Việc xem xét bình luận trên danh sách gửi thư chung của PostgreSQL đã tiết lộ một số ý kiến ​​không thuận lợi đối với việc sử dụng các trình kích hoạt và các hàm được lưu trữ mà tôi đề cập ở đây để hoàn thiện và khuyến khích bạn và nhóm của bạn cân nhắc những ưu và khuyết điểm cho việc triển khai của bạn.

Trong số các ý kiến ​​phản đối, ví dụ, nhận thức rằng các chức năng được lưu trữ không dễ duy trì, do đó đòi hỏi một người có kinh nghiệm với các kỹ năng và kiến ​​thức tinh vi về quản trị cơ sở dữ liệu để quản lý chúng. Một số chuyên gia phần mềm đã báo cáo rằng các kiểm soát thay đổi của công ty trên hệ thống cơ sở dữ liệu thường mạnh mẽ hơn so với mã ứng dụng, do đó nếu các quy tắc nghiệp vụ hoặc logic khác được thực hiện trong cơ sở dữ liệu, thì việc thực hiện các thay đổi khi các yêu cầu phát triển là cực kỳ phức tạp. Một quan điểm khác coi kích hoạt là một tác dụng phụ không mong muốn của một số hành động khác và như vậy, có thể khó hiểu, dễ bị bỏ sót, khó gỡ lỗi và khó bảo trì và do đó thường nên là lựa chọn cuối cùng chứ không phải lựa chọn đầu tiên.

Những phản đối này có thể có một phần xứng đáng, nhưng nếu bạn nghĩ về nó, dữ liệu là một tài sản có giá trị và vì vậy, bạn có thể thực sự muốn một người hoặc nhóm có kỹ năng và kinh nghiệm chịu trách nhiệm về RDBMS trong một công ty hoặc tổ chức chính phủ, và tương tự, Thay đổi Ban kiểm soát là một thành phần đã được chứng minh về khả năng bảo trì bền vững cho hệ thống hồ sơ thông tin và tác dụng phụ của một người cũng như sự tiện lợi mạnh mẽ của người khác, đó là quan điểm được áp dụng cho sự cân bằng của bài viết này.

Khai báo trình kích hoạt

Hãy bắt đầu tìm hiểu về các đai ốc và bu lông. Có nhiều tùy chọn có sẵn trong cú pháp DDL chung để khai báo trình kích hoạt và sẽ mất một khoảng thời gian đáng kể để xử lý tất cả các hoán vị có thể xảy ra, vì vậy, để ngắn gọn, chúng tôi sẽ chỉ nói về một tập hợp con được yêu cầu tối thiểu trong các ví dụ đó làm theo cách sử dụng cú pháp rút gọn này:

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

Các phần tử có thể định cấu hình bắt buộc ngoài tên khi nào , tại sao , ở đâu cái gì , tức là thời gian để mã kích hoạt được gọi liên quan đến hành động kích hoạt (khi nào), loại cụ thể của câu lệnh DML kích hoạt (tại sao), bảng được thực thi hoặc các bảng (ở đâu) và mã hàm được lưu trữ để thực thi (cái gì).

Khai báo một hàm

Khai báo trình kích hoạt ở trên yêu cầu đặc tả tên hàm, vì vậy về mặt kỹ thuật, khai báo kích hoạt DDL không thể được thực thi cho đến sau khi chức năng kích hoạt đã được xác định trước đó. Cú pháp DDL chung cho một khai báo hàm cũng có nhiều tùy chọn vì vậy để có thể quản lý, chúng tôi sẽ sử dụng cú pháp đủ tối thiểu này cho các mục đích của chúng tôi tại đây:

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

Một hàm kích hoạt không có tham số và kiểu trả về phải là TRIGGER. Chúng ta sẽ nói về các công cụ sửa đổi tùy chọn khi chúng ta gặp chúng trong các ví dụ bên dưới.

Lược đồ đặt tên cho các trình kích hoạt và chức năng

Nhà khoa học máy tính đáng kính Phil Karlton được cho là đã tuyên bố (ở dạng diễn giải ở đây) rằng việc đặt tên cho mọi thứ là một trong những thách thức lớn nhất đối với các nhóm phần mềm. Tôi sẽ trình bày ở đây một trình kích hoạt dễ sử dụng và quy ước đặt tên hàm được lưu trữ đã phục vụ tốt cho tôi và khuyến khích bạn xem xét áp dụng nó cho các dự án RDBMS của riêng bạn. Lược đồ đặt tên trong các ví dụ cho bài viết này tuân theo một mẫu sử dụng tên bảng được liên kết kèm theo chữ viết tắt cho biết trình kích hoạt được khai báo khi tại sao thuộc tính:Chữ cái có hậu tố đầu tiên sẽ là “b”, “a” hoặc “i” (cho “trước”, “sau” hoặc “thay vì”), tiếp theo sẽ là một hoặc nhiều chữ “i” , “U”, “d”, hoặc “t” (đối với “insert”, “update”, “delete” hoặc “truncate”) và chữ cái cuối cùng chỉ là “t” cho trigger. (Tôi sử dụng một quy ước đặt tên tương tự cho các quy tắc, và trong trường hợp đó, chữ cái cuối cùng là “r”). Vì vậy, ví dụ:các tổ hợp thuộc tính khai báo kích hoạt tối thiểu khác nhau cho bảng có tên “my_table” sẽ là:

|-------------+-------------+-----------+---------------+-----------------|
|  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   |
|-------------+-------------+-----------+---------------+-----------------|

Tên chính xác giống nhau có thể được sử dụng cho cả trình kích hoạt và chức năng được lưu trữ liên quan, điều này hoàn toàn được phép trong PostgreSQL vì RDBMS theo dõi các trình kích hoạt và các chức năng được lưu trữ riêng biệt theo các mục đích tương ứng và bối cảnh mà tên mục được sử dụng xóa mục mà tên đề cập đến.

Vì vậy, ví dụ:một khai báo trình kích hoạt tương ứng với kịch bản hàng đầu tiên từ bảng trên sẽ được xem như là

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

Trong trường hợp khi một trình kích hoạt được khai báo với nhiều tại sao các thuộc tính, chỉ cần mở rộng hậu tố một cách thích hợp, ví dụ:cho một chèn hoặc cập nhật kích hoạt, ở trên sẽ trở thành

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

Hiện cho tôi một số mã!

Hãy biến nó thành sự thật. Chúng tôi sẽ bắt đầu với một ví dụ đơn giản và sau đó mở rộng thêm để minh họa các tính năng khác. Các câu lệnh DDL kích hoạt yêu cầu một chức năng tồn tại từ trước, như đã đề cập, và cả một bảng để hoạt động, vì vậy trước tiên chúng ta cần một bảng để làm việc trên đó. Đối với các mục đích ví dụ, giả sử chúng tôi cần lưu trữ dữ liệu nhận dạng tài khoản cơ bản

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

Một số thực thi toàn vẹn dữ liệu có thể được xử lý đơn giản với cột DDL thích hợp, chẳng hạn như trong trường hợp này, yêu cầu tên đăng nhập tồn tại và không quá chín ký tự. Không thể chèn giá trị NULL hoặc giá trị quá dài của login_name và báo cáo thông báo lỗi có ý nghĩa:

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)

Các biện pháp thực thi khác có thể được xử lý bằng các ràng buộc kiểm tra, chẳng hạn như yêu cầu độ dài tối thiểu và từ chối các ký tự nhất định:

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).

nhưng lưu ý rằng thông báo lỗi không đầy đủ thông tin như trước, chỉ truyền tải nhiều như được mã hóa trong tên trình kích hoạt hơn là một thông báo văn bản giải thích có ý nghĩa. Thay vào đó, bằng cách triển khai logic kiểm tra trong một hàm được lưu trữ, bạn có thể sử dụng một ngoại lệ để gửi một tin nhắn văn bản hữu ích hơn. Ngoài ra, các biểu thức ràng buộc kiểm tra không được chứa các truy vấn con hoặc tham chiếu đến các biến khác ngoài các cột của hàng hiện tại cũng như các bảng cơ sở dữ liệu khác.

Vì vậy, hãy bỏ các ràng buộc kiểm tra

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

và tiếp tục với các trình kích hoạt và các chức năng được lưu trữ.

Cho tôi xem thêm một số mã

Chúng tôi có một cái bàn. Chuyển sang hàm DDL, chúng ta xác định một hàm có thân trống, chúng ta có thể điền vào sau bằng mã cụ thể:

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

Điều này cho phép chúng tôi cuối cùng truy cập DDL kích hoạt kết nối bảng và hàm để chúng tôi có thể làm một số ví dụ:

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

PostgreSQL cho phép các hàm được lưu trữ được viết bằng nhiều ngôn ngữ khác nhau. Trong trường hợp này và các ví dụ sau, chúng tôi đang soạn các hàm bằng ngôn ngữ PL / pgSQL được thiết kế đặc biệt cho PostgreSQL và hỗ trợ việc sử dụng tất cả các kiểu dữ liệu, toán tử và chức năng của PostgreSQL RDBMS. Tùy chọn SET SCHEMA đặt đường dẫn tìm kiếm lược đồ sẽ được sử dụng trong suốt thời gian thực thi chức năng. Đặt đường dẫn tìm kiếm cho mọi chức năng là một phương pháp hay, vì nó giúp tiết kiệm việc phải thêm tiền tố cho các đối tượng cơ sở dữ liệu bằng tên lược đồ và bảo vệ khỏi một số lỗ hổng nhất định liên quan đến đường dẫn tìm kiếm.

VÍ DỤ 0 - Xác thực dữ liệu

Ví dụ đầu tiên, hãy triển khai các bước kiểm tra trước đó, nhưng với thông điệp thân thiện với con người hơn.

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;
    $$;

Bộ định lượng "MỚI" là tham chiếu đến hàng dữ liệu sắp được chèn. Nó là một trong số các biến đặc biệt có sẵn trong một hàm kích hoạt. Chúng tôi sẽ giới thiệu một số người khác bên dưới. Cũng lưu ý, PostgreSQL cho phép thay thế các dấu ngoặc kép đơn phân tách thân hàm bằng các dấu phân cách khác, trong trường hợp này tuân theo quy ước chung là sử dụng các dấu đô la kép làm dấu phân cách, vì bản thân hàm bao gồm các ký tự ngoặc kép. Các chức năng kích hoạt phải thoát bằng cách trả về hàng MỚI được chèn hoặc NULL để hủy bỏ tác vụ một cách âm thầm.

Các nỗ lực chèn tương tự không thành công như mong đợi, nhưng giờ đây với tính năng nhắn tin thân thiện:

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.

VÍ DỤ 1 - Ghi nhật ký kiểm tra

Với các hàm được lưu trữ, chúng ta có phạm vi rộng về những gì mã được gọi thực hiện, bao gồm cả việc tham chiếu đến các bảng khác (điều này không thể thực hiện được với các ràng buộc kiểm tra). Như một ví dụ phức tạp hơn, chúng ta sẽ hướng dẫn cách triển khai bảng kiểm tra, tức là duy trì một bản ghi, trong một bảng riêng, về các lần chèn, cập nhật và xóa đối với một bảng chính. Bảng kiểm tra thường chứa các thuộc tính giống như bảng chính, được sử dụng để ghi lại các giá trị đã thay đổi, cộng với các thuộc tính bổ sung để ghi lại thao tác được thực hiện để thực hiện thay đổi, cũng như dấu thời gian giao dịch và bản ghi người dùng thực hiện thay đổi:

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
);

Trong trường hợp này, việc triển khai kiểm tra rất dễ dàng, chúng tôi chỉ cần sửa đổi chức năng kích hoạt hiện có để bao gồm DML để thực hiện chèn bảng kiểm tra, sau đó xác định lại trình kích hoạt để kích hoạt trên các bản cập nhật cũng như các lần chèn. Lưu ý rằng chúng tôi đã chọn không thay đổi hậu tố tên hàm kích hoạt thành “biut”, nhưng nếu chức năng kiểm tra là một yêu cầu đã biết tại thời điểm thiết kế ban đầu, thì đó sẽ là tên được sử dụng:

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();

Lưu ý rằng chúng tôi đã giới thiệu một biến đặc biệt khác “TG_OP” mà hệ thống đặt để xác định hoạt động DML đã kích hoạt trình kích hoạt tương ứng là “INSERT”, “UPDATE”, “DELETE”, của “TRUNCATE”.

Chúng tôi cần xử lý các lần xóa riêng biệt với các lần chèn và cập nhật vì các bài kiểm tra xác thực thuộc tính là không cần thiết và vì giá trị đặc biệt MỚI không được xác định khi nhập vào trước khi xóa chức năng kích hoạt và do đó xác định chức năng và trình kích hoạt được lưu trữ tương ứng:

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();

Lưu ý việc sử dụng giá trị đặc biệt CŨ làm tham chiếu đến hàng sắp bị xóa, tức là hàng tồn tại trước đó việc xóa xảy ra.

Chúng tôi thực hiện một số lần chèn để kiểm tra chức năng và xác nhận rằng bảng kiểm tra bao gồm bản ghi các lần chèn:

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)

Sau đó, chúng tôi thực hiện cập nhật cho một hàng và xác nhận rằng bảng kiểm tra bao gồm bản ghi về việc thay đổi thêm tên đệm vào một trong các tên hiển thị của bản ghi dữ liệu:

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)

Và cuối cùng, chúng tôi thực hiện chức năng xóa và xác nhận rằng bảng kiểm tra cũng bao gồm bản ghi đó:

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)

VÍ DỤ 2 - Giá trị bắt nguồn

Hãy tiến thêm một bước nữa và hãy tưởng tượng rằng chúng ta muốn lưu trữ một số tài liệu văn bản dạng tự do trong mỗi hàng, chẳng hạn như sơ yếu lý lịch được định dạng văn bản thuần túy hoặc báo cáo hội nghị hoặc bản tóm tắt nhân vật giải trí và chúng tôi muốn hỗ trợ việc sử dụng tính năng tìm kiếm toàn văn bản mạnh mẽ khả năng của PostgreSQL trên các tài liệu văn bản dạng tự do này.

Đầu tiên, chúng tôi thêm hai thuộc tính để hỗ trợ lưu trữ tài liệu và một vectơ tìm kiếm văn bản được liên kết vào bảng chính. Vì vectơ tìm kiếm văn bản được dẫn xuất trên cơ sở mỗi hàng, nên không có ích lợi gì khi lưu trữ nó trong bảng kiểm tra, có thể là chúng tôi thêm cột lưu trữ tài liệu vào bảng kiểm tra được liên kết:

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

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Sau đó, chúng tôi sửa đổi chức năng kích hoạt để xử lý các thuộc tính mới này. Cột văn bản thuần túy được xử lý theo cách giống như dữ liệu do người dùng nhập khác, nhưng vectơ tìm kiếm văn bản là một giá trị dẫn xuất và do đó được xử lý bởi một lệnh gọi hàm làm giảm văn bản tài liệu thành kiểu dữ liệu tsvector để tìm kiếm hiệu quả.

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;
    $$;

Để kiểm tra, chúng tôi cập nhật một hàng hiện có với một số văn bản chi tiết từ 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';

và sau đó xác nhận rằng quá trình xử lý vectơ tìm kiếm văn bản đã thành công:

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)

VÍ DỤ 3 - Trình kích hoạt &Lượt xem

Vectơ tìm kiếm văn bản dẫn xuất từ ​​ví dụ trên không dành cho con người, tức là nó không phải do người dùng nhập và chúng tôi không bao giờ mong đợi hiển thị giá trị cho người dùng cuối. Nếu người dùng cố gắng chèn một giá trị cho cột ts_abstract, bất kỳ thứ gì được cung cấp sẽ bị loại bỏ và thay thế bằng giá trị được dẫn xuất bên trong hàm kích hoạt, vì vậy chúng tôi có biện pháp bảo vệ chống đầu độc kho dữ liệu tìm kiếm. Để ẩn hoàn toàn cột, chúng tôi có thể xác định một chế độ xem tóm tắt không bao gồm thuộc tính đó, nhưng chúng tôi vẫn nhận được lợi ích của hoạt động kích hoạt trên bảng bên dưới:

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

Đối với một cái nhìn đơn giản, PostgreSQL tự động làm cho nó có thể ghi, vì vậy chúng tôi không phải làm bất cứ điều gì khác để chèn hoặc cập nhật dữ liệu thành công. Khi DML có hiệu lực trên bảng bên dưới, trình kích hoạt sẽ kích hoạt như thể câu lệnh được áp dụng trực tiếp vào bảng, vì vậy chúng tôi vẫn nhận được cả hỗ trợ tìm kiếm văn bản được thực thi trong nền điền vào cột vectơ tìm kiếm của bảng người cũng như thêm vào thay đổi thông tin vào bảng kiểm toán:

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)

Đối với các chế độ xem phức tạp hơn không đáp ứng các yêu cầu để tự động có thể ghi, hệ thống quy tắc hoặc thay vì trình kích hoạt có thể thực hiện công việc hỗ trợ ghi và xóa.

VÍ DỤ 4 - Giá trị Tóm tắt

Chúng ta hãy cải tạo thêm và xử lý tình huống có một số loại bảng giao dịch. Nó có thể là một bản ghi về số giờ làm việc, bổ sung và giảm hàng tồn kho hoặc kho bán lẻ, hoặc có thể là một sổ đăng ký séc với các khoản ghi nợ và ghi có cho mỗi người:

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)
);

Và giả sử rằng mặc dù điều quan trọng là phải giữ lại lịch sử giao dịch, nhưng các quy tắc kinh doanh đòi hỏi phải sử dụng số dư ròng trong quá trình xử lý đơn đăng ký chứ không phải bất kỳ chi tiết giao dịch nào. Để tránh phải thường xuyên tính toán lại số dư bằng cách tổng hợp tất cả các giao dịch mỗi khi cần số dư, chúng tôi có thể chuẩn hóa lại và giữ một giá trị số dư hiện tại ngay tại đó trong bảng người bằng cách thêm một cột mới và sử dụng trình kích hoạt và chức năng được lưu trữ để duy trì số dư ròng khi các giao dịch được chèn vào:

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();

Có vẻ kỳ lạ khi thực hiện cập nhật trước trong hàm được lưu trữ trước khi xác thực tính không âm của các giá trị ghi nợ, ghi có và số dư, nhưng về mặt xác thực dữ liệu, thứ tự không quan trọng vì phần thân của hàm kích hoạt được thực thi dưới dạng giao dịch cơ sở dữ liệu, vì vậy nếu những kiểm tra xác thực đó không thành công, thì toàn bộ giao dịch sẽ được khôi phục khi ngoại lệ được nâng lên. Ưu điểm của việc cập nhật trước là bản cập nhật khóa hàng bị ảnh hưởng trong suốt thời gian của giao dịch và do đó, bất kỳ phiên nào khác cố gắng cập nhật cùng một hàng sẽ bị chặn cho đến khi giao dịch hiện tại hoàn tất. Kiểm tra xác thực thêm đảm bảo rằng số dư kết quả là không âm và thông báo thông tin ngoại lệ có thể bao gồm một biến, trong trường hợp này sẽ trả về hàng giao dịch chèn vi phạm để gỡ lỗi.

Để chứng minh rằng nó thực sự hoạt động, đây là một vài mục nhập mẫu và một séc hiển thị số dư cập nhật ở mỗi bước:

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")

Lưu ý rằng giao dịch trên không thành công như thế nào khi không đủ tiền, tức là nó sẽ tạo ra số dư âm và hoàn trả thành công. Cũng lưu ý rằng chúng tôi đã trả lại toàn bộ hàng với biến đặc biệt MỚI làm chi tiết bổ sung trong thông báo lỗi để gỡ lỗi.

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)

VÍ DỤ 5 - Trình kích hoạt và Chế độ xem Redux

Tuy nhiên, có một vấn đề với việc triển khai ở trên và đó là không có gì ngăn cản người dùng độc hại in tiền:

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;

Chúng tôi đã ngăn chặn hành vi trộm cắp ở trên ngay bây giờ và sẽ chỉ ra một cách để xây dựng biện pháp bảo vệ chống lại bằng cách sử dụng trình kích hoạt trên một chế độ xem để ngăn cập nhật giá trị số dư.

Đầu tiên, chúng tôi tăng cường chế độ xem rút gọn từ trước đó để hiển thị cột số dư:

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

Điều này rõ ràng cho phép truy cập đọc vào số dư, nhưng nó vẫn không giải quyết được vấn đề vì đối với các chế độ xem đơn giản như thế này dựa trên một bảng duy nhất, PostgreSQL tự động làm cho chế độ xem có thể ghi:

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.

Tải xuống Báo cáo chính thức hôm nay Quản lý &Tự động hóa PostgreSQL với ClusterControlTìm hiểu về những điều bạn cần biết để triển khai, giám sát, quản lý và mở rộng PostgreSQLTải xuống Báo cáo chính thức

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.

Kết luận

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. PostgreSQL:Làm thế nào để truyền các tham số từ dòng lệnh?

  2. Thiết lập đa trung tâm dữ liệu với PostgreSQL

  3. PostgreSQL:Truy vấn không có đích cho dữ liệu kết quả

  4. Các tùy chọn khôi phục thảm họa cho PostgreSQL được triển khai cho một đám mây lai

  5. PostgreSQL, truy vấn phức tạp để tính toán các thành phần theo công thức