查询
查询是一个对象,用于描述如何从数据库检索或修改数据。它有多种方法来描述查询,以及执行它们的终止方法。数据库适配器可以通过多种方式扩展查询 API,以支持数据库特定的功能。
你可以使用 Database.query(T)
或 Session.query(T)
创建查询。我们建议使用 Session,因为它能提升性能。
@entity.name('user') class User { id: number & PrimaryKey & AutoIncrement = 0; created: Date = new Date; birthdate?: Date; visits: number = 0; constructor(public username: string) { } } const database = new Database(...); //[ { username: 'User1' }, { username: 'User2' }, { username: 'User2' } ] const users = await database.query(User).select('username').find();
过滤器
可以应用过滤器来限制结果集。
// 简单过滤器 const users = await database.query(User).filter({name: 'User1'}).find(); // 多个过滤器,全部 AND const users = await database.query(User).filter({name: 'User1', id: 2}).find(); // 范围过滤:$gt, $lt, $gte, $lte(大于,小于,...) // 等价于 WHERE created < NOW() const users = await database.query(User).filter({created: {$lt: new Date}}).find(); // 等价于 WHERE id > 500 const users = await database.query(User).filter({id: {$gt: 500}}).find(); // 等价于 WHERE id >= 500 const users = await database.query(User).filter({id: {$gte: 500}}).find(); // 集合过滤:$in, $nin(在,不在) // 等价于 WHERE id IN (1, 2, 3) const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find(); // 正则过滤 const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find(); // 分组:$and, $nor, $or // 等价于 WHERE (username = 'User1') OR (username = 'User2') const users = await database.query(User).filter({ $or: [{username: 'User1'}, {username: 'User2'}] }).find(); // 嵌套分组 // 等价于 WHERE username = 'User1' OR (username = 'User2' and id > 0) const users = await database.query(User).filter({ $or: [{username: 'User1'}, {username: 'User2', id: {$gt: 0}}] }).find(); // 嵌套分组 // 等价于 WHERE username = 'User1' AND (created < NOW() OR id > 0) const users = await database.query(User).filter({ $and: [{username: 'User1'}, {$or: [{created: {$lt: new Date}, id: {$gt: 0}}]}] }).find();
等于
大于 / 小于
正则表达式
分组 AND/OR
In
选择字段
要缩小从数据库接收的字段范围,可以使用 select('field1')
。
const user = await database.query(User).select('username').findOne(); const user = await database.query(User).select('id', 'username').findOne();
需要注意的是,一旦使用 select
缩小了字段范围,结果将不再是实体的实例,而只是普通对象字面量。
const user = await database.query(User).select('username').findOne(); user instanceof User; //false
排序
使用 orderBy(field, order)
可以改变条目的顺序。
可以多次调用 orderBy
以进一步细化排序。
const users = await session.query(User).orderBy('created', 'desc').find(); const users = await session.query(User).orderBy('created', 'asc').find();
分页
可以使用 itemsPerPage()
和 page()
方法对结果进行分页。页码从 1 开始。
const users = await session.query(User).itemsPerPage(50).page(1).find();
通过备用方法 limit
和 skip
也可以手动分页。
const users = await session.query(User).limit(5).skip(10).find();
[#database-join]
连接(Join)
默认情况下,实体中的引用既不会包含在查询中也不会被加载。若要在查询中包含连接但不加载引用,请使用 join()
(左连接)或 innerJoin()
。若要在查询中包含连接并加载引用,请使用 joinWith()
或 innerJoinWith()
。
以下所有示例都假设这些模型模式:
@entity.name('group') class Group { id: number & PrimaryKey & AutoIncrement = 0; created: Date = new Date; constructor(public username: string) { } } @entity.name('user') class User { id: number & PrimaryKey & AutoIncrement = 0; created: Date = new Date; group?: Group & Reference; constructor(public username: string) { } }
// 只选择有分配到组的用户(INNER JOIN) const users = await session.query(User).innerJoin('group').find(); for (const user of users) { user.group; // 报错,因为引用未被加载 }
// 只选择有分配到组的用户(INNER JOIN)并加载关系 const users = await session.query(User).innerJoinWith('group').find(); for (const user of users) { user.group.name; // 可用 }
要修改连接查询,请使用相同的方法,但加上 use
前缀:useJoin
、useInnerJoin
、useJoinWith
或 useInnerJoinWith
。要结束连接查询的修改,使用 end()
回到父查询。
// 只选择分配了名为 'admins' 的组的用户(INNER JOIN) const users = await session.query(User) .useInnerJoinWith('group') .filter({name: 'admins'}) .end() // 返回到父查询 .find(); for (const user of users) { user.group.name; // 始终为 admin }
聚合
聚合方法允许你统计记录并聚合字段。
以下示例假设此模型模式:
@entity.name('file') class File { id: number & PrimaryKey & AutoIncrement = 0; created: Date = new Date; downloads: number = 0; category: string = 'none'; constructor(public path: string & Index) { } }
groupBy
允许按指定字段对结果分组。
await database.persist( cast<File>({path: 'file1', category: 'images'}), cast<File>({path: 'file2', category: 'images'}), cast<File>({path: 'file3', category: 'pdfs'}) ); //[ { category: 'images' }, { category: 'pdfs' } ] await session.query(File).groupBy('category').find();
有多种聚合方法:withSum
、withAverage
、withCount
、withMin
、withMax
、withGroupConcat
。每个方法都需要字段名作为第一个参数,并可选地提供第二个参数以更改别名。
// 首先我们更新部分记录: await database.query(File).filter({path: 'images/file1'}).patchOne({$inc: {downloads: 15}}); await database.query(File).filter({path: 'images/file2'}).patchOne({$inc: {downloads: 5}}); //[{ category: 'images', downloads: 20 },{ category: 'pdfs', downloads: 0 }] await session.query(File).groupBy('category').withSum('downloads').find(); //[{ category: 'images', downloads: 10 },{ category: 'pdfs', downloads: 0 }] await session.query(File).groupBy('category').withAverage('downloads').find(); //[ { category: 'images', amount: 2 }, { category: 'pdfs', amount: 1 } ] await session.query(File).groupBy('category').withCount('id', 'amount').find();
返回(Returning)
使用 returning
可以在通过 patch
和 delete
更改数据时额外请求字段。
注意:并非所有数据库适配器都会以原子方式返回字段。请使用事务以确保数据一致性。
await database.query(User).patchMany({visits: 0}); //{ modified: 1, returning: { visits: [ 5 ] }, primaryKeys: [ 1 ] } const result = await database.query(User) .filter({username: 'User1'}) .returning('username', 'visits') .patchOne({$inc: {visits: 5}});
Find
返回匹配指定过滤器的条目数组。
const users: User[] = await database.query(User).filter({username: 'Peter'}).find();
FindOne
返回匹配指定过滤器的条目。
如果未找到条目,将抛出 ItemNotFound
错误。
const users: User = await database.query(User).filter({username: 'Peter'}).findOne();
FindOneOrUndefined
返回匹配指定过滤器的条目。 如果未找到条目,则返回 undefined。
const query = database.query(User).filter({username: 'Peter'}); const users: User|undefined = await query.findOneOrUndefined();
FindField
返回匹配指定过滤器的某个字段的列表。
const usernames: string[] = await database.query(User).findField('username');
FindOneField
返回匹配指定过滤器的某个字段的值。
如果未找到条目,将抛出 ItemNotFound
错误。
const username: string = await database.query(User).filter({id: 3}).findOneField('username');
Patch
Patch 是一种更改查询,用于修改查询所描述的记录。方法
patchOne
和 patchMany
会结束查询并执行补丁操作。
patchMany
会更改数据库中符合指定过滤器的所有记录。如果未设置过滤器,整个表都会被更改。使用 patchOne
可一次仅更改一个条目。
await database.query(User).filter({username: 'Peter'}).patch({username: 'Peter2'}); await database.query(User).filter({username: 'User1'}).patchOne({birthdate: new Date}); await database.query(User).filter({username: 'User1'}).patchOne({$inc: {visits: 1}}); await database.query(User).patchMany({visits: 0});
删除
deleteMany
会删除数据库中所有匹配指定过滤器的条目。
如果未设置过滤器,整个表都会被删除。使用 deleteOne
可一次仅删除一个条目。
const result = await database.query(User) .filter({visits: 0}) .deleteMany(); const result = await database.query(User).filter({id: 4}).deleteOne();
Has
返回数据库中是否至少存在一个条目。
const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has();
计数
返回条目数量。
const userCount: number = await database.query(User).count();
提升(Lift)
对查询进行“提升”意味着向其添加新功能。这通常由插件或复杂架构使用,以将较大的查询类拆分为多个方便、可复用的类。
import { FilterQuery, Query } from '@deepkit/orm'; class UserQuery<T extends {birthdate?: Date}> extends Query<T> { hasBirthday() { const start = new Date(); start.setHours(0,0,0,0); const end = new Date(); end.setHours(23,59,59,999); return this.filter({$and: [{birthdate: {$gte: start}}, {birthdate: {$lte: end}}]} as FilterQuery<T>); } } await session.query(User).lift(UserQuery).hasBirthday().find();