Bỏ qua

Relationships & Foreign Keys

Các loại Relationship

Relationship Types

┌─────────────────────────────────────────────────────────────┐
│                    RELATIONSHIP TYPES                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. ONE-TO-ONE (1:1)                                        │
│     User ←────────→ Profile                                 │
│     Mỗi user có một profile                                 │
│                                                              │
│  2. ONE-TO-MANY (1:N)                                       │
│     Project ←───────→ Tasks                                 │
│     Mỗi project có nhiều tasks                              │
│                                                              │
│  3. MANY-TO-MANY (M:N)                                      │
│     Users ←──┬──→ Projects                                  │
│              │                                              │
│        project_members (junction table)                     │
│                                                              │
└─────────────────────────────────────────────────────────────┘

One-to-One Relationship

User ↔ Profile

-- auth.users là built-in
-- profiles extend user data

CREATE TABLE public.profiles (
  -- FK is also PK (1:1)
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,

  full_name TEXT,
  avatar_url TEXT,
  bio TEXT,

  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Query với join
SELECT
  u.email,
  p.full_name,
  p.avatar_url
FROM auth.users u
JOIN public.profiles p ON u.id = p.id;

Supabase Client

// Fetch user with profile
const { data } = await supabase
  .from('profiles')
  .select(`
    *,
    user:auth.users(email)
  `)
  .eq('id', userId)
  .single();

One-to-Many Relationship

Project → Tasks

CREATE TABLE public.projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  owner_id UUID REFERENCES auth.users(id) ON DELETE CASCADE
);

CREATE TABLE public.tasks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,

  -- Foreign Key to projects (Many tasks → One project)
  project_id UUID NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,

  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for faster joins
CREATE INDEX idx_tasks_project ON tasks(project_id);

Supabase Client - Nested Query

// Get project with all tasks
const { data: project } = await supabase
  .from('projects')
  .select(`
    id,
    name,
    tasks (
      id,
      title,
      status,
      created_at
    )
  `)
  .eq('id', projectId)
  .single();

// Result:
// {
//   id: "uuid",
//   name: "Project 1",
//   tasks: [
//     { id: "uuid", title: "Task 1", ... },
//     { id: "uuid", title: "Task 2", ... }
//   ]
// }

Many-to-Many Relationship

Users ↔ Projects (với roles)

-- Junction table
CREATE TABLE public.project_members (
  -- Composite Primary Key
  project_id UUID REFERENCES public.projects(id) ON DELETE CASCADE,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,

  -- Extra data on relationship
  role TEXT DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
  joined_at TIMESTAMPTZ DEFAULT NOW(),

  PRIMARY KEY (project_id, user_id)
);

-- Indexes
CREATE INDEX idx_project_members_user ON project_members(user_id);
CREATE INDEX idx_project_members_project ON project_members(project_id);

Query Many-to-Many

// Get project with members
const { data: project } = await supabase
  .from('projects')
  .select(`
    id,
    name,
    project_members (
      role,
      joined_at,
      user:profiles (
        id,
        full_name,
        avatar_url
      )
    )
  `)
  .eq('id', projectId)
  .single();

// Get all projects for a user
const { data: userProjects } = await supabase
  .from('project_members')
  .select(`
    role,
    project:projects (
      id,
      name,
      owner_id
    )
  `)
  .eq('user_id', userId);

ON DELETE Behaviors

Cascade vs Set Null vs Restrict

-- CASCADE: Xóa parent → xóa children
project_id UUID REFERENCES projects(id) ON DELETE CASCADE
-- Xóa project → xóa tất cả tasks

-- SET NULL: Xóa parent → set FK = null
assignee_id UUID REFERENCES users(id) ON DELETE SET NULL
-- Xóa user → task.assignee_id = NULL

-- RESTRICT: Không cho xóa parent nếu có children
category_id UUID REFERENCES categories(id) ON DELETE RESTRICT
-- Không xóa được category nếu còn products

-- NO ACTION (default): Giống RESTRICT

Khi nào dùng gì?

Behavior Use Case
CASCADE Owned data (project → tasks)
SET NULL Optional reference (task → assignee)
RESTRICT Required reference (order → product)

