Thứ Sáu, 17 tháng 1, 2014

PostgreSQL : Giới thiệu hàm viết bằng ngôn ngữ thủ tục PL/pgSQL

1. Giới thiệu

Với PL/pgSQL ta có thể dùng tất cả các kiểu dữ liệu, các toán tử và các hàm của SQL.
Ngoài ra, PL/pgSQL còn cho phép ta định nghĩa các hàm động (hàm liên quan đến các cột khác nhau, các bảng khác nhau mỗi khi ta thực thi nó), thực thi các lệnh điều kiện (IF, ELSE, CASE), các vòng lặp (LOOP, WHILE, FOR, FOREACH), xử lý các ngoại lệ và lỗi (EXCEPTION, RAISE), định nghĩa TRIGGER, con trỏ (CURSOR, FETCH, MOVE)  ...
Kể từ PostgreSQL 9.0, PL/pgSQL được cài mặc định.

Định nghĩa một hàm đơn giản viết bằng PL/pgSQL có dạng sau :
CREATE FUNCTION function_name (argument_name argument_type, argument_name argument_type, ...) 
RETURNS return_type AS $$
[ DECLARE
    declarations ]
BEGIN
    statements
END;
$$ language plpgsql;

Định nghĩa thân hàm phải là một lốc DECLARE-BEGIN-END. Trong đó, mỗi khai báo (declaration) và mỗi dòng lệnh (statement) kết thúc bằng dấu chấm phẩy.

Trong trường hợp phức tạp, lốc gốc này có thể có nhiều lốc con DECLARE-BEGIN-END, khi đó mỗi lốc con này kết thúc bằng END và dấu chấm phẩy theo sau.

Chú ý không nên nhầm lẫn : BEGIN/END của PL/pgSQL nhằm nhóm các lệnh vào một lốc. Còn BEGIN/END của SQL nhằm khởi đầu và kết thúc một giao dịch (transaction).

2. Ví dụ

Ví dụ 1
Ví dụ sau định nghĩa một hàm động cho phép lấy một giá trị ngẫu nhiên trong một cột của một bảng
CREATE OR REPLACE FUNCTION get_field(field_name varchar, table_name varchar, numrow int)
RETURNS SETOF varchar AS
$BODY$
BEGIN
    RETURN QUERY EXECUTE 'SELECT ' || $1 || ' from ' || $2 ||' ORDER BY random()*' || $3 || ' LIMIT 1';
END;
$BODY$
LANGUAGE plpgsql VOLATILE;

SELECT get_field('name','mobile',10);

Giải thích :
- Ở đây ta không cần dùng biến nên không có đoạn DECLARE.
- Câu truy vấn SQL được phát sinh động bằng cách nối chuỗi dùng các tham số là tên cột, tên bảng và một số. "ORDER BY random()*số LIMIT 1" cho phép lấy dòng đầu tiên của kết quả trả về với thứ tự ngẫu nhiên.
- Lệnh EXECUTE cho phép thực thi câu truy vấn SQL động định nghĩa bằng một chuỗi.
- RETURN QUERY trả về kết quả của một câu truy vấn (chứ không chỉ là một giá trị đơn giản) đó là lý do tại sao trong khai báo hàm ta dùng RETURNS SETOF.
- Ở đây ta dùng dấu nháy ' để định nghĩa các chuỗi, trong trường hợp chuỗi bao gồm nhiều dấu nháy ta có thể dùng bởi dấu ngoặc đô-la để bọc chuỗi để câu truy vấn dễ hiểu hơn.
- Câu gọi lệnh ở cuối trả về giá trị ngẫu nhiên của cột name trong bảng mobile.

Ví dụ2
Viết lại hàm trên để trả về một giá trị đơn giản
DROP FUNCTION IF EXISTS get_field(varchar, varchar, int);
CREATE OR REPLACE FUNCTION get_field(field_name varchar, table_name varchar, numrow int)
RETURNS varchar AS
$BODY$
DECLARE
    field varchar;
BEGIN
    EXECUTE 'SELECT ' || $1 || ' from ' || $2 ||' ORDER BY random()*' || $3 || ' LIMIT 1' 
    INTO field;
    RETURN field;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
SELECT get_field('name','mobile',10);

Giải thích :
- Hàm này thực hiện cùng một chức năng như hàm trong ví dụ 1.
- Ta định nghĩa một biến field (trong đoạn DECLARE), thực thi câu truy vấn động và gán giá trị trả về vào biến này (EXCECUTE ... INTO ...) . Cuối cùng trả về kết quả này cho hàm.
- Hàm này trả về một giá trị đơn giản nên trong định nghĩa hàm ta không dùng SETOF.


Ví dụ3
Ta có bảng customer như sau :
            Table "public.customer"
   Column   |         Type          | Modifiers 
------------+-----------------------+-----------
 id         | integer               | not null
 address    | character varying(64) | 
 first_name | character varying(32) | 
 last_name  | character varying(32) | 
 email      | character varying(64) | 
Indexes:
    "customer_pkey" PRIMARY KEY, btree (id)

Ta muốn tạo một bảng temp_customer có cấu trúc giống hệt bảng customer; và bảng này có dữ liệu là dữ liệu lấy ngẫu nhiên từ bảng customer
DROP TABLE IF EXISTS temp_customer;
CREATE TABLE temp_customer
(
    id integer NOT NULL,
    address character varying(64),
    first_name character varying(32),
    last_name character varying(32),
    email character varying(64),
    CONSTRAINT temp_customer_pkey PRIMARY KEY (id)
);

Để điền tự động thông tin vào bảng temp_customer ta định nghĩa hàm sau :
CREATE OR REPLACE FUNCTION fill_temp_customer()
RETURNS VOID AS
$BODY$
DECLARE
    num_row int;
BEGIN
    SELECT COUNT(*) INTO num_row FROM customer;
    FOR i IN 1..1000 
    LOOP
        INSERT INTO temp_customer(id, address, first_name, last_name, email)
            values(i, get_field('address', 'customer', num_row), 
            get_field('first_name', 'customer', num_row), get_field('last_name', 'customer', num_row),
            concat('customer_', i, '@openspace.vn'));
        EXIT WHEN i > num_row;
    END LOOP;
    SELECT COUNT(*) INTO num_row FROM temp_customer;
    IF num_row = 0 THEN
        INSERT INTO temp_customer(id, address, first_name, last_name, email)
            values(1, 'address 1', 'first_name 1', 'last_name 1', 'customer_1@openspace.vn');
        INSERT INTO temp_customer(id, address, first_name, last_name, email)
            values(2, 'address 2', 'first_name 2', 'last_name 2', 'customer_2@openspace.vn');
    END IF;
    RETURN;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
SELECT fill_temp_customer();

Giải thích :

- Hàm định nghĩa biến  num_row.
- Hàm này nhằm điền dữ liệu vào bảng temp_customer và không trả về giá trị nên khai báo hàm dùng RETURNS VOID.
- Câu lệnh SELECT INTO đầu tiên nhằm đếm tổng số dòng của bảng customer. Chú ý lệnh SELECT INTO của PL/PgSQL này (nhằm gán kết quả trả về vào một biến) hoàn toàn khác lệnh SELECT INTO của PostgreSQL (nhằm tạo ra một bảng mới từ kết quả của một câu truy vấn).
- Vòng lặp FOR từ 1 đến 1000 nhằm chèn thông tin vào bảng temp_customer, với giá trị của address, first_name, last_name là giá trị tương ứng lấy ngẫu nhiên từ bảng customer bằng cách dùng hàm get_field định nghĩa trong ví dụ 1.
- Lệnh EXIT nhằm kết thúc vòng lặp trong trường hợp tổng số dòng trong bảng customer ít hơn 1000.
- Câu lệnh SELECT INTO tiếp theo nhằm đếm tổng số dòng của bảng temp_customer.
- Trong trường hợp bảng customer không có dữ liệu, dẫn đến bảng temp_customer cũng không có dữ liệu, lệnh IF tiếp theo cho phép chèn hai dòng dữ liệu vào bảng temp_customer.

Thứ Tư, 8 tháng 1, 2014

PostgreSQL : Giới thiệu hàm viết bằng ngôn ngữ truy vấn SQL

1. Hàm do người dùng định nghĩa

PostgreSQL cung cấp bốn loại hàm sau cho người dùng định nghĩa
- Hàm viết bằng ngôn ngữ truy vấn (query language functions : hàm viết bằng SQL)
- Hàm viết bằng ngôn ngữ thủ tục (procedural language functions : viết bằng PL/pgSQL, PL/Tcl, PL/Perl, PL/Python chẳng hạn)
- Hàm cung cấp sẵn (internal functions : là các hàm được viết bằng C được cung cấp sẵn trong server PostgreSQL)
- Hàm viết bằng ngôn ngữ C (C-language functions)

Ở đây tôi giới thiệu sơ về hàm viết bằng SQL

2. Giới thiệu hàm viết bằng ngôn ngữ truy vấn

