【Supabase 教學】#03 資料表設計與 CRUD 操作實戰

測驗:Supabase 資料表設計與 CRUD 操作實戰

共 5 題,點選答案後會立即顯示結果

1. 在 Supabase 中使用 .insert() 新增資料後,如果想要取得剛新增的資料,應該加上什麼方法?

  • A. .returning()
  • B. .select()
  • C. .get()
  • D. .fetch()

2. 在 Supabase 查詢中,.single() 方法的作用是什麼?

  • A. 只選取一個欄位
  • B. 限制查詢結果最多一筆
  • C. 預期只有一筆結果,直接回傳物件而非陣列
  • D. 將查詢結果合併為單一字串

3. 以下哪個過濾方法可以用來做不區分大小寫的模糊搜尋?

  • A. .like()
  • B. .ilike()
  • C. .contains()
  • D. .match()

4. 在 Supabase 中設定關聯查詢時,若要查詢文章及其所有留言,正確的 select 語法是?

  • A. .select('posts.*, comments.*')
  • B. .select('*').join('comments')
  • C. .select('* INNER JOIN comments')
  • D. .select('*, comments(*)')

5. 在進行 .update().delete() 操作時,如果沒有加上過濾條件(如 .eq()),會發生什麼事?

  • A. 操作會失敗並拋出錯誤
  • B. 會對整個資料表的所有資料執行操作
  • C. 只會影響第一筆資料
  • D. 系統會自動跳過,不執行任何操作

前言

在前兩篇文章中,我們已經認識了 Supabase 並完成專案建立與環境設定。現在是時候開始真正操作資料庫了。本篇將帶你從建立資料表開始,一路學會完整的 CRUD(Create, Read, Update, Delete)操作。

當你用 AI 輔助開發時,看到 Supabase 相關程式碼,你會知道每一行在做什麼,而不是盲目地複製貼上。

學習目標

讀完本篇後,你將能夠:

  • 在 Supabase 中設計並建立資料表
  • 使用 Supabase Client 進行完整的 CRUD 操作
  • 了解 Supabase 查詢語法與過濾條件
  • 處理資料關聯與 JOIN 查詢

一、使用 Table Editor 建立資料表

Supabase 提供直覺的圖形化介面讓你建立資料表,不需要手寫 SQL。

1.1 進入 Table Editor

登入 Supabase Dashboard 後,在左側選單點選 Table Editor,你會看到目前專案的所有資料表。

1.2 建立第一個資料表

點選 New Table 按鈕,會出現建立表單。以建立一個 posts(文章)資料表為例:

欄位名稱 資料型別 說明
id int8 主鍵,自動遞增
title text 文章標題
content text 文章內容
author_id uuid 作者 ID(關聯 auth.users)
is_published bool 是否已發布
view_count int4 瀏覽次數
created_at timestamptz 建立時間

1.3 常用資料型別對照

當你在 AI 生成的程式碼中看到這些型別時:

int2, int4, int8    → 整數(2/4/8 bytes)
float4, float8      → 浮點數
text, varchar       → 文字
bool                → 布林值
uuid                → 通用唯一識別碼
timestamptz         → 帶時區的時間戳記
jsonb               → JSON 資料(可查詢)
Code language: JavaScript (javascript)

1.4 重要約束設定

建立欄位時,你會看到這些選項:

  • Primary Key(主鍵):唯一識別每一筆資料
  • Is Nullable:是否允許空值(NULL)
  • Is Unique:是否要求值不重複
  • Default Value:預設值,例如 now() 自動填入當前時間

二、Supabase Client 初始化

在進行 CRUD 操作前,需要先初始化 Supabase Client。

2.1 JavaScript/TypeScript 初始化

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://your-project.supabase.co'
const supabaseAnonKey = 'your-anon-key'

const supabase = createClient(supabaseUrl, supabaseAnonKey)
Code language: JavaScript (javascript)