Self-referencing Relationship

Tasks với Subtasks

CREATE TABLE public.tasks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title TEXT NOT NULL,
  project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,

  -- Self-reference for subtasks
  parent_id UUID REFERENCES public.tasks(id) ON DELETE CASCADE,

  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index
CREATE INDEX idx_tasks_parent ON tasks(parent_id);

Query Hierarchical Data

// Get task with subtasks
const { data: task } = await supabase
  .from('tasks')
  .select(`
    id,
    title,
    subtasks:tasks!parent_id (
      id,
      title,
      status
    )
  `)
  .eq('id', taskId)
  .single();

// Get all root tasks (no parent)
const { data: rootTasks } = await supabase
  .from('tasks')
  .select(`
    *,
    subtasks:tasks!parent_id (*)
  `)
  .is('parent_id', null)
  .eq('project_id', projectId);

Recursive Query với CTE

Lấy toàn bộ tree

-- Get all descendants of a task
WITH RECURSIVE task_tree AS (
  -- Base case: root task
  SELECT id, title, parent_id, 0 as depth
  FROM tasks
  WHERE id = 'root-task-id'

  UNION ALL

  -- Recursive case: children
  SELECT t.id, t.title, t.parent_id, tt.depth + 1
  FROM tasks t
  JOIN task_tree tt ON t.parent_id = tt.id
)
SELECT * FROM task_tree;

Supabase RPC Function

-- Create function for recursive query
CREATE OR REPLACE FUNCTION get_task_tree(root_id UUID)
RETURNS TABLE (
  id UUID,
  title TEXT,
  parent_id UUID,
  depth INTEGER
) AS $$
  WITH RECURSIVE task_tree AS (
    SELECT t.id, t.title, t.parent_id, 0 as depth
    FROM tasks t
    WHERE t.id = root_id

    UNION ALL

    SELECT t.id, t.title, t.parent_id, tt.depth + 1
    FROM tasks t
    JOIN task_tree tt ON t.parent_id = tt.id
  )
  SELECT * FROM task_tree;
$$ LANGUAGE sql STABLE;
// Call from client
const { data } = await supabase.rpc('get_task_tree', {
  root_id: taskId
});

Relationship Shortcuts

// Supabase auto-detects relationships via FK

// Forward relation (child → parent)
const { data } = await supabase
  .from('tasks')
  .select(`
    *,
    project:projects(name),
    assignee:profiles(full_name)
  `);

// Reverse relation (parent → children)
const { data } = await supabase
  .from('projects')
  .select(`
    *,
    tasks(count)
  `);

// Multiple levels deep
const { data } = await supabase
  .from('projects')
  .select(`
    name,
    tasks (
      title,
      comments (
        content,
        author:profiles(full_name)
      )
    )
  `);

Best Practices

Relationship Design

1. Luôn đặt Foreign Keys
   - Data integrity
   - Auto-join trong Supabase

2. Index tất cả FK columns
   - Faster JOINs
   - Faster ON DELETE CASCADE

3. Chọn ON DELETE behavior phù hợp
   - CASCADE cho owned data
   - SET NULL cho optional references

4. Tránh deep nesting
   - Max 3-4 levels
   - Dùng RPC cho complex queries

5. Junction table cho M:N
   - Thêm metadata (role, joined_at)
   - Composite PK

Common Patterns

Pattern Implementation
User Profile 1:1 with auth.users
Comments 1:N with polymorphic
Tags M:N with junction
Tree/Hierarchy Self-reference
Audit Log 1:N với JSONB

Tổng kết

Relationship Types

Type FK Location Example
1:1 Either table user ↔ profile
1:N Child table project → tasks
M:N Junction table users ↔ projects
Self Same table task → subtasks

Supabase Query Patterns

// Forward: child.select('*, parent(*)')
// Reverse: parent.select('*, children(*)')
// M:N: either.select('*, junction(*, other(*))')

Q&A

  1. Có relationship phức tạp nào trong dự án?
  2. Đã dùng CTE recursive query chưa?
  3. Vấn đề gì với nested data?