QueryBuilder is one of the most powerful features of TypeORM - it allows you to build SQL queries using elegant and convenient syntax, execute them and get automatically transformed entities.
You can switch between different types of query builder within any of them, once you do, you will get a new instance of query builder (unlike all other methods).
Getting values using QueryBuilder
To get a single result from the database, for example to get a user by id or name, you must use getOne:
There are two types of results you can get using select query builder: entities or raw results. Most of the time, you need to select real entities from your database, for example, users. For this purpose, you use getOne and getMany. But sometimes you need to select some specific data, let's say the sum of all user photos. This data is not an entity, it's called raw data. To get raw data, you use getRawOne and getRawMany. Examples:
constphotosSums=await dataSource.getRepository(User).createQueryBuilder("user").select("user.id").addSelect("SUM(user.photosCount)","sum").groupBy("user.id").getRawMany()// result will be like this: [{ id: 1, sum: 25 }, { id: 2, sum: 13 }, ...]
Getting a count
You can get the count on the number of rows a query will return by using getCount(). This will return the count as a number rather than an Entity result.
SELECTcount(*) FROM users user WHERE user.name ='Timber'
What are aliases for?
We used createQueryBuilder("user"). But what is "user"? It's just a regular SQL alias. We use aliases everywhere, except when we work with selected data.
SELECT ... FROM users user WHERE user.name ='Timber'
See, we used the users table by using the user alias we assigned when we created a query builder.
One query builder is not limited to one alias, they can have multiple aliases. Each select can have its own alias, you can select from multiple tables each with its own alias, you can join multiple tables each with its own alias. You can use those aliases to access tables you are selecting (or data you are selecting).
Using parameters to escape data
We used where("user.name = :name", { name: "Timber" }). What does { name: "Timber" } stand for? It's a parameter we used to prevent SQL injection. We could have written: where("user.name = '" + name + "'), however this is not safe, as it opens the code to SQL injections. The safe way is to use this special syntax: where("user.name = :name", { name: "Timber" }), where :name is a parameter name and the value is specified in an object: { name: "Timber" }.
SELECT ... FROM users user WHERE user.registered = true ANDNOT((user.firstName ='Timber'OR user.lastName ='Saw'))
You can combine as many AND and OR expressions as you need. If you use .where more than once you'll override all previous WHERE expressions.
Note: be careful with orWhere - if you use complex expressions with both AND and OR expressions, keep in mind that they are stacked without any pretences. Sometimes you'll need to create a where string instead, and avoid using orWhere.
If you use .orderBy more than once you'll override all previous ORDER BY expressions.
Adding DISTINCT ON expression (Postgres only)
When using both distinct-on with an order-by expression, the distinct-on expression must match the leftmost order-by. The distinct-on expressions are interpreted using the same rules as order-by. Please note that, using distinct-on without an order-by expression means that the first row of each set is unpredictable.
If you use .groupBy more than once you'll override all previous GROUP BY expressions.
Adding LIMIT expression
Adding a LIMIT expression is easy as:
createQueryBuilder("user").limit(10)
Which will produce the following SQL query:
SELECT ... FROM users user LIMIT10
The resulting SQL query depends on the type of database (SQL, mySQL, Postgres, etc). Note: LIMIT may not work as you may expect if you are using complex queries with joins or subqueries. If you are using pagination, it's recommended to use take instead.
Adding OFFSET expression
Adding an SQL OFFSET expression is easy as:
createQueryBuilder("user").offset(10)
Which will produce the following SQL query:
SELECT ... FROM users user OFFSET 10
The resulting SQL query depends on the type of database (SQL, mySQL, Postgres, etc). Note: OFFSET may not work as you may expect if you are using complex queries with joins or subqueries. If you are using pagination, it's recommended to use skip instead.
As you can see leftJoinAndSelect automatically loaded all of Timber's photos. The first argument is the relation you want to load and the second argument is an alias you assign to this relation's table. You can use this alias anywhere in query builder. For example, let's take all Timber's photos which aren't removed.
SELECT user.*, photo.*FROM users userINNER JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSEWHERE user.name ='Timber'
The difference between LEFT JOIN and INNER JOIN is that INNER JOIN won't return a user if it does not have any photos. LEFT JOIN will return you the user even if it doesn't have photos. To learn more about different join types, refer to the SQL documentation.
Join without selection
You can join data without its selection. To do that, use leftJoin or innerJoin:
This will load Timber's profile photo and set it to user.profilePhoto. If you want to load and map a single entity use leftJoinAndMapOne. If you want to load and map multiple entities use leftJoinAndMapMany.
Getting the generated query
Sometimes you may want to get the SQL query generated by QueryBuilder. To do so, use getSql:
This query will return users and print the used sql statement to the console.
Getting raw results
There are two types of results you can get using select query builder: entities and raw results. Most of the time, you need to select real entities from your database, for example, users. For this purpose, you use getOne and getMany. However, sometimes you need to select specific data, like the sum of all user photos. Such data is not an entity, it's called raw data. To get raw data, you use getRawOne and getRawMany. Examples:
constphotosSums=await dataSource.getRepository(User).createQueryBuilder("user").select("user.id").addSelect("SUM(user.photosCount)","sum").groupBy("user.id").getRawMany()// result will be like this: [{ id: 1, sum: 25 }, { id: 2, sum: 13 }, ...]
Streaming result data
You can use stream which returns you a stream. Streaming returns you raw data, and you must handle entity transformation manually:
Most of the time when you develop an application, you need pagination functionality. This is used if you have pagination, page slider, or infinite scroll components in your application.
This will skip the first 5 users and take 10 users after them.
take and skip may look like we are using limit and offset, but they aren't. limit and offset may not work as you expect once you have more complicated queries with joins or subqueries. Using take and skip will prevent those issues.
Set locking
QueryBuilder supports both optimistic and pessimistic locking.
Lock modes
Support of lock modes, and SQL statements they translate to, are listed in the table below (blank cell denotes unsupported). When specified lock mode is not supported, a LockNotSupportedOnGivenDriverError error will be thrown.
| | pessimistic_read | pessimistic_write | dirty_read | pessimistic_partial_write (Deprecated, use onLocked instead) | pessimistic_write_or_fail (Deprecated, use onLocked instead) | for_no_key_update | for_key_share |
| --------------- | --------------------------------- | ----------------------- | ------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | ------------------- | ------------- |
| MySQL | FOR SHARE (8+)/LOCK IN SHARE MODE | FOR UPDATE | (nothing) | FOR UPDATE SKIP LOCKED | FOR UPDATE NOWAIT | | |
| Postgres | FOR SHARE | FOR UPDATE | (nothing) | FOR UPDATE SKIP LOCKED | FOR UPDATE NOWAIT | FOR NO KEY UPDATE | FOR KEY SHARE |
| Oracle | FOR UPDATE | FOR UPDATE | (nothing) | | | | |
| SQL Server | WITH (HOLDLOCK, ROWLOCK) | WITH (UPDLOCK, ROWLOCK) | WITH (NOLOCK) | | | | |
| AuroraDataApi | LOCK IN SHARE MODE | FOR UPDATE | (nothing) | | | | |
| CockroachDB | | FOR UPDATE | (nothing) | | FOR UPDATE NOWAIT | FOR NO KEY UPDATE | |
To use pessimistic read locking use the following method:
Optimistic locking works in conjunction with both @Version and @UpdatedDate decorators.
setOnLock
Allows you to control what happens when a row is locked. By default, the database will wait for the lock. You can control that behavior by using setOnLocked
If the model you are querying has a column with a select: false column, you must use the addSelect function in order to retrieve the information from the column.
If the model you are querying has a column with the attribute @DeleteDateColumn set, the query builder will automatically query rows which are 'soft deleted'.
You will get all the rows, including the ones which are deleted.
Common table expressions
QueryBuilder instances support common table expressions , if minimal supported version of your database supports them. Common table expressions aren't supported for Oracle yet.
constusers=awaitconnection.getRepository(User).createQueryBuilder('user').select("user.id",'id').addCommonTableExpression(` SELECT "userId" FROM "post" `,'post_users_ids').where(`user.id IN (SELECT "userId" FROM 'post_users_ids')`).getMany();
Result values of InsertQueryBuilder or UpdateQueryBuilder can be used in Postgres:
constinsertQueryBuilder=connection.getRepository(User).createQueryBuilder().insert({ name:'John Smith' }).returning(['id']);constusers=awaitconnection.getRepository(User).createQueryBuilder('user').addCommonTableExpression(insertQueryBuilder,'insert_results').where(`user.id IN (SELECT "id" FROM 'insert_results')`).getMany();
constrepository=connection.getRepository(Account)// create a new accountconstaccount=newAccount()account.name ="John Smith"account.balance =100awaitrepository.save(account)// imagine we update the account balance 1 hour after creationaccount.balance =200awaitrepository.save(account)// outputs { name: "John Smith", balance: "200" }console.log(account)// load account state on 1 hour backaccount =await repository.createQueryBuilder("account").timeTravelQuery(`'-1h'`).getOneOrFail()// outputs { name: "John Smith", balance: "100" }console.log(account)
By default timeTravelQuery() uses follower_read_timestamp() function if no arguments passed. For another supported timestamp arguments and additional information please refer to CockroachDB docs.
Debugging
You can get the generated SQL from the query builder by calling getQuery() or getQueryAndParameters().
["SELECT `user`.`id` as `userId`, `user`.`firstName` as `userFirstName`, `user`.`lastName` as `userLastName` FROM `users` `user` WHERE `user`.`id` = ?", [ 1 ]]