當你看到這段程式碼時,理解重點:

  • supabaseUrl:你的專案 API 端點
  • supabaseAnonKey:公開的匿名金鑰(可以放在前端)
  • createClient:建立連線實例,後續所有操作都透過它

2.2 Python 初始化

from supabase import create_client

url = "https://your-project.supabase.co"
key = "your-anon-key"

supabase = create_client(url, key)
Code language: JavaScript (javascript)

三、CRUD 操作詳解

3.1 Create:新增資料

新增單筆資料

const { data, error } = await supabase
  .from('posts')           // 指定資料表
  .insert({                // 插入的資料物件
    title: '我的第一篇文章',
    content: '這是內容...',
    is_published: true
  })
  .select()                // 回傳新增的資料
Code language: JavaScript (javascript)

逐行解讀:

  • .from('posts'):選擇要操作的資料表
  • .insert({...}):傳入要新增的資料物件
  • .select():讓回傳結果包含新增的資料(否則只回傳 null)

新增多筆資料

const { data, error } = await supabase
  .from('posts')
  .insert([
    { title: '文章一', content: '內容一' },
    { title: '文章二', content: '內容二' },
    { title: '文章三', content: '內容三' }
  ])
  .select()
Code language: JavaScript (javascript)

傳入陣列即可批次新增。

3.2 Read:查詢資料

查詢所有資料

const { data, error } = await supabase
  .from('posts')
  .select('*')  // * 表示選取所有欄位
Code language: JavaScript (javascript)

查詢特定欄位

const { data, error } = await supabase
  .from('posts')
  .select('id, title, created_at')  // 只取需要的欄位
Code language: JavaScript (javascript)

效能提示:只選取需要的欄位可以減少傳輸量。

查詢單筆資料

const { data, error } = await supabase
  .from('posts')
  .select('*')
  .eq('id', 1)      // 條件:id 等於 1
  .single()         // 預期只有一筆結果
Code language: JavaScript (javascript)

.single() 會直接回傳物件而非陣列,如果查到多筆或零筆會報錯。

3.3 Update:更新資料

const { data, error } = await supabase
  .from('posts')
  .update({
    title: '更新後的標題',
    is_published: false
  })
  .eq('id', 1)      // 重要:指定要更新哪一筆
  .select()
Code language: JavaScript (javascript)

注意:如果沒有加上過濾條件(如 .eq()),會更新整個資料表的所有資料!

3.4 Delete:刪除資料

const { data, error } = await supabase
  .from('posts')
  .delete()
  .eq('id', 1)      // 重要:指定要刪除哪一筆
Code language: JavaScript (javascript)

同樣地,沒有過濾條件會刪除所有資料。


四、查詢過濾條件

Supabase 提供豐富的過濾方法,讓你精確查詢資料。

4.1 比較運算子

// 等於
.eq('status', 'active')        // status = 'active'

// 不等於
.neq('status', 'deleted')      // status != 'deleted'

// 大於
.gt('view_count', 100)         // view_count > 100

// 大於等於
.gte('view_count', 100)        // view_count >= 100

// 小於
.lt('view_count', 50)          // view_count < 50

// 小於等於
.lte('view_count', 50)         // view_count <= 50
Code language: JavaScript (javascript)

4.2 文字搜尋

// 模糊搜尋(區分大小寫)
.like('title', '%教學%')       // title LIKE '%教學%'

// 模糊搜尋(不區分大小寫)
.ilike('title', '%tutorial%')  // title ILIKE '%tutorial%'
Code language: PHP (php)

% 是萬用字元,代表任意字元。

4.3 陣列與範圍

// 在指定值中
.in('status', ['draft', 'review'])  // status IN ('draft', 'review')

// 包含(陣列欄位)
.contains('tags', ['javascript'])   // tags 陣列包含 'javascript'

// 範圍查詢
.gte('created_at', '2024-01-01')
.lt('created_at', '2024-02-01')     // 查詢一月份的資料
Code language: JavaScript (javascript)

4.4 組合多個條件

