Query
A query is an object that describes how to retrieve or modify data from the database. It has several methods to describe the query and termination methods that execute them. The database adapter can extend the query API in many ways to support database specific features.
You can create a query using Database.query(T)
or Session.query(T)
. We recommend Sessions as it improves performance.
@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();
Filter
A filter can be applied to limit the result set.
//simple filters const users = await database.query(User).filter({name: 'User1'}).find(); //multiple filters, all AND const users = await database.query(User).filter({name: 'User1', id: 2}).find(); //range filter: $gt, $lt, $gte, $lte (greater than, lower than, ...) //equivalent to WHERE created < NOW() const users = await database.query(User).filter({created: {$lt: new Date}}).find(); //equivalent to WHERE id > 500 const users = await database.query(User).filter({id: {$gt: 500}}).find(); //equivalent to WHERE id >= 500 const users = await database.query(User).filter({id: {$gte: 500}}).find(); //set filter: $in, $nin (in, not in) //equivalent to WHERE id IN (1, 2, 3) const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find(); //regex filter const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find(); //grouping: $and, $nor, $or //equivalent to WHERE (username = 'User1') OR (username = 'User2') const users = await database.query(User).filter({ $or: [{username: 'User1'}, {username: 'User2'}] }).find(); //nested grouping //equivalent to 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(); //nested grouping //equivalent to 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();
Equal
Greater / Smaller
RegExp
Grouping AND/OR
In
Select
To narrow down the fields to be received from the database, select('field1')
can be used.
const user = await database.query(User).select('username').findOne(); const user = await database.query(User).select('id', 'username').findOne();
It is important to note that as soon as the fields are narrowed down using select
, the results are no longer instances of the entity, but only object literals.
const user = await database.query(User).select('username').findOne(); user instanceof User; //false
Order
With orderBy(field, order)
the order of the entries can be changed.
Several times orderBy
can be executed to refine the order more and more.
const users = await session.query(User).orderBy('created', 'desc').find(); const users = await session.query(User).orderBy('created', 'asc').find();
Pagination
The itemsPerPage()
and page()
methods can be used to paginate the results. Page starts at 1.
const users = await session.query(User).itemsPerPage(50).page(1).find();
With the alternative methods limit
and skip
you can paginate manually.
const users = await session.query(User).limit(5).skip(10).find();
[#database-join]
Join
By default, references from the entity are neither included in queries nor loaded. To include a join in the query without loading the reference, use join()
(left join) or innerJoin()
. To include a join in the query and load the reference, use joinWith()
or innerJoinWith()
.
All the following examples assume these model schemas:
@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) { } }
//select only users with a group assigned (INNER JOIN) const users = await session.query(User).innerJoin('group').find(); for (const user of users) { user.group; //error, since reference was not loaded }
//select only users with a group assigned (INNER JOIN) and load the relation const users = await session.query(User).innerJoinWith('group').find(); for (const user of users) { user.group.name; //works }
To modify join queries, use the same methods, but with the use
prefix: useJoin
, useInnerJoin
, useJoinWith
or useInnerJoinWith
. To end the join query modification, use end()
to get back the parent query.
//select only users with a group with name 'admins' assigned (INNER JOIN) const users = await session.query(User) .useInnerJoinWith('group') .filter({name: 'admins'}) .end() // returns to the parent query .find(); for (const user of users) { user.group.name; //always admin }
Aggregation
Aggregation methods allow you to count records and aggregate fields.
The following examples assume this model scheme:
@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
allows to group the result by the specified field.
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();
There are several aggregation methods: withSum
, withAverage
, withCount
, withMin
, withMax
, withGroupConcat
. Each requires a field name as the first argument and an optional second argument to change the alias.
// first let's update some of the records: 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
With returning
additional fields can be requested in case of changes via patch
and delete
.
Caution: Not all database adapters return fields atomically. Use transactions to ensure data consistency.
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
Returns an array of entries matching the specified filter.
const users: User[] = await database.query(User).filter({username: 'Peter'}).find();
FindOne
Returns an item that matches the specified filter.
If no item is found, an ItemNotFound
error is thrown.
const users: User = await database.query(User).filter({username: 'Peter'}).findOne();
FindOneOrUndefined
Returns an entry that matches the specified filter. If no entry is found, undefined is returned.
const query = database.query(User).filter({username: 'Peter'}); const users: User|undefined = await query.findOneOrUndefined();
FindField
Returns a list of a field that match the specified filter.
const usernames: string[] = await database.query(User).findField('username');
FindOneField
Returns a list of a field that match the specified filter.
If no entry is found, an ItemNotFound
error is thrown.
const username: string = await database.query(User).filter({id: 3}).findOneField('username');
Patch
Patch is a change query that patches the records described in the query. The methods
patchOne
and patchMany
finish the query and execute the patch.
patchMany
changes all records in the database that match the specified filter. If no filter is set, the whole table will be changed. Use patchOne
to change only one entry at a time.
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});
Delete
deleteMany
deletes all entries in the database that match the specified filter.
If no filter is set, the whole table will be deleted. Use deleteOne
to delete only one entry at a time.
const result = await database.query(User) .filter({visits: 0}) .deleteMany(); const result = await database.query(User).filter({id: 4}).deleteOne();
Has
Returns whether at least one entry exists in the database.
const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has();
Count
Returns the number of entries.
const userCount: number = await database.query(User).count();
Lift
Lifting a query means adding new functionality to it. This is usually used either by plugins or complex architectures to split larger query classes into several convenient, reusable classes.
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();