Fork me on GitHub

2025年5月

详解 Daytona 的镜像管理

在上一篇文章中,我们学习了 Daytona 的沙箱管理功能,包括沙箱的创建、资源限制、自动停止机制以及沙箱状态的管理。今天我们将继续学习 Daytona 的镜像管理功能。可以把镜像比喻成一个模板,它包含了沙箱中所需的所有依赖项、工具和环境设置。Daytona 支持所有标准的 Docker 或 OCI 兼容镜像。

我们在之前的代码中创建沙箱时使用了一个自定义镜像 debian:12.9,细心的同学可能已经发现了,如果你直接运行那段代码是会报错的:

daytona_sdk.common.errors.DaytonaError: 
    Failed to create sandbox: Image debian:12.9 not found or not accessible

这是因为在 Daytona 中使用自定义镜像,我们必须得提前创建它。

默认镜像

在创建镜像之前,我们先来看看 Daytona 的默认镜像长啥样,前面提到过,当使用默认镜像时,Daytona 会自动从池中获取,所以创建速度极快,如果用自定义镜像,就没有这个优势了,所以我们应该当且仅当默认镜像满足不了我们的需求时,才使用自定义镜像。

Daytona 默认镜像中预装了一些有用的工具,例如 python、node、pip 以及一些常见的 pip 包,如下:

daytona-default-image.png

镜像的创建

当我们确实需要使用自定义镜像时,可以进入 Dashboard -> Images 页面,点击 Create Image 按钮创建新镜像:

daytona-create-image.png

这里可以填写 Docker Hub 上的任何公共镜像,注意名称和标签都必须填写,比如我们这里使用的 debian:12.9,而且建议不要写 latest 或 stable 这样的标签。也可以填写来自其他公共仓库上的镜像,只要在前面加上仓库地址即可,比如来自 Github 的镜像仓库的 ghcr.io/graalvm/jdk-community:21 镜像。

另外,为了确保你的镜像运行后不会立即退出,一定要有一个长时间运行的入口点(entrypoint),如果不确定,可以直接填上 sleep infinity 命令。

镜像一旦被创建,就会被自动拉取,然后自动验证,验证通过后就会进入 Active 状态,此时我们才可以在程序中使用它:

daytona-images.png

私有仓库镜像

Daytona 也支持使用来自私有仓库的镜像,我们需要先在 Dashboard -> Registries 页面配置私有仓库:

daytona-add-registry.png

在这里输入仓库名称、URL、用户名、密码以及项目名称即可,确保 Daytona 能访问你的私有仓库。然后就可以创建私有仓库的镜像了,注意镜像名称包括私有仓库的 URL 和项目名称,比如 my-private-registry.com/<my-project>/alpine:3.21

本地镜像

除了公共镜像和私有仓库镜像,还有一种情况是,我们在本地开发和构建的镜像。我们可能没有自己的私有镜像仓库,也不想推送到公共镜像仓库中,这时,就可以使用 Daytona 提供的命令行工具将本地镜像推送到 Daytona 来使用。

首先,安装 Daytona CLI:

$ brew install daytonaio/cli/daytona

然后,通过 daytona login 登录:

$ daytona login

Select Authentication Method

  › Login with Browser
    Authenticate using OAuth in your browser

    Set Daytona API Key
    Authenticate using Daytona API key

Daytona 会显示两种登录方式:浏览器登录,或输入 API KEY。这里我们选择浏览器,登录成功:

 Opening the browser for authentication ...
                                           
 If opening fails, visit:
                         
https://daytonaio.us.auth0.com/authorize?audience=...
                        
 Successfully logged in!

接着就可以通过 daytona image push 推送本地镜像了:

$ daytona image push test-image:1.0
The push refers to repository [harbor-transient.internal.daytona.app/daytona/test-image]
08000c18d16d: Pushed 
1.0: digest: sha256:ec1b05d1eac264d9204a57f4ad9d4dc35e9e756e9fedaea0674aefc7edb1d6a4 size: 527
                                              
 Successfully pushed test-image:1.0 to Daytona
                                              
 ✓  Use 'harbor-transient.internal.daytona.app/daytona/test-image:1.0' to create a new sandbox using this image

这个命令将本地的 test-image:1.0 镜像成功推送到 Daytona 的镜像仓库中了,注意在使用这个镜像时,需要指定全路径。除了推送本地已有镜像,Daytona 也支持通过 Dockerfile 自动构建并推送镜像:

$ daytona image push test-image:1.0 --dockerfile ./Dockerfile

最后还有一点要注意的是,Daytona 目前仅支持 AMD 架构的镜像,还不支持 ARM 架构的镜像,否则会报错:

$ daytona image push test-image:1.0
FATA[0000] image 'test-image:1.0' is not compatible with AMD architecture

镜像管理

上面提到,我们可以在 Dashboard -> Images 页面对镜像进行管理,比如创建、禁用、删除等。我们也可以通过 daytona image 命令对镜像进行管理,比如:

  • daytona image create - 创建镜像
  • daytona image push - 推送本地镜像
  • daytona image delete - 删除镜像
  • daytona image list - 显示镜像列表

下面通过 daytona image list 命令列出所有镜像:

daytona-image-list.png

小结

好了,今天的内容就这么多。在本文中,我们详细探索了 Daytona 的镜像管理功能,包括镜像的创建、使用及管理。我们学习了默认镜像、创建自定义镜像以及如何使用私有仓库镜像和本地镜像,相信大家对 Daytona 的镜像功能有了更深入的了解。


详解 Daytona 的沙箱管理

在上一篇文章中,我们学习了 Daytona 的代码执行功能,特别是如何在沙箱中执行 Python 代码、Shell 脚本和长耗时任务。关于代码执行,还有一个小尾巴没有讲完,那就是代码执行的环境。我们知道,执行代码之前首先要创建沙箱,代码是在沙箱中被安全地执行,那么这个沙箱环境里都有什么东西呢?我们到底能在沙箱中执行哪些代码呢?我们今天就来看看这些问题。

沙箱的创建

回顾下前面的内容,创建沙箱的代码如下:

sandbox = daytona.create(CreateSandboxParams(language="python"))

我们是通过 Daytona 的 create() 方法创建沙箱的,其中 CreateSandboxParams 用于配置沙箱环境,该类的定义如下:

daytona-create-sandox-params.png

可以看到,除了编程语言还有不少的配置选项,包括镜像、环境变量、标签、资源限制、自动停止时间等等,下面是一个较复杂的例子:

params = CreateSandboxParams(
    language="python",
    image="debian:12.9",
    env_vars={"DEBUG": "true"},
    labels={"ENV": "dev"},
    resources=SandboxResources(cpu=2, memory=4),
    auto_stop_interval=0
)