const { data, error } = await supabase
  .from('posts')
  .select('*')
  .eq('is_published', true)
  .gt('view_count', 100)
  .order('created_at', { ascending: false })
Code language: JavaScript (javascript)

多個過濾條件會以 AND 邏輯組合。

4.5 OR 條件

const { data, error } = await supabase
  .from('posts')
  .select('*')
  .or('status.eq.draft,status.eq.review')
Code language: JavaScript (javascript)

使用 .or() 時,條件格式是 欄位.運算子.值,用逗號分隔。


五、排序與分頁

5.1 排序 (order)

// 依建立時間降序排列(最新的在前)
.order('created_at', { ascending: false })

// 多欄位排序
.order('is_published', { ascending: false })
.order('created_at', { ascending: false })
Code language: JavaScript (javascript)

5.2 限制筆數 (limit)

// 只取前 10 筆
.limit(10)
Code language: JavaScript (javascript)

5.3 分頁 (range)

// 取第 11-20 筆(從 0 開始算)
.range(10, 19)
Code language: JavaScript (javascript)

5.4 完整分頁範例

const page = 2          // 第幾頁(從 1 開始)
const pageSize = 10     // 每頁筆數
const from = (page - 1) * pageSize
const to = from + pageSize - 1

const { data, error, count } = await supabase
  .from('posts')
  .select('*', { count: 'exact' })  // 同時取得總筆數
  .order('created_at', { ascending: false })
  .range(from, to)

console.log(`總共 ${count} 筆,顯示第 ${from + 1}${to + 1} 筆`)
Code language: JavaScript (javascript)

{ count: 'exact' } 會額外回傳符合條件的總筆數,方便計算總頁數。


六、關聯查詢

6.1 設定 Foreign Key

在 Table Editor 中建立 comments 資料表時,設定 post_id 欄位關聯到 posts.id

  1. 建立 post_id 欄位(型別 int8)
  2. 點選欄位旁的連結圖示
  3. 選擇 Foreign Key 關聯到 posts 表的 id 欄位

6.2 查詢關聯資料

設定好 Foreign Key 後,可以用巢狀語法一次查詢關聯資料:

// 查詢文章,同時取得該文章的所有留言
const { data, error } = await supabase
  .from('posts')
  .select(`
    id,
    title,
    comments (
      id,
      content,
      created_at
    )
  `)
Code language: JavaScript (javascript)

回傳結構:

[
  {
    id: 1,
    title: '我的第一篇文章',
    comments: [
      { id: 1, content: '好文章!', created_at: '...' },
      { id: 2, content: '學到很多', created_at: '...' }
    ]
  }
]
Code language: JavaScript (javascript)

6.3 反向查詢

從 comments 查詢對應的 posts:

const { data, error } = await supabase
  .from('comments')
  .select(`
    id,
    content,
    posts (
      id,
      title
    )
  `)
Code language: JavaScript (javascript)

6.4 多層關聯

假設還有 users 資料表:

const { data, error } = await supabase
  .from('posts')
  .select(`
    id,
    title,
    users!author_id (
      id,
      name
    ),
    comments (
      id,
      content,
      users (
        id,
        name
      )
    )
  `)
Code language: JavaScript (javascript)

users!author_id 語法指定使用 author_id 這個 Foreign Key 關聯。


七、錯誤處理

每個操作都會回傳 error 物件,務必檢查:

const { data, error } = await supabase
  .from('posts')
  .select('*')

if (error) {
  console.error('查詢失敗:', error.message)
  // error.code 包含錯誤代碼
  // error.details 包含詳細資訊
  return
}

// 成功,使用 data
console.log('查詢結果:', data)
Code language: JavaScript (javascript)

常見錯誤代碼:

  • PGRST116:查詢結果為空(使用 .single() 時)
  • 23505:違反唯一約束(重複資料)
  • 23503:違反 Foreign Key 約束
  • 42P01:資料表不存在

八、實戰小結

快速參考表

