# NexBase
**Elixir 版的 Supabase 客户端** - 一个流畅的 PostgreSQL 查询构建器,灵感来自 Supabase 的 JavaScript 客户端。
用简洁、可链式的 API 构建类型安全的数据库查询。
## ✨ 特性
- 🔗 **流畅 API** - 链式调用,代码可读性强
- 🛡️ **类型安全** - 基于 Ecto,完全类型安全
- 📝 **PostgreSQL 优先** - 针对 PostgreSQL 优化
- 🎯 **极简设计** - 无魔法,直观易用
- 🚀 **生产就绪** - 基于久经考验的 Ecto
- � **Schema-less** - 支持无 schema 的动态表查询
## 📦 安装
在 `mix.exs` 中添加依赖:
```elixir
def deps do
[
{:nex_base, "~> 0.1.0"}
]
end
```
运行 `mix deps.get`。
## 🚀 快速开始
### 1. 初始化客户端
假设你已经有一个 Ecto Repo(如 `MyApp.Repo`),只需创建客户端:
```elixir
# 在模块属性中初始化客户端
@client = NexBase.client(repo: MyApp.Repo)
```
### 2. 构建查询
```elixir
# 查询
{:ok, users} = @client
|> NexBase.from("users")
|> NexBase.select(["id", "name", "email"])
|> NexBase.eq(:active, true)
|> NexBase.order(:created_at, :desc)
|> NexBase.limit(10)
|> NexBase.run()
# 插入
{:ok, result} = @client
|> NexBase.from("users")
|> NexBase.insert(%{name: "John", email: "john@example.com"})
|> NexBase.run()
# 更新
{:ok, result} = @client
|> NexBase.from("users")
|> NexBase.eq(:id, 123)
|> NexBase.update(%{name: "Jane"})
|> NexBase.run()
# 删除
{:ok, result} = @client
|> NexBase.from("users")
|> NexBase.eq(:id, 123)
|> NexBase.delete()
|> NexBase.run()
```
## 📚 API 参考
### 基础操作
| 方法 | 说明 | 示例 |
|------|------|------|
| `from(table)` | 指定表名 | `.from("users")` |
| `select(fields)` | 选择字段 | `.select(["id", "name"])` |
| `insert(data)` | 插入数据 | `.insert(%{name: "John"})` |
| `update(data)` | 更新数据 | `.update(%{name: "Jane"})` |
| `delete()` | 删除数据 | `.delete()` |
| `run()` | 执行查询 | `.run()` |
### 过滤器
| 方法 | 说明 | 示例 |
|------|------|------|
| `eq(col, val)` | 等于 | `.eq(:status, "active")` |
| `neq(col, val)` | 不等于 | `.neq(:status, "deleted")` |
| `gt(col, val)` | 大于 | `.gt(:score, 90)` |
| `gte(col, val)` | 大于等于 | `.gte(:age, 18)` |
| `lt(col, val)` | 小于 | `.lt(:price, 100)` |
| `lte(col, val)` | 小于等于 | `.lte(:quantity, 50)` |
| `like(col, pattern)` | 模糊匹配(区分大小写) | `.like(:name, "%john%")` |
| `ilike(col, pattern)` | 模糊匹配(不区分大小写) | `.ilike(:email, "%@gmail%")` |
| `in(col, values)` | 包含在列表中 | `.in(:status, ["active", "pending"])` |
| `is(col, val)` | IS NULL / IS TRUE / IS FALSE | `.is(:deleted_at, nil)` |
### 排序和分页
| 方法 | 说明 | 示例 |
|------|------|------|
| `order(col, direction)` | 排序 | `.order(:created_at, :desc)` |
| `limit(n)` | 限制结果数 | `.limit(10)` |
| `offset(n)` | 跳过前 N 条 | `.offset(20)` |
### 原始 SQL
```elixir
# 执行原始 SQL 查询
{:ok, result} = @client |> NexBase.query("SELECT * FROM users WHERE id = $1", [1])
# 使用 query!(失败时抛出异常)
result = @client |> NexBase.query!("SELECT version()", [])
```
## 💡 使用示例
### 完整的 CRUD 示例
```elixir
# 初始化客户端
client = NexBase.client(repo: MyApp.Repo)
# CREATE - 创建新用户
{:ok, user} = client
|> NexBase.from("users")
|> NexBase.insert(%{
name: "Alice",
email: "alice@example.com",
age: 25
})
|> NexBase.run()
# READ - 查询用户
{:ok, users} = client
|> NexBase.from("users")
|> NexBase.eq(:age, 25)
|> NexBase.order(:created_at, :desc)
|> NexBase.run()
# users 是一个列表,每个元素是一个 Map
# [
# %{"id" => 1, "name" => "Alice", "email" => "alice@example.com", "age" => 25},
# %{"id" => 2, "name" => "Bob", "email" => "bob@example.com", "age" => 25}
# ]
# 获取第一个用户
[first_user | _] = users
first_user["name"] # => "Alice"
first_user["email"] # => "alice@example.com"
# 或者使用 Enum 遍历
Enum.each(users, fn user ->
IO.puts("#{user["name"]}: #{user["email"]}")
end)
# UPDATE - 更新用户
{:ok, _} = client
|> NexBase.from("users")
|> NexBase.eq(:email, "alice@example.com")
|> NexBase.update(%{age: 26})
|> NexBase.run()
# DELETE - 删除用户
{:ok, _} = client
|> NexBase.from("users")
|> NexBase.eq(:email, "alice@example.com")
|> NexBase.delete()
|> NexBase.run()
```
### 复杂查询
```elixir
# 多条件查询
{:ok, results} = client
|> NexBase.from("orders")
|> NexBase.eq(:status, "completed")
|> NexBase.gt(:total, 100)
|> NexBase.lt(:created_at, DateTime.utc_now())
|> NexBase.in(:category, ["electronics", "books"])
|> NexBase.order(:created_at, :desc)
|> NexBase.limit(20)
|> NexBase.run()
# 模糊搜索
{:ok, results} = client
|> NexBase.from("products")
|> NexBase.ilike(:name, "%laptop%")
|> NexBase.gte(:price, 500)
|> NexBase.run()
# 分页
{:ok, page1} = client
|> NexBase.from("users")
|> NexBase.order(:id, :asc)
|> NexBase.limit(10)
|> NexBase.offset(0)
|> NexBase.run()
{:ok, page2} = client
|> NexBase.from("users")
|> NexBase.order(:id, :asc)
|> NexBase.limit(10)
|> NexBase.offset(10)
|> NexBase.run()
```
## 🏗️ 架构设计
### 客户端模式
NexBase 采用 **客户端模式**,类似于 Supabase:
```elixir
# 在模块属性中初始化客户端
defmodule MyApp.Pages.Users do
use Nex
@client NexBase.client(repo: MyApp.Repo)
def mount(_params) do
{:ok, users} = @client
|> NexBase.from("users")
|> NexBase.run()
%{users: users}
end
end
```
### Query 结构体
内部使用 `NexBase.Query` 结构体存储查询状态:
```elixir
%NexBase.Query{
table: "users",
select: ["id", "name"],
filters: [
{:eq, :status, "active"},
{:gt, :age, 18}
],
order_by: [{:desc, :created_at}],
limit: 10,
offset: 0,
repo: MyApp.Repo
}
```
## 🔒 错误处理
所有操作都返回 `{:ok, result}` 或 `{:error, reason}`:
```elixir
case client
|> NexBase.from("users")
|> NexBase.eq(:id, 123)
|> NexBase.update(%{name: "Updated"})
|> NexBase.run() do
{:ok, result} ->
IO.puts("更新成功")
{:error, reason} ->
IO.puts("更新失败: #{inspect(reason)}")
end
```
使用 `query!` 和 `run!` 在失败时抛出异常:
```elixir
# 失败时抛出异常
result = client
|> NexBase.from("users")
|> NexBase.eq(:id, 123)
|> NexBase.run!()
```
## 🎯 最佳实践
### 1. 在模块属性中初始化客户端
```elixir
defmodule MyApp.Users do
@client NexBase.client(repo: MyApp.Repo)
def list_active do
@client
|> NexBase.from("users")
|> NexBase.eq(:active, true)
|> NexBase.run()
end
end
```
### 2. 使用原子作为列名
```elixir
# ✅ 推荐
.eq(:status, "active")
# ❌ 避免
.eq("status", "active")
```
### 3. 链式调用保持可读性
```elixir
# ✅ 好
{:ok, users} = client
|> NexBase.from("users")
|> NexBase.eq(:active, true)
|> NexBase.order(:created_at, :desc)
|> NexBase.limit(10)
|> NexBase.run()
# ❌ 差
{:ok, users} = client |> NexBase.from("users") |> NexBase.eq(:active, true) |> NexBase.order(:created_at, :desc) |> NexBase.limit(10) |> NexBase.run()
```
### 4. 处理错误
```elixir
# ✅ 推荐
case client |> NexBase.from("users") |> NexBase.run() do
{:ok, users} -> users
{:error, reason} -> handle_error(reason)
end
# ❌ 避免(除非确定不会失败)
{:ok, users} = client |> NexBase.from("users") |> NexBase.run()
```
## 🔄 高级功能
### RPC 调用(存储过程)
```elixir
# 调用数据库函数
{:ok, result} = NexBase.rpc("my_function", %{param1: "value1", param2: 123}, repo: MyApp.Repo)
```
### 批量操作
```elixir
# 批量插入
data = [
%{name: "Alice", email: "alice@example.com"},
%{name: "Bob", email: "bob@example.com"},
%{name: "Charlie", email: "charlie@example.com"}
]
{:ok, result} = client
|> NexBase.from("users")
|> NexBase.insert(data)
|> NexBase.run()
# Upsert(插入或更新)
{:ok, result} = client
|> NexBase.from("users")
|> NexBase.upsert(data)
|> NexBase.run()
```
### 事务处理
```elixir
# 使用 Ecto 事务
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, %User{name: "Alice"})
|> Ecto.Multi.insert(:post, %Post{user_id: {:user, :id}, title: "Hello"})
|> MyApp.Repo.transaction()
```
## 🆚 Supabase 对比
### API 相似性
| 操作 | Supabase JS | NexBase Elixir |
|------|-------------|----------------|
| 初始化 | `supabase.from('table')` | `client \|> NexBase.from("table")` |
| 查询 | `.select()` | `.select(fields)` |
| 过滤 | `.eq('col', val)` | `.eq(:col, val)` |
| 排序 | `.order('col', {ascending: true})` | `.order(:col, :asc)` |
| 分页 | `.range(0, 9)` | `.limit(10).offset(0)` |
| 插入 | `.insert({...})` | `.insert(%{...})` |
| 更新 | `.update({...})` | `.update(%{...})` |
| 删除 | `.delete()` | `.delete()` |
| 执行 | `.then()` | `.run()` |
### 主要差异
| 特性 | Supabase | NexBase |
|------|----------|---------|
| 语言 | JavaScript | Elixir |
| 类型系统 | 动态 | 静态(Elixir) |
| 错误处理 | Promise/async | {:ok, result} / {:error, reason} |
| 实时订阅 | 支持 | 通过 Nex.stream/SSE |
| 认证 | 内置 | 通过 Nex 框架 |
| 存储 | 内置 | 通过 S3 集成 |
## 🔌 与 Nex 框架集成
### 在 Page 中使用
```elixir
defmodule MyApp.Pages.Products do
use Nex
@client NexBase.client(repo: MyApp.Repo)
# 初始化页面数据
def mount(_params) do
{:ok, products} = @client
|> NexBase.from("products")
|> NexBase.eq(:active, true)
|> NexBase.order(:created_at, :desc)
|> NexBase.run()
%{products: products}
end
# 处理表单提交(Page Action)
def create(%{"name" => name, "price" => price}) do
{:ok, product} = @client
|> NexBase.from("products")
|> NexBase.insert(%{
name: name,
price: String.to_float(price),
active: true
})
|> NexBase.run()
# 返回 HTML 片段
product_item(%{product: product})
end
# 更新产品
def update(%{"id" => id, "name" => name}) do
{:ok, _} = @client
|> NexBase.from("products")
|> NexBase.eq(:id, String.to_integer(id))
|> NexBase.update(%{name: name})
|> NexBase.run()
:empty
end
# 删除产品
def delete(%{"id" => id}) do
{:ok, _} = @client
|> NexBase.from("products")
|> NexBase.eq(:id, String.to_integer(id))
|> NexBase.delete()
|> NexBase.run()
:empty
end
# 私有组件
defp product_item(assigns) do
~H"""
<div id={"product-#{@product["id"]}"}>
<h3><%= @product["name"] %></h3>
<p>¥<%= @product["price"] %></p>
</div>
"""
end
end
```
### 在 API 中使用
```elixir
defmodule MyApp.Api.Products do
use Nex
@client NexBase.client(repo: MyApp.Repo)
def get(req) do
id = req.query["id"]
case @client
|> NexBase.from("products")
|> NexBase.eq(:id, String.to_integer(id))
|> NexBase.run() do
{:ok, [product]} ->
Nex.json(%{data: product})
{:ok, []} ->
Nex.json(%{error: "Not found"}, status: 404)
{:error, reason} ->
Nex.json(%{error: inspect(reason)}, status: 500)
end
end
def post(req) do
{:ok, product} = @client
|> NexBase.from("products")
|> NexBase.insert(req.body)
|> NexBase.run()
Nex.json(%{data: product}, status: 201)
end
end
```
## 📊 性能优化
### 1. 连接池配置
```elixir
# config/config.exs
config :my_app, MyApp.Repo,
url: System.get_env("DATABASE_URL"),
pool_size: 10,
queue_target: 5000,
queue_interval: 1000
```
### 2. 查询优化
```elixir
# ❌ 不好:N+1 查询
{:ok, users} = client |> NexBase.from("users") |> NexBase.run()
Enum.map(users, fn user ->
{:ok, posts} = client
|> NexBase.from("posts")
|> NexBase.eq(:user_id, user["id"])
|> NexBase.run()
Map.put(user, "posts", posts)
end)
# ✅ 好:使用 JOIN(通过原始 SQL)
{:ok, result} = client
|> NexBase.query("""
SELECT u.*, json_agg(p.*) as posts
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id
""", [])
```
### 3. 索引建议
```sql
-- 常用查询字段
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
-- 复合索引
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
```
### 4. 选择字段优化
```elixir
# ❌ 不好:查询所有字段
{:ok, users} = client
|> NexBase.from("users")
|> NexBase.run()
# ✅ 好:只查询需要的字段
{:ok, users} = client
|> NexBase.from("users")
|> NexBase.select(["id", "name", "email"])
|> NexBase.run()
```
## ❓ 常见问题
### Q: 如何处理 NULL 值?
```elixir
# 查询 NULL
{:ok, results} = client
|> NexBase.from("users")
|> NexBase.is(:deleted_at, nil)
|> NexBase.run()
# 查询非 NULL
{:ok, results} = client
|> NexBase.from("users")
|> NexBase.neq(:deleted_at, nil)
|> NexBase.run()
```
### Q: 如何进行 LIKE 搜索?
```elixir
# 区分大小写
{:ok, results} = client
|> NexBase.from("products")
|> NexBase.like(:name, "%laptop%")
|> NexBase.run()
# 不区分大小写(推荐)
{:ok, results} = client
|> NexBase.from("products")
|> NexBase.ilike(:name, "%laptop%")
|> NexBase.run()
```
### Q: 如何处理日期范围查询?
```elixir
start_date = Date.new!(2024, 1, 1)
end_date = Date.new!(2024, 12, 31)
{:ok, results} = client
|> NexBase.from("orders")
|> NexBase.gte(:created_at, start_date)
|> NexBase.lte(:created_at, end_date)
|> NexBase.run()
```
### Q: 如何进行分组和聚合?
```elixir
# 使用原始 SQL
{:ok, result} = client
|> NexBase.query("""
SELECT category, COUNT(*) as count, AVG(price) as avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 5
ORDER BY count DESC
""", [])
```
### Q: 如何处理事务?
```elixir
# 使用 Ecto.Multi
result = Ecto.Multi.new()
|> Ecto.Multi.run(:insert_user, fn _repo, _changes ->
client
|> NexBase.from("users")
|> NexBase.insert(%{name: "Alice"})
|> NexBase.run()
end)
|> Ecto.Multi.run(:insert_post, fn _repo, %{insert_user: {:ok, user}} ->
client
|> NexBase.from("posts")
|> NexBase.insert(%{user_id: user["id"], title: "Hello"})
|> NexBase.run()
end)
|> MyApp.Repo.transaction()
```
### Q: 如何处理大数据集?
```elixir
# 使用分页
page_size = 100
total_pages = 10
Enum.each(1..total_pages, fn page ->
offset = (page - 1) * page_size
{:ok, results} = client
|> NexBase.from("large_table")
|> NexBase.order(:id, :asc)
|> NexBase.limit(page_size)
|> NexBase.offset(offset)
|> NexBase.run()
process_batch(results)
end)
```
## � 安全最佳实践
### 1. 参数化查询
```elixir
# ✅ 安全:使用参数化查询
{:ok, result} = client
|> NexBase.query("SELECT * FROM users WHERE id = $1", [user_id])
|> NexBase.run()
# ❌ 不安全:字符串插值
{:ok, result} = client
|> NexBase.query("SELECT * FROM users WHERE id = #{user_id}")
|> NexBase.run()
```
### 2. 行级安全 (RLS)
```sql
-- 启用 RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- 创建策略
CREATE POLICY "Users can view their own data"
ON users FOR SELECT
USING (auth.uid() = id);
```
### 3. 权限管理
```elixir
# 在 Page Action 中检查权限
def update(%{"id" => id} = params) do
current_user_id = get_current_user_id()
case @client
|> NexBase.from("posts")
|> NexBase.eq(:id, String.to_integer(id))
|> NexBase.eq(:user_id, current_user_id)
|> NexBase.run() do
{:ok, [_post]} ->
# 用户有权限,执行更新
@client
|> NexBase.from("posts")
|> NexBase.eq(:id, String.to_integer(id))
|> NexBase.update(params)
|> NexBase.run()
{:ok, []} ->
# 用户无权限
{:error, "Unauthorized"}
end
end
```
## �� 完整示例
查看 [nex_base_demo](../examples/nex_base_demo) 获取完整的 SSR 应用示例。
## 🔗 相关资源
- [Nex 框架文档](https://github.com/gofenix/nex)
- [Ecto 文档](https://hexdocs.pm/ecto)
- [PostgreSQL 文档](https://www.postgresql.org/docs/)
- [Supabase 文档](https://supabase.com/docs)
## 🤝 贡献
欢迎提交 Issue 和 Pull Request!
## 📄 许可证
MIT