在这个例子里,我们指定了基础镜像为 debian:12.9,学过 Docker 的同学应该很快就能理解这里的沙箱其实就是容器,而容器都是由镜像创建而来,我们指定不同的镜像,也就是为沙箱配置不同的环境,关于镜像的使用,我们后面再看,这里先看另几个配置。

值得注意的是,Daytona 针对默认镜像提前创建了一些沙箱放在池中,当用户使用默认镜像创建沙箱时,就会直接从池中取出一个可用的沙箱,从而将创建时间缩短到毫秒。所以,当使用自定义镜像时,你会发现创建沙箱的速度会变慢,因为自定义的镜像没有提前池化。

对沙箱进行资源限制

我们在用 Docker 启动一个容器的时候,可以通过 --memory--cpus 等参数限制容器能使用的系统资源:

$ docker run --memory=512m --cpus=1 ubuntu

Daytona 也提供了类似的机制,通过 SandboxResources 可以控制沙箱能使用的资源上限,该类定义如下:

daytona-sandbox-resources.png

下面是一个示例,限制 2 个 CPU 核心,1 个 GPU,4G 内存 和 20G 磁盘:

resources = SandboxResources(
    cpu=2,
    gpu=1,
    memory=4,
    disk=20
)

沙箱的自动停止

在前面的学习中,我们每次使用完沙箱后都是通过 daytona.remove(sandbox) 将沙箱删除掉。如果你不手工删除,Daytona 为了避免资源浪费,会自动检测沙箱的活动状态,对于 15 分钟内不活跃的沙箱,将自动停止。

这个 15 分钟就是默认的自动停止时间,我们可以在创建沙箱时通过 auto_stop_interval 参数修改这个值,如果我们不希望沙箱被自动停止,可以将其设置为 0:

sandbox = daytona.create(CreateSandboxParams(
    language = "python",
    auto_stop_interval = 0
))

操作沙箱

Daytona 对象提供了一系列和沙箱有关的操作,包括:

  • create() - 创建沙箱
  • delete() - 删除沙箱
  • get_current_sandbox() - 根据 ID 获取沙箱
  • find_one() - 根据 ID 或标签获取沙箱
  • list() - 列出所有沙箱
  • start() - 启动沙箱
  • stop() - 停止沙箱

这些操作都比较简单,和 Docker 操作容器也很类似,此处略过。

沙箱的状态

对沙箱的操作必然导致沙箱状态的变动,Daytona 的沙箱可以有三种状态:

  • 运行中:运行中的沙箱会占用 CPU、内存和磁盘存储,每种资源都按使用时间计费,所以当沙箱不再被主动使用时,建议将其停止。可以选择手动停止,也可以通过设置自动停止时间自动停止;
  • 已停止:已停止的沙箱不会占用 CPU 和内存,只占用磁盘存储,当你需要使用沙箱时可以再启动它;
  • 已归档:当沙箱被归档时,整个文件系统会迁移到更廉价的对象存储,这样可以更加节约成本,但是启动一个已归档的沙箱需要更多时间,你需要根据实际情况来权衡是停止还是归档你的沙箱。

小结

在本篇文章中,我们详细探讨了 Daytona 的沙箱管理,包括沙箱的创建、资源限制、自动停止机制以及沙箱状态的管理。了解沙箱的配置选项,不仅能提高代码的执行效率,还能有效控制成本,避免资源的浪费。自动停止功能进一步提升了资源管理的灵活性,使得开发者无需担心长时间未使用的沙箱占用系统资源。

沙箱和镜像是两个密不可分的概念,在后续的内容中,我们将进一步学习 Daytona 的镜像管理功能,希望大家继续关注!


详解 Daytona 的代码执行

在上一篇文章中,我们学习了 Daytona 的核心特性和基本用法,然后通过 Python SDK 创建沙箱并执行代码。回顾下之前学过的内容,可以将 Daytona 执行代码分成四步:

  1. 初始化 Daytona 客户端;
  2. 通过客户端创建沙箱,指定沙箱环境,Daytona 支持执行 Python 和 JS 代码;
  3. 在沙箱执行代码,返回执行结果;
  4. 销毁沙箱;

其中第三步执行代码是 Daytona 最核心的功能,我们今天继续深入学习它。

执行代码和运行命令

Daytona 的 Sandbox 类含有不少属性,如下:

daytona-sandbox.png

其中 process 属性是我们关注的焦点,通过它的 code_run() 方法可以执行代码:

response = sandbox.process.code_run('print("Hello")')

这个方法支持传入一些额外参数,比如环境变量或命令行参数等:

params = CodeRunParams(
    argv=[
        "-h", "-i"
    ], 
    env={
        'DEMO_ENV': 'xxx'
    }
)
response = sandbox.process.code_run('''
import os
import sys

print("环境变量:DEMO_ENV = ", os.getenv("DEMO_ENV"))
print("命令行参数:", *[f"{arg}" for _, arg in enumerate(sys.argv)])
''', params=params)

在代码中可以通过 os.getenv()sys.argv 来获取,调用结果如下:

环境变量:DEMO_ENV =  xxx
命令行参数: -c -h -i

这里的命令行参数比较有意思,可以看到我们只传入了 -h -i 两个参数,但是输出却多了一个 -c,这说明 Daytona 很有可能是通过 python -c 去执行我们的 Python 代码的:

python -c "print('Hello')"

process 属性还有一个 exec() 方法,可以运行 Shell 命令:

response = sandbox.process.exec('echo "Hello"')

这个方法同样支持传入一些额外参数,比如设置当前工作目录:

response = sandbox.process.exec('ls', cwd="/")

设置环境变量:

response = sandbox.process.exec('echo $DEMO_ENV', env={"DEMO_ENV": "xxx"})

运行长耗时任务

对于一些长耗时任务,为避免超时,可以在运行时加上 timeout 参数:

response = sandbox.process.exec("sleep 5", timeout=10)

除此之外,Daytona 还提供了一种推荐做法:通过后台会话运行长耗时任务

sandbox.process 属性提供了一系列和会话相关的方法:

  • create_session() - 创建会话
  • get_session() - 获取会话
  • list_sessions() 列出所有会话
  • delete_session() - 删除会话
  • execute_session_command() - 在会话中执行命令
  • get_session_command() - 获取会话中的命令
  • get_session_command_logs() - 获取会话中的命令日志
  • get_session_command_logs_async() - 异步获取会话中的命令日志

我们首先选择一个唯一标志作为会话 ID,然后通过它创建一个会话:

session_id = "my-session"
sandbox.process.create_session(session_id)

接着就可以使用 execute_session_command() 在会话中执行命令,要执行的命令需要放在一个 SessionExecuteRequest 对象中:

req = SessionExecuteRequest(
    run_async=True,
    command="for ((i=0; i<10; i++)); do echo $i; sleep 1; done"
)
response = sandbox.process.execute_session_command(session_id, req)

