名称: bknd-crud-read 描述: 使用SDK或REST API从Bknd实体查询和检索数据时使用。涵盖readOne、readMany、readOneBy、过滤(where子句)、排序、字段选择、加载关系(with/join)和响应处理。
CRUD 读取
使用SDK或REST API从Bknd数据库查询和检索数据。
先决条件
- Bknd项目运行中(本地或部署)
- 实体存在且包含数据(使用
bknd-create-entity、bknd-crud-create) - SDK配置好或API端点已知
何时使用UI模式
- 手动浏览数据
- 开发期间的快速查找
- 操作后验证数据
UI步骤: 管理面板 > 数据 > 选择实体 > 浏览/搜索记录
何时使用代码模式
- 应用程序数据显示
- 搜索/过滤功能
- 构建列表、表格、详情页面
- API集成
代码方法
步骤1:设置SDK客户端
import { Api } from "bknd";
const api = new Api({
host: "http://localhost:7654",
});
// 如需认证:
api.updateToken("your-jwt-token");
步骤2:按ID读取单个记录
使用readOne(entity, id, query?):
const { ok, data, error } = await api.data.readOne("posts", 1);
if (ok) {
console.log("文章:", data.title);
} else {
console.error("未找到或错误:", error.message);
}
步骤3:按查询读取单个记录
使用readOneBy(entity, query)通过字段值查找:
const { data } = await api.data.readOneBy("users", {
where: { email: { $eq: "user@example.com" } },
});
if (data) {
console.log("找到用户:", data.id);
}
步骤4:读取多个记录
使用readMany(entity, query?):
const { ok, data, meta } = await api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
limit: 20,
offset: 0,
});
console.log(`共找到${meta.total}条,显示${data.length}条`);
步骤5:处理响应
响应对象结构:
type ReadResponse = {
ok: boolean;
data?: T | T[]; // 单个对象或数组
meta?: { // 用于readMany
total: number; // 总匹配记录数
limit: number; // 当前页面大小
offset: number; // 当前偏移量
};
error?: {
message: string;
code: string;
};
};
过滤(Where子句)
比较运算符
// 相等(隐式或显式)
{ where: { status: "published" } }
{ where: { status: { $eq: "published" } } }
// 不相等
{ where: { status: { $ne: "deleted" } } }
// 数字比较
{ where: { age: { $gt: 18 } } } // 大于
{ where: { age: { $gte: 18 } } } // 大于或等于
{ where: { price: { $lt: 100 } } } // 小于
{ where: { price: { $lte: 100 } } } // 小于或等于
字符串运算符
// LIKE模式(% = 通配符)
{ where: { title: { $like: "%hello%" } } } // 包含(区分大小写)
{ where: { title: { $ilike: "%hello%" } } } // 包含(不区分大小写)
// 便捷方法
{ where: { name: { $startswith: "John" } } }
{ where: { email: { $endswith: "@gmail.com" } } }
{ where: { bio: { $contains: "developer" } } }
数组运算符
// 在数组中
{ where: { id: { $in: [1, 2, 3] } } }
// 不在数组中
{ where: { type: { $nin: ["archived", "deleted"] } } }
空值检查
// 是NULL
{ where: { deleted_at: { $isnull: true } } }
// 不是NULL
{ where: { published_at: { $isnull: false } } }
逻辑运算符
// AND(隐式 - 多个字段)
{
where: {
status: { $eq: "published" },
category: { $eq: "news" },
}
}
// OR
{
where: {
$or: [
{ status: { $eq: "published" } },
{ featured: { $eq: true } },
]
}
}
// 组合AND/OR
{
where: {
category: { $eq: "news" },
$or: [
{ status: { $eq: "published" } },
{ author_id: { $eq: currentUserId } },
]
}
}
排序
// 对象语法(首选)
{ sort: { created_at: "desc" } }
{ sort: { name: "asc", created_at: "desc" } } // 多重排序
// 字符串语法(-前缀 = 降序)
{ sort: "-created_at" }
{ sort: "name,-created_at" }
字段选择
通过选择特定字段减少负载:
const { data } = await api.data.readMany("users", {
select: ["id", "email", "name"],
});
// data[0]仅包含id、email、name
加载关系
With子句(单独查询)
// 简单 - 加载关系
{ with: "author" }
{ with: ["author", "comments"] }
{ with: "author,comments" }
// 嵌套带子查询选项
{
with: {
author: {
select: ["id", "name", "avatar"],
},
comments: {
where: { approved: { $eq: true } },
sort: { created_at: "desc" },
limit: 10,
with: ["user"], // 嵌套加载
},
}
}
结果结构:
const { data } = await api.data.readOne("posts", 1, {
with: ["author", "comments"],
});
console.log(data.author.name); // 嵌套对象
console.log(data.comments[0].text); // 嵌套数组
Join子句(SQL JOIN)
使用join通过相关字段过滤:
const { data } = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" }, // 通过连接字段过滤
},
sort: "-author.created_at", // 通过连接字段排序
});
With vs Join
| 特性 | with |
join |
|---|---|---|
| 查询方法 | 单独查询 | SQL JOIN |
| 返回结构 | 嵌套对象 | 扁平(除非同时使用with) |
| 用例 | 加载相关数据 | 通过相关字段过滤 |
| 性能 | 多个查询 | 单个查询 |
分页
// 第1页(记录0-19)
{ limit: 20, offset: 0 }
// 第2页(记录20-39)
{ limit: 20, offset: 20 }
// 通用页面公式
{ limit: pageSize, offset: (page - 1) * pageSize }
如果未指定,默认限制为10。
分页助手
async function paginate<T>(
entity: string,
page: number,
pageSize: number,
query: object = {}
) {
const { data, meta } = await api.data.readMany(entity, {
...query,
limit: pageSize,
offset: (page - 1) * pageSize,
});
return {
data,
page,
pageSize,
total: meta.total,
totalPages: Math.ceil(meta.total / pageSize),
hasNext: page * pageSize < meta.total,
hasPrev: page > 1,
};
}
REST API方法
读取多个
# 基本
curl http://localhost:7654/api/data/posts
# 带查询参数
curl "http://localhost:7654/api/data/posts?limit=20&offset=0&sort=-created_at"
# 带where子句
curl "http://localhost:7654/api/data/posts?where=%7B%22status%22%3A%22published%22%7D"
按ID读取单个
curl http://localhost:7654/api/data/posts/1
复杂查询(POST)
对于复杂查询,使用POST到/api/data/:entity/query:
curl -X POST http://localhost:7654/api/data/posts/query \
-H "Content-Type: application/json" \
-d '{
"where": {"status": {"$eq": "published"}},
"sort": {"created_at": "desc"},
"limit": 20,
"with": ["author"]
}'
读取相关记录
# 获取用户的文章
curl http://localhost:7654/api/data/users/1/posts
React集成
基本列表
import { useApp } from "bknd/react";
import { useEffect, useState } from "react";
function PostsList() {
const { api } = useApp();
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
limit: 20,
}).then(({ data }) => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <div>加载中...</div>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
使用SWR
import { useApp } from "bknd/react";
import useSWR from "swr";
function PostsList() {
const { api } = useApp();
const { data: posts, isLoading, error } = useSWR(
"posts-published",
() => api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
}).then((r) => r.data)
);
if (isLoading) return <div>加载中...</div>;
if (error) return <div>加载文章错误</div>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
详情页面
function PostDetail({ postId }: { postId: number }) {
const { api } = useApp();
const { data: post, isLoading } = useSWR(
`post-${postId}`,
() => api.data.readOne("posts", postId, {
with: ["author", "comments"],
}).then((r) => r.data)
);
if (isLoading) return <div>加载中...</div>;
if (!post) return <div>文章未找到</div>;
return (
<article>
<h1>{post.title}</h1>
<p>作者 {post.author?.name}</p>
<div>{post.content}</div>
<h2>评论 ({post.comments?.length})</h2>
</article>
);
}
带防抖的搜索
import { useState, useMemo } from "react";
import { useApp } from "bknd/react";
import useSWR from "swr";
import { useDebouncedValue } from "@mantine/hooks"; // 或自定义钩子
function SearchPosts() {
const { api } = useApp();
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
const { data: results, isLoading } = useSWR(
debouncedSearch ? `search-${debouncedSearch}` : null,
() => api.data.readMany("posts", {
where: { title: { $ilike: `%${debouncedSearch}%` } },
limit: 10,
}).then((r) => r.data)
);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索文章..."
/>
{isLoading && <p>搜索中...</p>}
<ul>
{results?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
完整示例
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// 获取带关系的单个文章
const { data: post } = await api.data.readOne("posts", 1, {
with: {
author: { select: ["id", "name"] },
tags: true,
},
});
console.log(post.title, "作者", post.author.name);
// 通过邮箱查找用户
const { data: user } = await api.data.readOneBy("users", {
where: { email: { $eq: "admin@example.com" } },
});
// 列出已发布文章带分页
const { data: posts, meta } = await api.data.readMany("posts", {
where: {
status: { $eq: "published" },
deleted_at: { $isnull: true },
},
sort: { created_at: "desc" },
limit: 10,
offset: 0,
with: ["author"],
});
console.log(`第1页,共${Math.ceil(meta.total / 10)}页`);
// 复杂查询:管理员作者在类别的文章
const { data: adminPosts } = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" },
category: { $eq: "announcements" },
$or: [
{ status: { $eq: "published" } },
{ featured: { $eq: true } },
],
},
select: ["id", "title", "created_at"],
sort: "-created_at",
});
常见模式
计数记录
const { data } = await api.data.count("posts", {
status: { $eq: "published" },
});
console.log(`${data.count}篇已发布文章`);
检查存在性
const { data } = await api.data.exists("users", {
email: { $eq: "test@example.com" },
});
if (data.exists) {
console.log("邮箱已注册");
}
软删除过滤
// 总是排除软删除的
const { data } = await api.data.readMany("posts", {
where: { deleted_at: { $isnull: true } },
});
获取用户的相关数据
// 使用readManyByReference
const { data: userPosts } = await api.data.readManyByReference(
"users", userId, "posts",
{ sort: { created_at: "desc" }, limit: 10 }
);
常见陷阱
未检查响应
问题: 假设数据存在。
修复: 总是检查ok或处理undefined:
// 错误
const { data } = await api.data.readOne("posts", 999);
console.log(data.title); // 如果未找到则错误!
// 正确
const { ok, data } = await api.data.readOne("posts", 999);
if (!ok || !data) {
console.log("文章未找到");
return;
}
console.log(data.title);
错误的运算符语法
问题: 运算符使用不正确。
修复: 在运算符对象中包装值:
// 错误
{ where: { age: ">18" } }
// 正确
{ where: { age: { $gt: 18 } } }
过滤相关字段时缺少Join
问题: 未通过join过滤相关字段。
修复: 添加join子句:
// 错误 - 不会工作
{ where: { "author.role": { $eq: "admin" } } }
// 正确 - 添加join
{
join: ["author"],
where: { "author.role": { $eq: "admin" } }
}
N+1查询问题
问题: 在循环中加载关系。
修复: 使用with批量加载关系:
// 错误 - N+1查询
const { data: posts } = await api.data.readMany("posts");
for (const post of posts) {
const { data: author } = await api.data.readOne("users", post.author_id);
}
// 正确 - 单个批量查询
const { data: posts } = await api.data.readMany("posts", {
with: ["author"],
});
posts.forEach(p => console.log(p.author.name));
区分大小写的搜索
问题: $like区分大小写。
修复: 使用$ilike进行不区分大小写的搜索:
// 区分大小写(可能错过结果)
{ where: { title: { $like: "%React%" } } }
// 不区分大小写
{ where: { title: { $ilike: "%react%" } } }
验证
先在管理面板测试查询:
- 管理面板 > 数据 > 选择实体
- 使用过滤器/搜索UI
- 检查返回数据是否符合预期
或在代码中记录响应:
const response = await api.data.readMany("posts", query);
console.log("响应:", JSON.stringify(response, null, 2));
应该做和不应该做
应该做:
- 访问
data前检查ok - 使用
with加载关系 - 通过相关字段过滤时使用
join - 不区分大小写的文本搜索使用
$ilike - 大数据集使用分页
- 使用
select减少负载大小
不应该做:
- 假设查询总是返回数据
- 在循环中加载关系(N+1问题)
- 通过关系字段过滤时忘记
join - 需要不区分大小写时使用
$like - 仅需少量字段时请求所有字段
- 可能大数据集跳过分页
相关技能
- bknd-crud-create - 插入记录以查询
- bknd-crud-update - 修改查询的记录
- bknd-crud-delete - 删除查询的记录
- bknd-query-filter - 高级过滤技术
- bknd-pagination - 分页策略
- bknd-define-relationship - 设置关系以使用
with加载