Functions & Triggers
PostgreSQL Functions trong Supabase
Tại sao dùng Functions?
┌─────────────────────────────────────────────────────────────┐
│ WHY DATABASE FUNCTIONS? │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. PERFORMANCE │
│ - Chạy trong database (không network latency) │
│ - Batch operations nhanh hơn │
│ │
│ 2. SECURITY │
│ - SECURITY DEFINER bypasses RLS │
│ - Complex authorization logic │
│ │
│ 3. REUSABILITY │
│ - Dùng lại trong triggers, RLS policies │
│ - Call từ client via RPC │
│ │
│ 4. DATA INTEGRITY │
│ - Validate complex business rules │
│ - Atomic transactions │
│ │
└─────────────────────────────────────────────────────────────┘
Function Basics
Simple Function
-- Basic function
CREATE OR REPLACE FUNCTION hello_world()
RETURNS TEXT AS $$
BEGIN
RETURN 'Hello, World!';
END;
$$ LANGUAGE plpgsql;
-- Call it
SELECT hello_world(); -- 'Hello, World!'
Function với Parameters
-- Function with parameters
CREATE OR REPLACE FUNCTION add_numbers(a INTEGER, b INTEGER)
RETURNS INTEGER AS $$
BEGIN
RETURN a + b;
END;
$$ LANGUAGE plpgsql;
-- Call it
SELECT add_numbers(5, 3); -- 8
Function trả về Table
-- Return multiple rows
CREATE OR REPLACE FUNCTION get_active_projects(user_uuid UUID)
RETURNS TABLE (
id UUID,
name TEXT,
task_count BIGINT
) AS $$
BEGIN
RETURN QUERY
SELECT
p.id,
p.name,
COUNT(t.id)::BIGINT as task_count
FROM projects p
LEFT JOIN tasks t ON t.project_id = p.id
WHERE p.owner_id = user_uuid
GROUP BY p.id, p.name;
END;
$$ LANGUAGE plpgsql STABLE;
SECURITY DEFINER vs INVOKER
Sự khác biệt
-- SECURITY INVOKER (default)
-- Chạy với quyền của người gọi
-- RLS policies áp dụng
CREATE FUNCTION my_function()
RETURNS void AS $$
...
$$ LANGUAGE plpgsql SECURITY INVOKER;
-- SECURITY DEFINER
-- Chạy với quyền của người tạo function (thường là postgres)
-- BYPASS RLS
CREATE FUNCTION admin_function()
RETURNS void AS $$
...
$$ LANGUAGE plpgsql SECURITY DEFINER;
Khi nào dùng SECURITY DEFINER?
-- ✅ USE CASE: Auto-create profile sau signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
-- Cần bypass RLS để INSERT vào profiles
INSERT INTO public.profiles (id, email)
VALUES (NEW.id, NEW.email);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- ✅ USE CASE: Admin operations
CREATE OR REPLACE FUNCTION admin_delete_user(target_user_id UUID)
RETURNS void AS $$
BEGIN
-- Check if caller is admin first
IF NOT is_admin(auth.uid()) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- Bypass RLS to delete
DELETE FROM profiles WHERE id = target_user_id;
DELETE FROM auth.users WHERE id = target_user_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Triggers
Trigger là gì?
┌─────────────────────────────────────────────────────────────┐
│ TRIGGER FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ INSERT/UPDATE/DELETE ──▶ BEFORE Trigger ──▶ Operation │
│ │ │
│ ▼ │
│ AFTER Trigger │
│ │
│ Timing: BEFORE or AFTER │
│ Events: INSERT, UPDATE, DELETE │
│ Level: FOR EACH ROW or FOR EACH STATEMENT │
│ │
└─────────────────────────────────────────────────────────────┘
Trigger Variables
-- Special variables trong trigger function
NEW -- Row mới (INSERT, UPDATE)
OLD -- Row cũ (UPDATE, DELETE)
TG_OP -- Operation: 'INSERT', 'UPDATE', 'DELETE'
Common Trigger Patterns
1. Auto-update Timestamps
-- Sử dụng extension moddatetime
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON tasks
FOR EACH ROW
EXECUTE FUNCTION extensions.moddatetime(updated_at);
-- Hoặc tự viết
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_timestamp
BEFORE UPDATE ON tasks
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
2. Auto-create Profile
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'full_name'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();
3. Audit Log
-- Audit table
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
changed_by UUID,
changed_at TIMESTAMPTZ DEFAULT NOW()
);
-- Generic audit function
CREATE OR REPLACE FUNCTION audit_changes()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_by)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP != 'INSERT' THEN to_jsonb(OLD) END,
CASE WHEN TG_OP != 'DELETE' THEN to_jsonb(NEW) END,
auth.uid()
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Apply to table
CREATE TRIGGER audit_tasks
AFTER INSERT OR UPDATE OR DELETE ON tasks
FOR EACH ROW
EXECUTE FUNCTION audit_changes();
4. Notify External Service
-- Notify via pg_notify (for Realtime or external consumers)
CREATE OR REPLACE FUNCTION notify_task_change()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(
'task_changes',
json_build_object(
'action', TG_OP,
'task_id', COALESCE(NEW.id, OLD.id),
'project_id', COALESCE(NEW.project_id, OLD.project_id)
)::text
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_task_change
AFTER INSERT OR UPDATE OR DELETE ON tasks
FOR EACH ROW
EXECUTE FUNCTION notify_task_change();
5. Enqueue to Message Queue
-- Send to pgmq on specific events
CREATE OR REPLACE FUNCTION enqueue_notification()
RETURNS TRIGGER AS $$
BEGIN
-- Enqueue email notification
PERFORM pgmq.send(
'email_queue',
jsonb_build_object(
'type', 'task_assigned',
'task_id', NEW.id,
'assignee_id', NEW.assignee_id
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Only trigger when assignee changes
CREATE TRIGGER on_task_assigned
AFTER UPDATE ON tasks
FOR EACH ROW
WHEN (OLD.assignee_id IS DISTINCT FROM NEW.assignee_id AND NEW.assignee_id IS NOT NULL)
EXECUTE FUNCTION enqueue_notification();
Call Functions từ Client
Supabase RPC
// Simple function call
const { data, error } = await supabase.rpc('hello_world');
// With parameters
const { data, error } = await supabase.rpc('add_numbers', {
a: 5,
b: 3
});
// Function returning rows
const { data: projects } = await supabase.rpc('get_active_projects', {
user_uuid: userId
});
Complex Business Logic
-- Transaction with multiple operations
CREATE OR REPLACE FUNCTION create_project_with_tasks(
project_name TEXT,
task_titles TEXT[]
)
RETURNS UUID AS $$
DECLARE
new_project_id UUID;
BEGIN
-- Create project
INSERT INTO projects (name, owner_id)
VALUES (project_name, auth.uid())
RETURNING id INTO new_project_id;
-- Create tasks
INSERT INTO tasks (project_id, title)
SELECT new_project_id, unnest(task_titles);
RETURN new_project_id;
END;
$$ LANGUAGE plpgsql SECURITY INVOKER;
// Call from client
const { data: projectId } = await supabase.rpc('create_project_with_tasks', {
project_name: 'New Project',
task_titles: ['Task 1', 'Task 2', 'Task 3']
});
Function Attributes
-- STABLE: Function không modify data, same input = same output trong transaction
CREATE FUNCTION get_user_count()
RETURNS BIGINT AS $$
SELECT COUNT(*) FROM profiles;
$$ LANGUAGE sql STABLE;
-- IMMUTABLE: Pure function, same input = same output luôn
CREATE FUNCTION calculate_tax(amount NUMERIC)
RETURNS NUMERIC AS $$
SELECT amount * 0.1;
$$ LANGUAGE sql IMMUTABLE;
-- VOLATILE (default): Có thể modify data hoặc return khác nhau mỗi lần
CREATE FUNCTION insert_log(msg TEXT)
RETURNS void AS $$
INSERT INTO logs (message) VALUES (msg);
$$ LANGUAGE sql VOLATILE;
Usage in Indexes
-- IMMUTABLE functions có thể dùng trong index
CREATE INDEX idx_lower_email ON users (lower(email));
-- lower() là immutable
Debugging Functions
RAISE for Logging
CREATE OR REPLACE FUNCTION debug_example()
RETURNS void AS $$
BEGIN
RAISE DEBUG 'Starting function';
RAISE NOTICE 'Processing...';
RAISE WARNING 'This might be slow';
RAISE EXCEPTION 'Something went wrong!'; -- Aborts transaction
END;
$$ LANGUAGE plpgsql;
View Logs
-- In Supabase Dashboard > Logs > Postgres
-- Or via SQL
SELECT * FROM postgres_logs
WHERE event_message LIKE '%debug_example%'
ORDER BY timestamp DESC;
Tổng kết
Function Best Practices
- Naming:
verb_noun format (get_active_projects, create_task)
- SECURITY DEFINER: Chỉ khi cần bypass RLS
- Attributes: Set STABLE/IMMUTABLE khi có thể
- Error handling: Use RAISE EXCEPTION với clear messages
- Input validation: Validate parameters ngay đầu function
Trigger Best Practices
- Keep it fast: Trigger chạy synchronously
- Avoid loops: Trigger A → Trigger B → Trigger A
- Use WHEN clause: Filter events cần handle
- SECURITY DEFINER: Cho operations cần elevated privileges
Common Patterns
| Pattern |
Implementation |
| Auto timestamp |
moddatetime trigger |
| Auto-create related |
AFTER INSERT trigger |
| Audit log |
AFTER trigger with JSONB |
| Enqueue job |
Trigger → pgmq.send() |
| Notify realtime |
pg_notify() |
Q&A
- Có business logic nào nên move vào function?
- Đã dùng triggers trước đây chưa?
- Vấn đề gì với audit logging?