注意这里的 run_async=True 表示异步执行,调用完 execute_session_command() 方法后会立即返回,不会阻塞后面的代码。返回的 response 有一个 cmd_id 字段,通过 get_session_command() 方法可以检查命令的实时状态以及命令输出的日志。下面的代码示例中每隔 1 秒检查一次命令状态:

while(True):
    time.sleep(1)
    command = sandbox.process.get_session_command(session_id, response.cmd_id)
    if command.exit_code is None:
        print(f"Command is running ...")
        continue
    if command.exit_code == 0:
        print(f"Command {command.command} completed successfully")    
    else:
        print(f"Command {command.command} failed")
    logs = sandbox.process.get_session_command_logs(session_id, response.cmd_id)
    print(f"{logs}")
    break

当命令处于运行中状态时,exit_code 一直等于 None,当 exit_code 为 0 时表示命令运行成功,否则运行失败,再通过 get_session_command_logs() 方法获取命令的完整日志。

有时候我们不想等命令运行结束才打印日志,而是希望实时地输出日志,这对调试和监控都非常有意义。这时我们可以使用 get_session_command_logs_async() 方法:

await sandbox.process.get_session_command_logs_async(
    session_id,
    response.cmd_id,
    lambda chunk: print(f"Log chunk: {chunk}")
)

该方法接受一个 callback 函数,当有日志输出时就回调该函数。

小结

在本文中,我们深入探讨了 Daytona 的代码执行功能,特别是如何在沙箱中有效地执行代码和长耗时任务。我们学习了 Daytona 的 process 属性以及其便捷的 code_run()exec() 方法,这些方法支持环境变量、命令行参数等设置,让我们能够灵活地执行 Python 和 Shell 代码。

此外,对于长耗时任务,我们介绍了如何通过后台会话进行处理,实现非阻塞的命令执行。我们可以创建会话、执行异步命令,并实时获取日志输出,这为任务的监控和调试提供了极大的便利。通过 get_session_command_logs_async() 方法,我们甚至能够实现实时日志的流式处理,进一步增强了我们对代码执行过程的可控性。

以上就是今天的全部内容了,基本上涉及到了和 Daytona 代码执行相关的各个细节,不过还有一点非常重要,那就是代码执行的环境,我们下一篇继续学习。


Daytona 快速入门

在当今的技术浪潮中,人工智能的飞速发展正在重新定义我们的工作方式,尤其是在软件开发领域。随着大模型生成代码能力的不断提升,越来越多的开发者开始探索如何利用这些强大的工具提高工作效率。然而,伴随而来的是一个重要的问题:生成的代码的安全性和可靠性能否得到保障?我们今天要介绍一个新的开源项目 Daytona,就是为了解决这一挑战。

daytona-homepage.png

Daytona 是一个旨在提供安全且弹性的基础设施的 SaaS 服务,专门用于在隔离环境中运行 AI 生成的代码。这样的设计理念不仅能确保代码在执行过程中的安全性,还能为开发者们提供一个稳定和可靠的运行平台。自从 Daytona 在开源社区推出以来,它受到了广泛关注,Github Star 数量已接近 20k,更在 Product Hunt 榜单上夺得了 Top 1 的佳绩。这一切都表明,Daytona 已成为开发者们的新宠,正引领着未来代码生成与执行的潮流。接下来,就让我们快速学习下 Daytona 的核心特性和用法。

核心特性

Daytona 的核心特性如下:

  • 极速基础设施:快速创建沙箱,低于 90 毫秒;
  • 独立与隔离的运行时:以零风险执行 AI 生成的代码,保护你的基础设施;
  • 大规模并行化以支持并发 AI 工作流程:支持沙箱文件系统和内存状态的分叉;

daytona-features.png

此外,还有很多对开发人员友好的特性:

  • 编程控制:支持文件、Git、LSP 和 执行 API;
  • 无限持久化:支持将沙箱设置为永久存在;
  • 原生 SSH 访问:支持通过 SSH 或 IDE 访问沙箱;
  • OCI/Docker 兼容性:支持使用任何 OCI/Docker 镜像来创建沙箱;
  • 开源透明:代码完全开源,支持自托管;

Daytona 被称为 AI 优先基础设施(AI-First Infrastructure),针对大模型、智能体和 AI 评估专门优化,通过 Daytona 可以实现下面这些功能:

  • 代码解释器(Code Interpreter)
  • 代码智能体(Coding Agents)
  • 数据分析(Data Analysis)
  • 数据可视化(Data Visualisation)
  • AI 评估(AI Evaluations)

准备 API Key

为了体验 Daytona 的功能,我们需要做一些环境准备。首先登录 Daytona 官网,注册一个账号:https://app.daytona.io

daytona-signup.png

然后,进入 Dashboard -> Keys 页面,创建一个 API Key:

daytona-create-apikey.png

注意这个 API Key 只会出现一次,务必记下这个 API Key,后面会用到。

然后就可以使用 Daytona 的各种功能了,Daytona 为免费用户提供了 10 个沙箱、20 核 CPU、40G 内存、50G 磁盘,以及 5 个镜像、10G 镜像大小的限额:

daytona-usage.png

使用 Python SDK

接下来我们使用 Python SDK 简单体验下 Daytona 的基础用法。首先安装依赖:

$ pip install daytona-sdk

然后编写代码如下:

from daytona_sdk import Daytona, DaytonaConfig, CreateSandboxParams

# Initialize the Daytona client
daytona = Daytona(DaytonaConfig(api_key=DAYTONA_API_KEY))

# Create the Sandbox instance
sandbox = daytona.create(CreateSandboxParams(language="python"))

# Run code securely inside the Sandbox
response = sandbox.process.code_run('print("Sum of 3 and 4 is " + str(3 + 4))')
if response.exit_code != 0:
    print(f"Error running code: {response.exit_code} {response.result}")
else:
    print(response.result)

# Clean up the Sandbox
daytona.remove(sandbox)

整个代码结构非常清晰,就是四大步:

  1. 初始化 Daytona 客户端:传入刚刚在 Dashboard 页面生成的 API Key;
  2. 创建沙箱:指定沙箱环境为 python 语言,这里也可以传入 typescript,Daytona 也支持执行 JS 代码;
  3. 在沙箱执行代码:这里传入 Python 代码,代码在沙箱中安全地被执行,并返回执行结果;
  4. 销毁沙箱:如果沙箱不需要了,可以销毁掉;也可以不销毁,这时我们可以在 Dashboard -> Sandboxes 页面查看沙箱状态,甚至通过 SSH 进到沙箱环境中排查问题;

daytona-sandboxes.png

最后的运行结果如下:

Sum of 3 and 4 is 7

至此我们对 Daytona 已经有了一个基本了解,下一篇我们将学习更高级的用法。


再学 Supabase 的 GraphQL API

在上一篇文章中我们学习了 Supabase 的 GraphQL API 功能,并通过 Supabase Studio 内置的 GraphiQL IDE 和 curl 简单验证了查询接口。但是,关于 GraphQL API,还有一些点可以展开聊聊,比如如何实现复杂查询,如何实现增删改,如何通过 SDK 调用,等等,我们今天来进一步学习它。

实现复杂查询

下面是一个稍微复杂点的带过滤条件的查询,查询 id=1 的班级:

{
  classesCollection(filter: {id: {eq: 1}}) {
    edges {
      node {
        id
        name
      }
    }
  }
}

其中 classesCollection 表示查询 classes 表,后面的括号里除了加上过滤条件,还可以加上分页、排序等条件。可以在 GraphiQL IDE 右侧的文档中查看 classesCollection 的定义如下:

classes-collection.png

分页

Supabase 的 GraphQL API 支持两种分页方式:键集分页(Keyset Pagination)偏移分页(Offset Pagination)。键集分页是通过游标实现的,使用 firstlastbeforeafter 参数来处理在集合中向前和向后分页,遵循 Relay 的分页规范

我们首先通过 first:10, after:null 查询表中前 10 条记录:

{
  classesCollection(first:10, after:null) {
    pageInfo {
      startCursor,
      endCursor,
      hasNextPage
    }
    edges {
      node {
        id
        name
      }
    }
  }
}

其中 pageInfo 表示查询结果中要带上分页信息,分页信息中的 endCursor 是最后一条记录的游标:

{
  "data": {
    "classesCollection": {
      "edges": [
        {
          "node": {
            "id": 1,
            "name": "一年级一班"
          }
        },
        ...
      ],
      "pageInfo": {
        "endCursor": "WzJd",
        "hasNextPage": true,
        "startCursor": "WzFd"
      }
    }
  }
}

我们将 pageInfo.endCursor 赋值到 after 就可以继续查询下一页:

{
  classesCollection(first:10, after:"WzJd") {
    pageInfo {
      startCursor,
      endCursor,
      hasNextPage
    }
    edges {
      node {
        id
        name
      }
    }
  }
}

偏移分页和传统 SQL 的 limitoffset 类似,使用 firstoffset 参数进行分页,可以跳过结果中的 offset 条记录。下面的查询表示一页 10 条记录,查询第二页:

{
  classesCollection(first:10, offset:10) {
    ...
  }
}

过滤

filter 参数用于设置过滤条件,它的类型为 classesFilter,定义如下:

classes-filter.png

我们可以通过 classes 表的每一个字段进行过滤,也可以通过 and/or/not 逻辑操作符组合过滤条件。当我们根据字段进行过滤时,不同的字段类型对应的过滤类是不一样的,支持的过滤操作也不一样,比如 int 类型对应的 IntFilterstring 类型对应 StringFilter,下面的表格列出了常见的过滤操作符:

filter-operators.png

比如过滤 id 小于 2 的班级:

{
  classesCollection(filter:{id:{lt:2}}) {
    ...
  }
}

过滤名称以 一年级 开头的班级:

{
  classesCollection(filter:{name:{like:"一年级%"}}) {
    ...
  }
}

使用 and 将多个条件组合:

{
  classesCollection(filter:{
    and: [
      {name:{like:"一年级%"}}
      {id: {lt:10}}
    ]  
  }) {
    ...
  }
}

也可以简写成:

{
  classesCollection(filter:{
    name:{like:"一年级%"},
    id: {lt:10}
  }) {
    ...
  }
}

排序

orderBy 参数用于控制排序,它的类型为 classesOrderBy!

classes-orderby.png

支持四种排序方式:

  • AscNullsFirst - 正序,空值靠前
  • AscNullsLast - 正序,空值靠后
  • DescNullsFirst - 倒序,空值靠前
  • DescNullsLast - 倒序,空值靠后

比如下面是按 id 倒序的例子:

{
  classesCollection(orderBy: {id: DescNullsLast}) {
    ...
  }
}

表间关联

和 RESTful API 一样,Supabase 会自动检测表之间的外键关系,下面是一个关联查询的例子,查询 id=1 的班级下的前 10 个学生:

{
  classesCollection(filter: {id: {eq: 1}}) {
    edges {
      node {
        id
        name
        studentsCollection(first: 10) {
          edges {
            node {
              id
              name
            }
          }
        }
      }
    }
  }
}

实现增删改

在上一篇文章中我们了解到,GraphQL 支持三种基本操作:

  • Query(查询):用于从服务器获取数据,客户端通过发送一个查询语句来请求特定的数据,查询语句是一个树状结构,描述了需要获取的数据字段及其子字段;
  • Mutation(变更):用于修改数据,如创建、更新或删除操作,遵循严格的类型定义,负责执行对服务器数据的写操作;
  • Subscription(订阅):用于监听数据变化并实现实时更新,允许客户端实时接收数据更新,通常用于实现实时通信功能;

Supabase 的 GraphQL API 支持 Mutation 操作实现数据的创建、更新或删除。打开右侧 Docs 面板,可以看到支持 Mutation 操作:

mutation.png

通过下面的语句往 classes 表新增两条记录:

mutation {
  insertIntoclassesCollection (
    objects: [
      {name: "二年级一班"},
      {name: "二年级二班"},
    ]
  ) {
    affectedCount
    records {
      id
      name
    }
  }
}

通过下面的语句编辑 classes 表中 id=4 的记录:

mutation {
  updateclassesCollection (
    set: {name: "二年级X班"},
    filter: {id: {eq: 4}}
  ) {
    affectedCount
    records {
      id
      name
    }
  }
}

通过下面的语句删除 classes 表中 id=4 的记录:

mutation {
  deleteFromclassesCollection (
    filter: {id: {eq: 4}}
  ) {
    affectedCount
    records {
      id
      name
    }
  }
}

通过 SDK 调用

Supabase 提供的 SDK,比如 Python SDK 或 JS SDK 等是通过 RESTful API 来实现查询的,要实现 GraphQL API 的调用,可以采用一些 GraphQL JS 框架,比如 RelayApollo 等。

可参考官方的集成文档:

深入 Supabase GraphQL API 原理

我们前面曾提到过,Supabase 的 GraphQL API 是基于 Postgres 扩展 pg_graphql 实现的,它会根据数据库架构自动生成 GraphQL 接口。如果更深入一步,我们会发现,这个扩展是通过内置函数 graphql.resolve(...) 来实现 GraphQL 接口的:当我们请求 GraphQL 接口时,实际上调用的是 graphql.resolve(...) 函数。我们可以打开 SQL Editor 输入下面的查询语句验证下:

