当你加载一组记录,然后为每条记录的关联分别发起查询时,就会产生 N+1 查询问题。
// 差:1 次查询文章 + N 次查询评论
const posts = await Post.find({ authorId: 1 });
for (const post of posts) {
post.comments = await Comment.find({ postId: post.id });
}
使用 .with() 或 .include() 在单次查询中加载关联:
// 好:1 次 JOIN 查询
const posts = await Post.find({ authorId: 1 }).with('comments');
for (const post of posts) {
console.log(post.comments); // 已加载
}
可以同时加载多个关联:
const posts = await Post.find().with('author', 'comments');
默认情况下,Leoric 会查询所有列(SELECT *)。当只需要特定列时,使用 .select():
// 差:加载所有列,包括大文本字段
const posts = await Post.find();
// 好:只加载需要的
const posts = await Post.find().select('id', 'title', 'createdAt');
对于有大 TEXT 或 BLOB 列的表尤其重要。
处理大表时,避免一次性加载所有记录到内存。应使用带索引且唯一的游标,而不是不断增大的 offset:
// 差:全部加载到内存
const allPosts = await Post.find();
// 好:不带额外过滤条件时,按主键稳定排序并分批处理
const pageSize = 100;
let lastId;
while (true) {
let query = Post.order('id').limit(pageSize);
if (lastId != null) {
query = query.where({ id: { $gt: lastId } });
}
const posts = await query;
if (posts.length === 0) break;
for (const post of posts) {
// 处理每条记录
}
lastId = posts.at(-1).id;
}
这个例子会遍历整张表且没有额外的过滤条件,因此按主键进行范围扫描是高效的。如果批处理查询带有过滤条件,游标及排序字段应该与查询使用的复合索引相匹配:先放使用等值过滤的字段,再放有序的游标字段,最后追加主键作为唯一的排序条件。例如,按 tenantId、status 过滤并按 id 分页的查询通常应使用 (tenant_id, status, id) 索引。请通过数据库的 EXPLAIN 功能确认实际执行计划。
建议尽量使用不会变化的字段作为游标。Limit/offset、复合游标和窗口函数分页的细节请参考《分页》。
对于长时间运行的应用,配置空闲超时以防止过期连接:
const realm = new Realm({
host: 'localhost',
database: 'my_app',
idleTimeout: 30000, // 30 秒
});
应用关闭时务必断开连接:
process.on('SIGTERM', async () => {
await realm.disconnect();
process.exit(0);
});
// 差:事务内调用外部 API 会长时间占用连接
await Bone.transaction(async ({ connection }) => {
const user = await User.create({ name: 'Alice' }, { connection });
const result = await fetch('https://api.example.com/notify'); // 慢!
await AuditLog.create({ action: 'user_created' }, { connection });
});
// 好:将外部调用移到事务之外
const user = await Bone.transaction(async ({ connection }) => {
const user = await User.create({ name: 'Alice' }, { connection });
await AuditLog.create({ action: 'user_created' }, { connection });
return user;
});
await fetch('https://api.example.com/notify');
// 生成器函数自动传递 connection
await Bone.transaction(function* () {
const user = yield User.create({ name: 'Alice' });
yield AuditLog.create({ action: 'user_created', userId: user.id });
});
如果你经常使用多条件查询:
// 如果这是常见的查询模式:
Post.find({ authorId: 1, status: 'published' }).order('createdAt', 'desc')
考虑添加复合索引:(author_id, status, created_at)。
当查询优化器做出次优选择时:
Post.find({ authorId: 1 }).forceIndex('idx_author_created')
详见索引提示。
const realm = new Realm({
models: 'app/models', // 自动从目录加载所有模型
});
initialize() 中定义关联class Post extends Bone {
static initialize() {
this.belongsTo('author', { Model: 'User' });
this.hasMany('comments');
this.hasMany('tags', { through: 'tagMaps' });
}
}
class Post extends Bone {
@Column({ primaryKey: true })
id: bigint;
@BelongsTo()
author: User;
@HasMany()
comments: Comment[];
}
// 差:SQL 注入漏洞
await realm.query(`SELECT * FROM posts WHERE title = '${userInput}'`);
// 好:参数化查询
await realm.query('SELECT * FROM posts WHERE title = ?', [userInput]);
// 好:使用 ORM 查询接口
await Post.find({ title: userInput });
raw()raw() 函数绕过转义。只用于 SQL 函数和表达式,绝不用于用户提供的值:
// 好:SQL 函数
await Post.update({ id: 1 }, { viewCount: raw('view_count + 1') });
// 差:在 raw() 中使用用户输入
await Post.find({ title: raw(userInput) }); // SQL 注入!