Một hàm viết bằng SQL cho phép thực thi các câu truy vấn SQL định nghĩa trong hàm, và trả về kết quả của câu lệnh cuối cùng. Trường hợp đơn giản, hàm trả về dòng đầu tiên của kết quả câu truy vấn cuối cùng, nếu câu truy vấn này không trả về dòng kết quả nào thì hàm trả về giá trị null.

Muốn hàm trả về toàn bộ các dòng của câu truy vấn cuối, ta phải định nghĩa SETOF return_type hoặc TABLE(columns) trong đoạn RETURNS

Trong thân hàm, các câu lệnh phải được phân cách bằng dấu chấm phẩy, và không được sử dụng các lệnh transaction (như COMMIT, SAVEPOINT, ...). Một định nghĩa hàm đơn giản có cấu trúc như sau :
CREATE FUNCTION function_name (argument_name argument_type, argument_name argument_type, ...) 
RETURNS return_type AS $$
     function_body_query;
$$ language sql;

Thân hàm được viết dưới dạng hằng chuỗi (string constant), thông thường ta sử dụng dấu ngoặc đô-la (dollar quoting : $) để bao hằng chuỗi này. Dấu ngoặc đô-la không phải là một chuẩn của SQL.

Một hằng chuỗi bao bởi dấu ngoặc đô-la có dạng sau $[tag]$<string constant>$[tag]$ tức dấu đô-la + một tag (có thể có hoặc không) bao gồm không hay nhiều ký tự + dấu đô-la + hằng chuỗi + dấu đô-la + cùng tag vừa định nghĩa + dấu đô-la. Ví dụ cụ thể :
$$Toan's string constant$$
$BODY$Toan's string constant$BODY$

3. Ví dụ đơn giản

Ví dụ  1
Trong ví dụ sau, ta định nghĩa hàm phát sinh ngẫu nhiên một chuỗi các số
CREATE OR REPLACE FUNCTION random_numero(amount int)
RETURNS TEXT AS
$BODY$
SELECT array_to_string(
    ARRAY(
        SELECT chr((48 + round(random() * 9)) :: integer) 
        FROM generate_series(1,$1)
    ), '');
$BODY$
LANGUAGE sql VOLATILE;