select graphql.resolve($$
{
  classesCollection {
    edges {
      node {
        id
        name
      }
    }
  }
}
$$)

运行结果如下:

graphql-resolve.png

对 pg_graphql 的原理感兴趣的同学,可以看官方这篇博客:https://supabase.com/blog/how-pg-graphql-works


学习 Supabase 的 GraphQL API

在前面的文章中,我们知道了 Supabase 使用 PostgREST 将 PostgreSQL 数据库直接转换为 RESTful API,方便开发者通过简单的 HTTP 请求与数据库进行通信,实现增、删、改、查等操作。其实,Supabase 还提供了另一种 GraphQL API 供开发者使用,我们今天就来学习它。

GraphQL 简介

GraphQL 是一种用于 API 的查询语言,由 Facebook 于 2012 年开发并在 2015 年开源。它相比于传统的 RESTful API 提供了一种更高效、强大和灵活的方式来获取和操作数据。

graphql.png

GraphQL 的核心特点如下:

  • 单一端点:与 RESTful API 不同,GraphQL 使用单一端点(通常是 /graphql),客户端通过查询语句指定需要的数据,简化了客户端的实现,减少了 API 管理的复杂性;
  • 精确的数据获取:客户端可以精确请求所需字段,避免了 RESTful API 中常见的 过度获取不足获取 问题,提高了性能和响应速度;
  • 强类型系统:使用强类型的 Schema 定义数据结构,确保客户端和服务器之间的数据交互是安全的,数据的一致性和可靠性得到保障,客户端可以在编译时检查查询的有效性,减少运行时错误;
  • 支持复杂查询:允许嵌套查询,一次请求即可获取多个相关资源,能减少网络往返次数,提高用户体验,适用于处理复杂或经常变化的数据需求;

GraphQL 支持三种基本操作:

  • Query(查询):用于从服务器获取数据,客户端通过发送一个查询语句来请求特定的数据,查询语句是一个树状结构,描述了需要获取的数据字段及其子字段;
  • Mutation(变更):用于修改数据,如创建、更新或删除操作,遵循严格的类型定义,负责执行对服务器数据的写操作;
  • Subscription(订阅):用于监听数据变化并实现实时更新,允许客户端实时接收数据更新,通常用于实现实时通信功能;

开启 Supabase 的 GraphQL 功能

Supabase 的 GraphQL API 是基于 Postgres 扩展 pg_graphql 实现的,它会根据数据库架构自动生成 GraphQL 接口:

  • 支持基本的增删改查操作;
  • 支持表、视图、物化视图和外部表;
  • 支持查询表/视图之间任意深度的关系;
  • 支持用户定义的计算字段;
  • 支持 Postgres 的安全模型,包括行级安全、角色和权限。

进入 Dashboard -> Database -> Extension 页面,确认下 pg_graphql 扩展是否开启(默认是开启的):

database-extensions.png

GraphQL 初体验

如果是第一次使用 Supabase 的 GraphQL 功能,推荐通过 Supabase Studio 内置的 GraphiQL IDE 来调试验证。首先进入 Dashborad -> Integrations -> GraphQL 页面:

integration-graphql-overview.png

点击 GraphiQL 页签,在查询编辑器中输入 GraphQL 语句,并点击绿色图标发送请求,查询结果显示在编辑器的右侧区域:

integration-graphql-graphiql.png

这里使用的查询语句是:

{
  classesCollection {
    edges {
      node {
        id
        name
      }
    }
  }
}

表示查询我们之前创建的 classes 表,查询结果要包含 idname 两个字段,查询结果如下:

{
  "data": {
    "classesCollection": {
      "edges": [
        {
          "node": {
            "id": 1,
            "name": "一年级一班"
          }
        },
        {
          "node": {
            "id": 2,
            "name": "一年级二班"
          }
        }
      ]
    }
  }
}

下面是一个更复杂的查询,查询 id=1 的班级下的前 10 个学生:

{
  classesCollection(filter: {id: {eq: 1}}) {
    edges {
      node {
        id
        name
        studentsCollection(first: 10) {
          edges {
            node {
              id
              name
            }
          }
        }
      }
    }
  }
}

另外,还可以通过 Mutation 请求类型实现数据的增删改,关于 Supabase 的 GraphQL API 接口规范和核心概念,可以参考这里的 官方文档,也可以点击旁边的 Docs 按钮查看支持的 API 接口类型:

integration-graphql-docs.png

GraphQL API

Supabase GraphQL API 请求格式如下:

POST https://<PROJECT-REF>.supabase.co/graphql/v1
Content-Type: application/json
apiKey: <API-KEY>

{"query": "<QUERY>", "variables": {}}

其中,<PROJECT-REF> 是 Supabase 项目的 ID,请求头中的 <API-KEY> 是 API 密钥,都可以在 Supabase 项目的 API 设置中找到。请求体中的 <QUERY> 是查询语句,也就是上面我们在 GraphiQL IDE 中输入的 GraphQL 语句。

下面通过 curl 来查询所有的班级:

$ curl -X POST https://lsggedvvakgatnhfehlu.supabase.co/graphql/v1 \
    -H 'apiKey: <API-KEY>' \
    -H 'Content-Type: application/json' \
    --data-raw '{"query": "{ classesCollection { edges { node { id, name } } } }", "variables": {}}'

学习 Supabase 的关联查询

Supabase 本质上是 Postgres 数据库的一个封装。由于 Postgres 是一个关系数据库,所以处理表之间的关系是一种非常常见的情况。Supabase 会通过外键自动检测表之间的关系,在调用 RESTful API 或 SDK 时,提供了便捷的语法实现跨表查询。

一对多关系

班级和学生可以是一对多关系,一个班级可以有多个学生,一个学生只能属于一个班级。

classes-students.png

我们创建两个表:

create table classes (
  id serial primary key,
  name text not null
);

create table students (
  id serial primary key,
  name text not null,
  age int not null,
  class_id integer references classes(id)
);

再准备一些测试数据:

insert into classes (name) values 
('一年级一班'),
('一年级二班');
insert into students (name, age, class_id) values 
('张三', 7, 1), 
('李四', 8, 1), 
('王五', 8, 2), 
('赵六', 7, 2);

我们通过下面的代码查询所有班级:

response = (
    supabase.table("classes")
    .select("name")
    .execute()
)

如果要查询所有班级以及该班级的学生,可以这样:

response = (
    supabase.table("classes")
    .select("name, students(name, age)")
    .execute()
)

在上面的查询中 classes 为主表,students 为从表。Supabase 会自动检测它们之间的关系,知道它们是一对多,因此返回 students 时会返回一个数组:

[
    {
        'name': '一年级一班', 
        'students': [
            {'age': 7, 'name': '张三'}, 
            {'age': 8, 'name': '李四'}
        ]
    }, 
    {
        'name': '一年级二班', 
        'students': [
            {'age': 8, 'name': '王五'}, 
            {'age': 7, 'name': '赵六'}
        ]
    }
]