操作 方法 範例
新增 .insert() .insert({ title: '...' }).select()
查詢 .select() .select('id, title')
更新 .update() .update({ title: '...' }).eq('id', 1)
刪除 .delete() .delete().eq('id', 1)
過濾 .eq() .gt() .like() .eq('status', 'active')
排序 .order() .order('created_at', { ascending: false })
分頁 .range() .limit() .range(0, 9)
關聯 巢狀 select .select('*, comments(*)')

常見模式

// 取得分頁列表
const getPostList = async (page, pageSize) => {
  const from = (page - 1) * pageSize
  const to = from + pageSize - 1

  return await supabase
    .from('posts')
    .select('id, title, created_at', { count: 'exact' })
    .eq('is_published', true)
    .order('created_at', { ascending: false })
    .range(from, to)
}

// 取得單篇文章(含留言)
const getPostDetail = async (id) => {
  return await supabase
    .from('posts')
    .select(`
      *,
      comments (
        id, content, created_at,
        users (id, name)
      )
    `)
    .eq('id', id)
    .single()
}
Code language: JavaScript (javascript)

結語

現在你已經掌握了 Supabase 的資料表設計與 CRUD 操作。這些是與資料庫互動的基本功,無論你用什麼框架,這些概念都通用。

下一篇我們將深入探討 Row Level Security(RLS),學習如何保護你的資料安全,確保使用者只能存取他們被授權的資料。


延伸閱讀

進階測驗:Supabase 資料表設計與 CRUD 操作實戰

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個部落格系統,需要實作文章列表頁面的分頁功能。每頁顯示 10 篇文章,使用者目前在第 3 頁。你應該如何設定 range 參數? 情境題

const page = 3 const pageSize = 10 // 計算 range 參數…
  • A. .range(3, 12)
  • B. .range(30, 39)
  • C. .range(20, 29)
  • D. .range(21, 30)

2. 你需要查詢所有已發布且瀏覽次數超過 100 的文章,並按建立時間從新到舊排序。以下哪個寫法最正確? 情境題

  • A. .select('*').eq('is_published', true).gt('view_count', 100).order('created_at')
  • B. .select('*').eq('is_published', true).gt('view_count', 100).order('created_at', { ascending: false })
  • C. .select('*').or('is_published.eq.true,view_count.gt.100').order('created_at', { ascending: false })
  • D. .select('*').filter('is_published = true AND view_count > 100').orderBy('created_at DESC')

3. 你想要查詢一篇文章的詳細資訊,包含該文章的所有留言,以及每則留言的作者資訊。假設 comments 表有 user_id 欄位關聯到 users 表。以下哪個 select 語法最適合? 情境題

  • A. .select('*, comments, users')
  • B. .select('*').join('comments').join('users')
  • C. .select('*, comments(*), comments.users(*)')
  • D. .select('*, comments(*, users(*))')

4. 小明使用以下程式碼查詢單一文章,但執行後發生錯誤。最可能的原因是什麼? 錯誤診斷

const { data, error } = await supabase .from(‘posts’) .select(‘*’) .eq(‘slug’, ‘my-first-post’) .single() // error: { code: ‘PGRST116’, message: ‘…’ }
  • A. slug 欄位不存在於資料表中
  • B. 查詢結果為空(沒有找到符合條件的資料)
  • C. .single() 不能與 .eq() 一起使用
  • D. 缺少 .from() 之前的 await

5. 小華想要更新特定文章的標題,但執行後發現所有文章的標題都被改掉了。請問程式碼哪裡出了問題? 錯誤診斷

const { data, error } = await supabase .from(‘posts’) .update({ title: ‘新標題’ }) .select() // 預期只更新 id=5 的文章,但所有文章都被更新了
  • A. .select() 應該放在 .update() 之前
  • B. 應該使用 .updateOne() 而非 .update()
  • C. 缺少過濾條件(如 .eq('id', 5))來指定要更新哪筆資料
  • D. .update() 的參數格式錯誤,應該用陣列

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *