BkndCRUD读取Skill bknd-crud-read

这个技能用于通过Bknd SDK或REST API从数据库查询和检索数据,包括读取单个记录、批量读取、过滤排序、字段选择、加载关系等操作。关键词:Bknd、CRUD、数据查询、后端开发、数据库API、RESTful服务。

后端开发 0 次安装 0 次浏览 更新于 3/9/2026

名称: bknd-crud-read 描述: 使用SDK或REST API从Bknd实体查询和检索数据时使用。涵盖readOne、readMany、readOneBy、过滤(where子句)、排序、字段选择、加载关系(with/join)和响应处理。

CRUD 读取

使用SDK或REST API从Bknd数据库查询和检索数据。

先决条件

  • Bknd项目运行中(本地或部署)
  • 实体存在且包含数据(使用bknd-create-entitybknd-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%" } } }

验证

先在管理面板测试查询:

  1. 管理面板 > 数据 > 选择实体
  2. 使用过滤器/搜索UI
  3. 检查返回数据是否符合预期

或在代码中记录响应:

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加载