我们也可以对主表和从表进行过滤,比如只查询一年级一班以及该班级的学生:

response = (
    supabase.table("classes")
    .select("name, students(name, age)")
    .eq("name", "一年级一班")
    .execute()
)

或者查询所有班级以及该班级年龄为7岁的学生:

response = (
    supabase.table("classes")
    .select("name, students(name, age)")
    .eq("students.age", 7)
    .execute()
)

多对一关系

上面的主表和从表可以反过来,让 students 为主表,classes 为从表,比如查询所有学生以及该学生的班级:

response = (
    supabase.table("students")
    .select("name, age, classes(name)")
    .execute()
)

这时 Supabase 会自动检测出主表和从表之间是多对一关系,因此返回的 classes 是一个对象:

[
    {'name': '张三', 'age': 7, 'classes': {'name': '一年级一班'}},
    {'name': '李四', 'age': 8, 'classes': {'name': '一年级一班'}}, 
    {'name': '王五', 'age': 8, 'classes': {'name': '一年级二班'}}, 
    {'name': '赵六', 'age': 7, 'classes': {'name': '一年级二班'}}
] 

多对多关系

接着我们再来看下多对多关系。我们新建一个老师表,老师和班级是多对多关系,一个老师可以教多个班级,一个班级也可以有多个老师。

create table teachers (
  id serial primary key,
  name text not null
);

多对多关系不能直接在两个表之间用外键约束进行表达,要表达多对多关系,需要一张额外的表,该表需要包含两个外键约束,分别关联到不同的表:

create table classes_teachers (
  class_id integer references classes(id),
  teacher_id integer references teachers(id),
  primary key (class_id, teacher_id)
);

它们之间的关系如下图所示:

classes-teachers.png

我们再准备一些测试数据:

insert into teachers (name) values 
('张老师'), 
('李老师'), 
('王老师'), 
('赵老师');
insert into classes_teachers (class_id, teacher_id) values 
(1, 1), 
(1, 2), 
(1, 4), 
(2, 1), 
(2, 3), 
(2, 4);

从上面的建表语句可以看出,classesteachers 之间没有直接的外键关联关系,而是通过 classes_teachers 表进行关联,Supabase 会自动识别这种情况,我们在编写代码时,可以直接关联 classesteachers 表。比如查询所有班级以及该班级的老师:

response = (
    supabase.table("classes")
    .select("name, teachers(name)")
    .execute()
)

查询结果如下:

[
    {
        'name': '一年级一班', 
        'teachers': [
            {'name': '张老师'}, 
            {'name': '李老师'}, 
            {'name': '赵老师'}
        ]
    }, 
    {
        'name': '一年级二班', 
        'teachers': [
            {'name': '张老师'}, 
            {'name': '王老师'}, 
            {'name': '赵老师'}
        ]
    }
]

也可以反过来,查询所有老师以及该老师的班级:

response = (
    supabase.table("teachers")
    .select("name, classes(name)")
    .execute()
)

查询结果如下:

[
    {
        'name': '张老师', 
        'classes': [
            {'name': '一年级一班'}, 
            {'name': '一年级二班'}
        ]
    }, 
    {
        'name': '李老师', 
        'classes': [
            {'name': '一年级一班'}
        ]
    }, 
    {
        'name': '王老师', 
        'classes': [
            {'name': '一年级二班'}
        ]
    }, 
    {
        'name': '赵老师', 
        'classes': [
            {'name': '一年级一班'}, 
            {'name': '一年级二班'}
        ]
    }
]

可以看到 Supabase 会自动根据关联表 classes_teachers 检测出这两个表是多对多关系,在这种情况下,无论哪个表是主表,从表返回的都是一个数组。

一对一关系

表和表之间还有一种特殊的一对一关系,比如老师表和老师档案表,老师表存储老师的基本信息,老师档案表存储老师的详细信息,每个老师只能有一份老师档案。

teachers-profiles.png

我们创建老师档案表如下:

create table teacher_profiles (
  id serial primary key,
  teacher_id integer references teachers(id) unique,
  address text not null,
  phone text not null
);

注意这里的 teacher_id 字段,它是一个外键,关联到 teachers 表,同时它又是一个唯一约束,这说明一个老师只能有一份老师档案。如果没有这个唯一约束,那么老师和老师档案表之间就是多对一关系了。

以及一些测试数据:

insert into teacher_profiles (teacher_id, address, phone) values 
(1, '北京市朝阳区', '13800000000'), 
(2, '上海市浦东新区', '13900000000'), 
(3, '广州市天河区', '13700000000'), 
(4, '深圳市南山区', '13600000000');

我们查询所有老师以及该老师的档案信息:

response = (
    supabase.table("teachers")
    .select("name, teacher_profiles(address, phone)")
    .execute()
)

查询结果如下:

[
    {
        'name': '张老师', 
        'teacher_profiles': {
            'phone': '13800000000', 
            'address': '北京市朝阳区'
        }
    }, 
    ...
]

可以看到 Supabase 自动检测出老师和老师档案表之间是一对一关系,因此返回的 teacher_profiles 是一个对象。

如果反过来,查询所有老师档案以及对应的老师:

response = (
    supabase.table("teacher_profiles")
    .select("address, phone, teachers(name)")
    .execute()
)

查询结果中的 teachers 也是一个对象:

[
    {
        'address': '北京市朝阳区', 
        'phone': '13800000000', 
        'teachers': {'name': '张老师'}
    }, 
    ...
]

两个表之间有多个外键关联

还有一种特殊情况,两个表之间有多个外键关联,比如订单表和地址表,订单表中有一个发货地址和一个收货地址,它们都关联到地址表。

建表语句如下:

create table addresses (
  id serial primary key,
  name text not null
);

create table orders (
  id serial primary key,
  shipping_address_id integer references addresses(id),
  receiving_address_id integer references addresses(id)
);

插入示例数据:

insert into addresses (name) values 
('北京市朝阳区'), 
('上海市浦东新区'), 
('广州市天河区'), 
('深圳市南山区');
insert into orders (shipping_address_id, receiving_address_id) values 
(1, 2), 
(2, 3), 
(3, 4), 
(4, 1);

如果我们要查询出所有订单以及发货地址和收货地址,在查询语句中指定 addresses(name) 就不行了,因为 Supabase 不知道是查询发货地址还是收货地址,所以需要在查询语句中指定外键字段名:

response = (
    supabase.table("orders")
    .select("""
        id, 
        shipping_address_id(name), 
        receiving_address_id(name)
    """)
    .execute()
)

不过这样查询出来的结果中,发货地址和收货地址的字段名是 shipping_address_idreceiving_address_id

