Fork me on GitHub

分类 Supabase 下的文章

再学 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"

Supabase 快速入门

今天给大家介绍一个开源的 后端即服务(BaaS) 平台 Supabase,它被认为是 Firebase 的替代品,旨在帮助开发者更快地构建产品,让开发者可以专注于前端开发,而无需花费大量时间和精力来构建和维护后端基础设施。

supabase-homepage.png

Supabase 的核心功能如下:

  • 数据库:使用 PostgreSQL 作为数据库,支持 SQL 和 RESTful API 访问。它会自动为每个表生成 RESTful API,方便开发者通过简单的 HTTP 请求与数据库进行通信,实现增、删、改、查等操作。
  • 认证系统:提供完整的认证体系,支持邮箱、手机号、第三方服务(如谷歌、苹果、Twitter、Facebook、Github、Azure、Gitlab 和 Bitbucket 等)等多种登录方式,还支持像 SAML 这样的企业登录,并且可以轻松管理用户注册和登录流程。
  • 实时订阅:允许通过 WebSocket 实现实时数据同步,开发者可以订阅数据库的变更,如插入、更新和删除操作,从而创建实时应用,如聊天应用或协作工具等,用户能在操作发生时立即看到更新。
  • 存储服务:提供对象存储服务,可方便地上传、下载和管理文件,适用于图片、视频等各种文件类型,并具有较高的可扩展性。
  • 边缘函数:支持在边缘节点上运行 JavaScript 函数,可用于处理请求或触发事件。

本文学习和实践 Supabase 的数据库功能,并结合不同用例给出代码实例。

创建 Supabase 项目

首先,我们需要访问 Supabase 官网,并创建一个账户,然后进入 Dashboard 页面,创建新项目:

supabase-create-project.png

项目创建后,进入项目概览页:

supabase-project-overview.png

此时,我们就可以创建表以及准备数据了。可以看到 Supabase 提供了 Table Editor 和 SQL Editor 两个工具,Table Editor 通过像电子表格一样的可视化页面管理数据,SQL Editor 则是通过 SQL 语句管理数据。

创建表和数据

我们通过 Table Editor 创建一个 students 表:

supabase-create-table.png

再通过 SQL Editor 插入一些数据:

INSERT INTO students (name, age)
VALUES
  ('John Doe', 15),
  ('Jane Doe', 16),
  ('Bob Smith', 14),
  ('Alice Johnson', 17);

访问并操作数据库

操作 Supabase 和操作普通 PostgreSQL 数据库一样,我们点击项目顶部的 Connect 按钮,可以看到数据库的连接信息:

connect-to-your-project.png

然后创建一个 .env 文件,将连接信息写进去:

host=db.lsggedvvakgatnhfehlu.supabase.co
port=5432
database=postgres
user=postgres
password=[YOUR-PASSWORD]

这些信息可以通过 dotenv 加载:

from dotenv import load_dotenv
import os

# Load environment variables from .env
load_dotenv()

# Fetch variables
USER = os.getenv("user")
PASSWORD = os.getenv("password")
HOST = os.getenv("host")
PORT = os.getenv("port")
DBNAME = os.getenv("dbname")

然后就可以通过 psycopg2 访问和操作数据库了:

import psycopg2

# Connect to the database
try:
    connection = psycopg2.connect(
        user=USER,
        password=PASSWORD,
        host=HOST,
        port=PORT,
        dbname=DBNAME
    )
    print("Connection successful!")
    
    # Create a cursor to execute SQL queries
    cursor = connection.cursor()
    
    # Example query
    cursor.execute("SELECT * FROM students;")
    result = cursor.fetchone()
    print("Query result:", result)

    # Close the cursor and connection
    cursor.close()
    connection.close()
    print("Connection closed.")

except Exception as e:
    print(f"Failed to connect: {e}")

xqq.jpg