Giải thích :
- Hàm có tên là random_numero, nhận một tham số kiểu int
- Hàm trả về kết quả là một chuỗi kiểu TEXT
- Trong ngoặc đô-la $BODY$ là thân hàm
- Thân hàm là một câu truy vấn SQL select trả về một chuỗi ngẫu nhiên gồm n số, với n là tham số amount hàm nhận vào. Chú ý, hàm không nhận biết tên tham số chuyển vào : Các tham số được sử dụng trong hàm dưới dạng $n; $1 trỏ đến tham số đầu tiên, $2 đến tham số thứ hai, vân vân.
- Với tham số truyền vào chỉ có kiểu (type) là bắt buộc, do đó hàm trên có thể được định nghĩa random_numero(int). Nhưng ta nên định nghĩa hàm với tên tham số cho dễ hiểu
- Trong trường hợp nếu tham số chuyển vào có dạng phức thì ta dùng đến toán tử dấu chấm để truy xuất đến thuộc tính của biến truyền vào. VD : $1.name
- Trong câu truy vấn này ta sử dụng hàm định nghĩa sẵn array_to_string để chuyển một mảng thành một chuỗi. Sử dụng hàm generate_series ở đây chủ yếu để phát sinh vòng lặp từ 1 đến n với n là giá trị của biến $1 truyền vào. Với mỗi vòng lặp này, ta phát sinh một số ngẫu nhiên trong khoảng từ 0 đến 9 để tạo thành một phần tử của mảng. :: integer dùng để ép kiểu biểu thức chr((48 + round(random() * 9)) về số nguyên. round(random() * 9) trả về giá trị từ 0 đến 9
- Cuối cùng "LANGUAGE sql VOLATILE" định nghĩa hàm này được viết bằng ngôn ngữ truy vấn SQL, volatile là giá trị mặc định của một phân loại (volatility classification) dùng để tối ưu hàm (hay không).

Gọi hàm này như sau : SELECT random_numero(5);
openspace_vn_db=# SELECT random_numero(5);
 random_numero 
---------------
 08182
(1 row)

Ví dụ 2
Định nghĩa hàm cộng hai số với tham số trả về (ouput parameter)
CREATE OR REPLACE FUNCTION add_two (IN a int, IN b int, OUT sum int)
AS 'SELECT $1 + $2'
LANGUAGE SQL;

SELECT add_two(4,58);
 add_two 
---------
      62
(1 row) 

Giải thích :
- Hàm này nhận vào hai tham số và trả về tổng của hai giá trị này
- Tiện ích của cách định nghĩa này là ta có thể trả về nhiều giá trị, chẳng hạn
CREATE FUNCTION sum_n_product (a int, b int, OUT sum int, OUT product int)
AS 'SELECT $1 + $2, $1 * $2'
LANGUAGE SQL;
- Chú ý khi gọi hàm ta chỉ truyền vào các tham số in (tức chỉ 2 tham số a và b)
- Các tham số có thể được đánh dấu là IN, OUT, INOUT hay VARIADIC.
- Dấu chấm phẩy ở câu lệnh cuối của một hàm là tuỳ ý tức có cũng được hay không có cũng được. Do vậy, ở đây tuy không có dấu chấm phẩy ở cuối nhưng hàm vẫn hợp lệ.

Ví dụ3
Nếu hàm của bạn chỉ thực hiện các việc như UPDATE hoặc DELETE và không trả về giá trị gì, bạn có thể định nghĩa hàm trả về VOID.
Trong ví dụ sau, hàm thực hiện việc giấu thông tin về tên và tên nước của provider với việc dùng hàm md5 như sau :
CREATE OR REPLACE FUNCTION mask_provider()
RETURNS VOID AS
$BODY$
    UPDATE provider SET name = md5(name), country = md5(country);
$BODY$
LANGUAGE sql VOLATILE;

SELECT mask_provider();

Giải thích :
- Hàm thực hiện việc cập nhật và giấu thông tin tên và tên nước của provider bằng cách mã hoá hai thông tin này bằng hàm md5.

4. Ví dụ trả về nhiều dòng kết quả

Ví dụ 1
Hàm trả về danh sách tên của điện thoại di động trong bảng mobile.
CREATE OR REPLACE FUNCTION get_mobile_name()
RETURNS SETOF TEXT AS
$BODY$
    SELECT name FROM mobile;
$BODY$
LANGUAGE sql VOLATILE;

SELECT get_mobile_name();
get_mobile_name  
-------------------
 Samsung Galaxy S4
 iPhone 6
(2 rows)

Giải thích :
- Ở đây ta dùng từ khoá SETOF sau RETURNS để hàm trả về toàn bộ kết quả của câu truy vấn cuối trong hàm (trong ví dụ, hàm chỉ có một câu truy vấn).


Ví dụ 2
Hàm trả về danh sách tên của điện thoại di động trong bảng mobile, với kiểu trả về cũng chính là kiểu của cột name trong bảng mobile.
CREATE OR REPLACE FUNCTION get_mobile_name()
RETURNS SETOF mobile.name%TYPE AS
$BODY$
    SELECT name FROM mobile;
$BODY$
LANGUAGE sql VOLATILE;

Giải thích :
- %TYPE thực hiện việc chép kiểu : mobile.name%TYPE cung cấp kiểu của cột name trong bảng mobile.


Ví dụ 3
Hàm trả về nội dung của toàn bộ bảng mobile
CREATE OR REPLACE FUNCTION get_mobile()
RETURNS SETOF mobile AS
$BODY$
    SELECT * FROM mobile;
$BODY$
LANGUAGE sql VOLATILE;

SELECT * FROM get_mobile();

Giải thích :
- Ta có thể dùng bảng có sẵn trong CSDL làm kiểu trả về (ở đây là bảng mobile)


Ví dụ 4
Ta có thể định nghĩa lại kiểu của bảng trả về như sau
CREATE OR REPLACE FUNCTION get_mobile()
RETURNS TABLE(id integer, name text) AS
$BODY$
    SELECT id, name FROM mobile;
$BODY$
LANGUAGE sql VOLATILE;

SELECT * FROM get_mobile();

Giải thích :
- TABLE(id integer, name text) định nghĩa kiểu của bảng trả về gồm hai cột có kiểu lần lượt là integer và text. RETURNS TABLE(colums) thuộc về chuần gần đây của SQL nên nó có tính khả chuyển (portable) hơn là dùng SETOF.

5.Tóm tắt
Ta có thể tóm gọn việc định nghĩa hàm bằng ngôn ngữ truy vấn như sau
CREATE FUNCTION function_name ([ [ [IN | OUT | INOUT | VARIADIC
[argument_name] argument_type] [,  ...] ]) 
[RETURNS {[SETOF] return_type | TABLE(columns)} ] AS $$
     function_body_query;
$$ language sql;

Xem cụ thể tại đây CREATE FUNCTION