[
    {
        'id': 1, 
        'shipping_address_id': {'name': '北京市朝阳区'}, 
        'receiving_address_id': {'name': '上海市浦东新区'}
    }, 
    ...
]

为了方便,我们可以在查询语句中指定别名:

response = (
    supabase.table("orders")
    .select("""
        id, 
        shipping:shipping_address_id(name), 
        receiving:receiving_address_id(name)
    """)
    .execute()
)

这样查询出来的结果中,发货地址和收货地址的字段名就变成了 shippingreceiving

[
    {
        'id': 1, 
        'shipping': {'name': '北京市朝阳区'}, 
        'receiving': {'name': '上海市浦东新区'}
    }, 
    ...
]

参考


学习 Supabase 的过滤器

在上一篇文章中,我们通过 Python SDK 实现了 Supabase 数据库的增删改查,在查询、修改和删除数据时,我们使用了类似于 eqin_ 这样的过滤方法,这被称为 过滤器(filters)

supabase-filters.jpg

Supabase 提供了丰富的过滤器,可以满足各种需求,今天我们来详细了解一下 Supabase 的过滤器。

基本用法

下面是 Supabase Python SDK 中的过滤器的基本用法:

response = (
    supabase.table("students")
    .select("*")
    .eq("name", "zhangsan")
    .execute()
)

Supabase 常用的过滤器包括:

过滤器描述示例
eq等于.eq("id", 15)
neq不等于.neq("id", 15)
gt大于.gt("age", 18)
gte大于等于.gte("age", 18)
lt小于.lt("age", 18)
lte小于等于.lte("age", 18)
like模糊匹配.like("name", "zhang%")
ilike模糊匹配,不区分大小写.ilike("name", "zhang%")
is_是否满足某种条件,比如是否为 NULL.is_("name", "null")
not_对某个过滤器取反.not_.is_("name", "null")
in_在列表中.in_("id", [1, 2, 3])

范围列和数组列

在 Supabase 中,范围列用于存储数值范围,这种类型的列可以表示一个区间,例如 [1, 10] 表示从 1 到 10 的范围;数组列用于存储数组,例如 [1, 2, 3] 表示一个数组。我们创建一个示例表,包括范围列和数组列:

CREATE TABLE examples (
    id SERIAL PRIMARY KEY,
    range_column INT4RANGE, -- 范围列
    array_column INT[] -- 数组列
);

并开启 RLS:

ALTER TABLE examples
ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Enable all access"
ON "public"."examples"
FOR ALL
USING (true);

然后插入几条示例数据:

response = (
    supabase.table("examples")
    .insert([
        {"range_column": [1, 5], "array_column": [1, 2, 3, 4, 5]},
        {"range_column": [6, 10], "array_column": [6, 7, 8, 9, 10]},
    ])
    .execute()
)

范围列和数组列支持一些特殊的过滤器,包括:

过滤器描述示例
contains数组中包含所有元素.contains("array_column", ["1", "2", "3"])
contained_by数组中所有元素被包含.contained_by("array_column", ["1", "2", "3", "4", "5", "6"])
range_gt范围大于,所有元素都大于范围内的值.range_gt("range_column", [1, 5])
range_gte范围大于等于,所有元素都大于等于范围内的值.range_gte("range_column", [1, 5])
range_lt范围小于,所有元素都小于范围内的值.range_lt("range_column", [6, 10])
range_lte范围小于等于,所有元素都小于等于范围内的值.range_lte("range_column", [6, 10])
range_adjacent范围相邻且互斥.range_adjacent("range_column", [10, 15])
overlaps数组中含有重叠元素.overlaps("array_column", ["1", "3", "5"])

JSON 列

Supabase 支持 JSON 列,可以存储 JSON 格式的数据,JSON 列有两种类型:

  • JSON - 作为字符串存储
  • JSONB - 作为二进制存储

在几乎所有情况下,推荐使用 JSONB 类型,我们创建一个示例表,包括 JSONB 列:

CREATE TABLE examples2 (
    id SERIAL PRIMARY KEY,
    json_column JSONB
);

和之前一样,开启 RLS:

ALTER TABLE examples2
ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Enable all access"
ON "public"."examples2"
FOR ALL
USING (true);

然后插入几条示例数据:

response = (
    supabase.table("examples2")
    .insert([
        {"json_column": {"name": "zhangsan", "age": 15}},
        {"json_column": {"name": "lisi", "age": 16}},
    ])
    .execute()
)

我们可以在查询时,使用 -> 操作符来获取 JSON 中的某个字段:

response = (
    supabase.table("examples2")
    .select("id, json_column->name, json_column->age")
    .execute()
)

我们也可以在过滤器中使用 -> 操作符:

response = (
    supabase.table("examples2")
    .select("*")
    .eq("json_column->age", 15)
    .execute()
)

如果要过滤的字段值是字符串类型,可以使用 ->> 操作符:

response = (
    supabase.table("examples2")
    .select("*")
    .eq("json_column->>name", "zhangsan")
    .execute()
)

复合过滤器

Supabase 支持复合过滤器,可以将多个过滤器组合在一起,比如:

response = (
    supabase.table("students")
    .select("*")
    .gt("age", 15)
    .lt("age", 18)
    .execute()
)

这个表示 age 大于 15 且小于 18 的数据,如果要表示 age 小于等于 15 或者大于等于 18 的数据,可以使用 or_ 过滤器:

response = (
    supabase.table("students")
    .select("*")
    .or_("age.lte.15,age.gte.18")
    .execute()
)

or_ 过滤器的参数是一个字符串,使用的是原始的 PostgREST 语法,格式为 column.operator.value,多个过滤器之间用逗号分隔。

当我们希望同时匹配多个字段时,可以使用 match 过滤器:

response = (
    supabase.table("students")
    .select("*")
    .match({"age": 15, "name": "zhangsan"})
    .execute()
)

这个表示 age 等于 15 且 name 等于 zhangsan 的数据,match 和多个 eq 是等价的:

response = (
    supabase.table("students")
    .select("*")
    .eq("age", 15)
    .eq("name", "zhangsan")
    .execute()
)

使用 Supabase Python SDK 实现数据库的增删改查

在之前的文章中,我们学习了 RESTful API 来操作 Supabase 数据库,但是调用方式比较繁琐,其实,Supabase 还提供了多种编程语言的客户端库,可以更方便地操作数据库,包括 JavaScript、Flutter、Swift、Python、C# 和 Kotlin 等。

supabase-client-libraries.png

我们今天继续学习 Supabase,使用 Python SDK 来实现数据库的增删改查。

Python SDK 快速上手

首先安装所需的依赖:

$ pip install dotenv supabase

然后创建一个 .env 文件,写入 Supabase 项目地址和密钥:

SUPABASE_URL=https://lsggedvvakgatnhfehlu.supabase.co
SUPABASE_KEY=<ANON_KEY>

这两个值可以在 Supabase 项目的 API 设置中找到:

supabase-api-settings.png

然后通过 dotenv 加载:

from dotenv import load_dotenv
import os

# Load environment variables from .env
load_dotenv()

# Fetch variables
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")

然后就可以通过 supabase 库的 create_client 方法创建客户端,访问和操作数据库了:

from supabase import create_client, Client
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
response = (
    supabase.table("students")
    .select("*")
    .execute()
)
print(response)

增删改查

Supabase Python SDK 提供了 table 方法来操作表,然后通过 insertupdatedeleteselect 方法来实现增删改查。

新增数据:

response = (
    supabase.table("students")
    .insert({"name": "zhangsan", "age": 18})
    .execute()
)

其中 insert 也可以接收一个列表,批量插入数据:

response = (
    supabase.table("students")
    .insert([
        {"name": "zhangsan", "age": 18},
        {"name": "lisi", "age": 20},
    ])
    .execute()
)

修改数据:

response = (
    supabase.table("students")
    .update({"age": 20})
    .eq("name", "zhangsan")
    .execute()
)

Supabase 也提供了 upsert 方法,根据主键判断,如果数据不存在,则插入,如果存在,则更新:

response = (
    supabase.table("students")
    .upsert({"id": 15, "name": "zhangsan", "age": 20})
    .execute()
)

删除数据:

response = (
    supabase.table("students")
    .delete()
    .eq("id", 15)
    .execute()
)

当有多个 id 需要删除时,可以使用 in_ 方法批量删除:

response = (
    supabase.table("students")
    .delete()
    .in_("id", [11, 12, 13, 14])
    .execute()
)

查询数据:

response = (
    supabase.table("students")
    .select("*")
    .execute()
)

其中 select("*") 用于查询所有字段,也可以指定查询的字段:

response = (
    supabase.table("students")
    .select("name, age")
    .execute()
)

使用 Supabase REST API 实现数据库的增删改查

在上一篇文章中,我们学习了如何在 Supabase 中创建项目和数据库,并知道了 Supabase 本质上就是 PostgreSQL 数据库,所以可以通过 psycopg2 库来连接和操作它。另外,Supabase 还提供了 RESTful API,方便我们通过 HTTP 请求来操作数据库。

supabase-rest-api.png

我们今天继续学习 Supabase,使用 RESTful API 来实现数据库的增删改查。

PostgREST

PostgREST 是一个开源的 Web 服务器,用于将 PostgreSQL 数据库直接转换为 RESTful API,方便开发者通过简单的 HTTP 请求与数据库进行通信,实现增、删、改、查等操作。

postgrest.png

Supabase 的 REST API 就是基于 PostgREST 开发的,我们可以通过 RESTful API 来操作 Supabase 数据库。

RESTful API

Supabase REST API 的 URL 格式如下:

https://<project-ref>.supabase.co/rest/v1/<table-name>

其中,<project-ref> 是 Supabase 项目的 ID,<table-name> 是数据库表的名称。

请求中需要带上 apikey 请求头,<project-ref>apikey 都可以在 Supabase 项目的 API 设置中找到:

supabase-api-settings.png

在 Project API Keys 中可以看到 anonservice_role 两个密钥,这两个密钥都可以用于访问 Supabase REST API,但是它们的权限不同:anon 是受限的,所有操作都受到 RLS 的约束,因此可以安全地公开;而 service_role 可以绕过 RLS 限制,访问数据库中的所有表和数据,因此需要高度保密,应仅在服务器端或安全环境中使用。关于这两个密钥的详细说明,可以参考 Understanding API Keys

下面通过 curl 来查询 students 表中的所有数据:

$ curl 'https://lsggedvvakgatnhfehlu.supabase.co/rest/v1/students' \
    -H "apikey: <ANON_KEY>"

Row Level Security

如果你第一次尝试访问上面的接口,你会发现返回的结果是空数组,这是因为我们在创建数据表的时候, Supabase 默认会启用 Row Level Security,即行级安全,它会根据请求头中的 apikey 来判断用户是否有权限访问数据:

supabase-rls.png

我们可以打开 SQL Editor,执行如下 SQL 语句为 students 表创建一个匿名访问的策略:

-- Allow anonymous access
create policy "Allow public access"
  on students
  for select
  to anon
  using (true);

我们也可以在 Authentication - Policies 页面手动创建:

supabase-rls-policy.png

再次访问上面的接口,就可以看到返回的数据了。

增删改查

Supabase REST API 和 PostgREST API 一致,支持以下 HTTP 方法:

  • GET:获取数据
  • POST:插入数据
  • PUT:更新数据
  • PATCH:部分更新数据
  • DELETE:删除数据

在开始之前,我们需要为 students 表创建一个策略,允许 anon 对数据进行增删改:

-- Allow anonymous write
create policy "Allow public write"
  on students
  for all
  to anon
  using (true);

否则可能会报权限错误:

{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"students\""}

插入数据:

$ curl -X POST 'https://lsggedvvakgatnhfehlu.supabase.co/rest/v1/students' \
    -H "apikey: <ANON_KEY>" \
    -H "Content-Type: application/json" \
    -d '{"name": "zhangsan", "age": 18}'

获取满足条件的数据:

$ curl -X GET 'https://lsggedvvakgatnhfehlu.supabase.co/rest/v1/students?id=eq.3' \
    -H "apikey: <ANON_KEY>"

其中 id=eq.3 是 PostgREST 特有的过滤器语法,表示查询 id 等于 3 的数据,它还支持更多的过滤器,如下图所示:

supabase-filter-operators.png

更新数据:

$ curl -X PUT 'https://lsggedvvakgatnhfehlu.supabase.co/rest/v1/students?id=eq.10' \
    -H "apikey: <ANON_KEY>" \
    -H "Content-Type: application/json" \
    -d '{"id": 10, "name": "zhangsan", "age": 19}'

注意 id=eq.10 和请求体中的 id 必须一致,否则会报错:

{"code":"PGRST115","details":null,"hint":null,"message":"Payload values do not match URL in primary key column(s)"}

部分更新数据:

$ curl -X PATCH 'https://lsggedvvakgatnhfehlu.supabase.co/rest/v1/students?id=eq.10' \
    -H "apikey: <ANON_KEY>" \
    -H "Content-Type: application/json" \
    -d '{"id": 10, "age": 20}'

部分更新和更新的区别在于,部分更新只更新请求体中指定的字段,而更新会更新所有字段。

删除数据:

$ curl -X DELETE 'https://lsggedvvakgatnhfehlu.supabase.co/rest/v1/students?id=eq.10' \
    -H "apikey: <ANON_KEY>" \
    -H "Content-Type: application/json"