Saltar al contenido principal

Selección usando Query Builder

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

¿Qué es un QueryBuilder?

QueryBuilder es una de las características más potentes de TypeORM - te permite construir consultas SQL usando una sintaxis elegante y conveniente, ejecutarlas y obtener entidades transformadas automáticamente.

Ejemplo simple de QueryBuilder:

const firstUser = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 })
.getOne()

Construye la siguiente consulta SQL:

SELECT
user.id as userId,
user.firstName as userFirstName,
user.lastName as userLastName
FROM users user
WHERE user.id = 1

y te devuelve una instancia de User:

User {
id: 1,
firstName: "Timber",
lastName: "Saw"
}

Nota importante al usar QueryBuilder

Al usar QueryBuilder, debes proporcionar parámetros únicos en tus expresiones WHERE. Esto NO funcionará:

const result = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.linkedSheep", "linkedSheep")
.leftJoinAndSelect("user.linkedCow", "linkedCow")
.where("user.linkedSheep = :id", { id: sheepId })
.andWhere("user.linkedCow = :id", { id: cowId })

... pero esto sí:

const result = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.linkedSheep", "linkedSheep")
.leftJoinAndSelect("user.linkedCow", "linkedCow")
.where("user.linkedSheep = :sheepId", { sheepId })
.andWhere("user.linkedCow = :cowId", { cowId })

Observa que usamos nombres únicos :sheepId y :cowId en lugar de usar :id dos veces para parámetros diferentes.

¿Cómo crear y usar un QueryBuilder?

Hay varias formas de crear un Query Builder:

  • Usando DataSource:

    const user = await dataSource
    .createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne()
  • Usando el entity manager:

    const user = await dataSource.manager
    .createQueryBuilder(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne()
  • Usando el repositorio:

    const user = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .getOne()

Existen 5 tipos diferentes de QueryBuilder:

  • SelectQueryBuilder - usado para construir y ejecutar consultas SELECT. Ejemplo:

    const user = await dataSource
    .createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne()
  • InsertQueryBuilder - usado para construir y ejecutar consultas INSERT. Ejemplo:

    await dataSource
    .createQueryBuilder()
    .insert()
    .into(User)
    .values([
    { firstName: "Timber", lastName: "Saw" },
    { firstName: "Phantom", lastName: "Lancer" },
    ])
    .execute()
  • UpdateQueryBuilder - usado para construir y ejecutar consultas UPDATE. Ejemplo:

    await dataSource
    .createQueryBuilder()
    .update(User)
    .set({ firstName: "Timber", lastName: "Saw" })
    .where("id = :id", { id: 1 })
    .execute()
  • DeleteQueryBuilder - usado para construir y ejecutar consultas DELETE. Ejemplo:

    await dataSource
    .createQueryBuilder()
    .delete()
    .from(User)
    .where("id = :id", { id: 1 })
    .execute()
  • RelationQueryBuilder - usado para construir y ejecutar operaciones específicas de relaciones [TBD]. Ejemplo:

    await dataSource
    .createQueryBuilder()
    .relation(User, "photos")
    .of(id)
    .loadMany()

Puedes cambiar entre diferentes tipos de query builder dentro de cualquiera de ellos, y al hacerlo obtendrás una nueva instancia del query builder (a diferencia de otros métodos).

Obteniendo valores con QueryBuilder

Para obtener un único resultado de la base de datos, por ejemplo para obtener un usuario por id o nombre, debes usar getOne:

const timber = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id OR user.name = :name", { id: 1, name: "Timber" })
.getOne()

getOneOrFail obtendrá un único resultado de la base de datos, pero si no existe ningún resultado lanzará un EntityNotFoundError:

const timber = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id OR user.name = :name", { id: 1, name: "Timber" })
.getOneOrFail()

Para obtener múltiples resultados de la base de datos, por ejemplo, para obtener todos los usuarios, usa getMany:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.getMany()

Hay dos tipos de resultados que puedes obtener con el constructor de consultas SELECT: entidades o resultados sin procesar. La mayoría de las veces necesitas seleccionar entidades reales de tu base de datos, como usuarios. Para este propósito usas getOne y getMany. Pero a veces necesitas seleccionar datos específicos, como la suma de todas las fotos de usuarios. Estos datos no son una entidad, se llaman datos sin procesar. Para obtener datos sin procesar, usa getRawOne y getRawMany. Ejemplos:

const { sum } = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("SUM(user.photosCount)", "sum")
.where("user.id = :id", { id: 1 })
.getRawOne()
const photosSums = 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 }, ...]

Obteniendo un conteo

Puedes obtener el conteo de filas que devolverá una consulta usando getCount(). Esto retornará el conteo como número en lugar de un resultado de Entidad.

const count = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.name = :name", { name: "Timber" })
.getCount()

Lo cual produce la siguiente consulta SQL:

SELECT count(*) FROM users user WHERE user.name = 'Timber'

¿Para qué sirven los alias?

Usamos createQueryBuilder("user"). ¿Pero qué es "user"? Es simplemente un alias SQL regular. Usamos alias en todas partes, excepto cuando trabajamos con datos seleccionados.

createQueryBuilder("user") equivale a:

createQueryBuilder().select("user").from(User, "user")

Lo que resultará en la siguiente consulta SQL:

SELECT ... FROM users user

En esta consulta SQL, users es el nombre de la tabla y user es el alias que le asignamos. Luego usamos este alias para acceder a la tabla:

createQueryBuilder()
.select("user")
.from(User, "user")
.where("user.name = :name", { name: "Timber" })

Lo cual produce la siguiente consulta SQL:

SELECT ... FROM users user WHERE user.name = 'Timber'

Como ves, accedimos a la tabla users usando el alias user que asignamos al crear el constructor de consultas.

Un constructor de consultas no está limitado a un solo alias, puede tener múltiples. Cada SELECT puede tener su propio alias, puedes seleccionar de múltiples tablas cada una con su alias, puedes unir múltiples tablas cada una con su alias. Puedes usar esos alias para acceder a las tablas que estás seleccionando (o a los datos que estás seleccionando).

Usando parámetros para escapar datos

Usamos where("user.name = :name", { name: "Timber" }). ¿Qué significa { name: "Timber" }? Es un parámetro que usamos para prevenir inyección SQL. Podríamos haber escrito: where("user.name = '" + name + "'), pero esto no es seguro, ya que expone el código a inyecciones SQL. La forma segura es usar esta sintaxis especial: where("user.name = :name", { name: "Timber" }), donde :name es el nombre del parámetro y el valor se especifica en un objeto: { name: "Timber" }.

.where("user.name = :name", { name: "Timber" })

es un atajo para:

.where("user.name = :name")
.setParameter("name", "Timber")

Nota: no uses el mismo nombre de parámetro para diferentes valores en el constructor de consultas. Los valores se sobrescribirán si los estableces múltiples veces.

También puedes proporcionar un array de valores y transformarlos en una lista de valores en la sentencia SQL usando la sintaxis de expansión especial:

.where("user.name IN (:...names)", { names: [ "Timber", "Crystal", "Lina" ] })

Lo cual se convierte en:

WHERE user.name IN ('Timber', 'Crystal', 'Lina')

Añadiendo expresión WHERE

Añadir una expresión WHERE es tan simple como:

createQueryBuilder("user").where("user.name = :name", { name: "Timber" })

Lo que producirá:

SELECT ... FROM users user WHERE user.name = 'Timber'

Puedes añadir AND a una expresión WHERE existente:

createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName: "Timber" })
.andWhere("user.lastName = :lastName", { lastName: "Saw" })

Lo que producirá esta consulta SQL:

SELECT ... FROM users user WHERE user.firstName = 'Timber' AND user.lastName = 'Saw'

Puedes añadir OR a una expresión WHERE existente:

createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })

Lo que producirá esta consulta SQL:

SELECT ... FROM users user WHERE user.firstName = 'Timber' OR user.lastName = 'Saw'

Puedes hacer una consulta IN con la expresión WHERE:

createQueryBuilder("user").where("user.id IN (:...ids)", { ids: [1, 2, 3, 4] })

Lo que producirá esta consulta SQL:

SELECT ... FROM users user WHERE user.id IN (1, 2, 3, 4)

Puedes añadir una expresión WHERE compleja en una WHERE existente usando Brackets

createQueryBuilder("user")
.where("user.registered = :registered", { registered: true })
.andWhere(
new Brackets((qb) => {
qb.where("user.firstName = :firstName", {
firstName: "Timber",
}).orWhere("user.lastName = :lastName", { lastName: "Saw" })
}),
)

Lo que producirá esta consulta SQL:

SELECT ... FROM users user WHERE user.registered = true AND (user.firstName = 'Timber' OR user.lastName = 'Saw')

Puedes añadir una expresión WHERE compleja negada en un WHERE existente usando NotBrackets

createQueryBuilder("user")
.where("user.registered = :registered", { registered: true })
.andWhere(
new NotBrackets((qb) => {
qb.where("user.firstName = :firstName", {
firstName: "Timber",
}).orWhere("user.lastName = :lastName", { lastName: "Saw" })
}),
)

Lo que producirá esta consulta SQL:

SELECT ... FROM users user WHERE user.registered = true AND NOT((user.firstName = 'Timber' OR user.lastName = 'Saw'))

Puedes combinar tantas expresiones AND y OR como necesites. Si usas .where más de una vez, sobrescribirás todas las expresiones WHERE anteriores.

Nota: ten cuidado con orWhere - si usas expresiones complejas con combinaciones de AND y OR, recuerda que se apilan sin precedentes explícitos. A veces será necesario crear una cadena WHERE manualmente en lugar de usar orWhere.

Agregar expresión HAVING

Agregar una expresión HAVING es sencillo:

createQueryBuilder("user").having("user.name = :name", { name: "Timber" })

Lo que producirá la siguiente consulta SQL:

SELECT ... FROM users user HAVING user.name = 'Timber'

Puedes agregar AND a una expresión HAVING existente:

createQueryBuilder("user")
.having("user.firstName = :firstName", { firstName: "Timber" })
.andHaving("user.lastName = :lastName", { lastName: "Saw" })

Lo que producirá esta consulta SQL:

SELECT ... FROM users user HAVING user.firstName = 'Timber' AND user.lastName = 'Saw'

Puedes agregar OR a una expresión HAVING existente:

createQueryBuilder("user")
.having("user.firstName = :firstName", { firstName: "Timber" })
.orHaving("user.lastName = :lastName", { lastName: "Saw" })

Lo que producirá esta consulta SQL:

SELECT ... FROM users user HAVING user.firstName = 'Timber' OR user.lastName = 'Saw'

Puedes combinar tantas expresiones AND y OR como necesites. Si usas .having más de una vez, sobrescribirás todas las expresiones HAVING anteriores.

Agregar expresión ORDER BY

Agregar una expresión ORDER BY es sencillo:

createQueryBuilder("user").orderBy("user.id")

Lo que producirá:

SELECT ... FROM users user ORDER BY user.id

Puedes cambiar la dirección de ordenamiento de ascendente a descendente (o viceversa):

createQueryBuilder("user").orderBy("user.id", "DESC")

createQueryBuilder("user").orderBy("user.id", "ASC")

Puedes agregar múltiples criterios de orden:

createQueryBuilder("user").orderBy("user.name").addOrderBy("user.id")

También puedes usar un mapa de campos para ordenar:

createQueryBuilder("user").orderBy({
"user.name": "ASC",
"user.id": "DESC",
})

Si usas .orderBy más de una vez, sobrescribirás todas las expresiones ORDER BY anteriores.

Agregar expresión DISTINCT ON (solo Postgres)

Al usar distinct-on con una expresión order-by, la expresión distinct-on debe coincidir con el order-by más a la izquierda. Las expresiones distinct-on se interpretan usando las mismas reglas que order-by. Ten en cuenta que usar distinct-on sin una expresión order-by significa que la primera fila de cada conjunto es impredecible.

Agregar una expresión DISTINCT ON es sencillo:

createQueryBuilder("user").distinctOn(["user.id"]).orderBy("user.id")

Lo que producirá:

SELECT DISTINCT ON (user.id) ... FROM users user ORDER BY user.id

Agregar expresión GROUP BY

Agregar expresión GROUP BY

createQueryBuilder("user").groupBy("user.id")

Lo que producirá esta consulta SQL:

SELECT ... FROM users user GROUP BY user.id

Para agregar más criterios de agrupamiento usa addGroupBy:

createQueryBuilder("user").groupBy("user.name").addGroupBy("user.id")

Si usas .groupBy más de una vez, sobrescribirás todas las expresiones GROUP BY anteriores.

Agregar expresión LIMIT

Agregar una expresión LIMIT es sencillo:

createQueryBuilder("user").limit(10)

Lo que producirá esta consulta SQL:

SELECT ... FROM users user LIMIT 10

La consulta SQL resultante depende del tipo de base de datos (SQL, MySQL, Postgres, etc). Nota: LIMIT puede no funcionar como esperas en consultas complejas con joins o subconsultas. Si estás implementando paginación, se recomienda usar take en su lugar.

Agregar expresión OFFSET

Agregar una expresión SQL OFFSET es sencillo:

createQueryBuilder("user").offset(10)

Lo que producirá esta consulta SQL:

SELECT ... FROM users user OFFSET 10

La consulta SQL resultante depende del tipo de base de datos (SQL, MySQL, Postgres, etc). Nota: OFFSET puede no funcionar como esperas en consultas complejas con joins o subconsultas. Si estás implementando paginación, se recomienda usar skip en su lugar.

Uniendo relaciones

Supongamos que tienes las siguientes entidades:

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"
import { Photo } from "./Photo"

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@OneToMany((type) => Photo, (photo) => photo.user)
photos: Photo[]
}
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"
import { User } from "./User"

@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number

@Column()
url: string

@ManyToOne((type) => User, (user) => user.photos)
user: User
}

Ahora supongamos que quieres cargar al usuario "Timber" con todas sus fotos:

const user = await createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.getOne()

Obtendrás el siguiente resultado:

{
id: 1,
name: "Timber",
photos: [{
id: 1,
url: "me-with-chakram.jpg"
}, {
id: 2,
url: "me-with-trees.jpg"
}]
}

Como puedes ver, leftJoinAndSelect cargó automáticamente todas las fotos de Timber. El primer argumento es la relación que deseas cargar y el segundo es un alias que asignas a la tabla de esta relación. Puedes usar este alias en cualquier parte del constructor de consultas. Por ejemplo, tomemos todas las fotos de Timber que no estén eliminadas.

const user = await createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.andWhere("photo.isRemoved = :isRemoved", { isRemoved: false })
.getOne()

Esto generará la siguiente consulta SQL:

SELECT user.*, photo.* FROM users user
LEFT JOIN photos photo ON photo.user = user.id
WHERE user.name = 'Timber' AND photo.isRemoved = FALSE

También puedes agregar condiciones directamente a la expresión de unión en lugar de usar "where":

const user = await createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo", "photo.isRemoved = :isRemoved", {
isRemoved: false,
})
.where("user.name = :name", { name: "Timber" })
.getOne()

Esto generará la siguiente consulta SQL:

SELECT user.*, photo.* FROM users user
LEFT JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
WHERE user.name = 'Timber'

Uniones internas y izquierdas

Si quieres usar INNER JOIN en lugar de LEFT JOIN, simplemente usa innerJoinAndSelect:

const user = await createQueryBuilder("user")
.innerJoinAndSelect(
"user.photos",
"photo",
"photo.isRemoved = :isRemoved",
{ isRemoved: false },
)
.where("user.name = :name", { name: "Timber" })
.getOne()

Esto generará:

SELECT user.*, photo.* FROM users user
INNER JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
WHERE user.name = 'Timber'

La diferencia entre LEFT JOIN e INNER JOIN es que INNER JOIN no devolverá un usuario si no tiene fotos. LEFT JOIN sí te devolverá el usuario aunque no tenga fotos. Para aprender más sobre los diferentes tipos de unión, consulta la documentación de SQL.

Unión sin selección

Puedes unir datos sin seleccionarlos. Para hacerlo, usa leftJoin o innerJoin:

const user = await createQueryBuilder("user")
.innerJoin("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.getOne()

Esto generará:

SELECT user.* FROM users user
INNER JOIN photos photo ON photo.user = user.id
WHERE user.name = 'Timber'

Esto seleccionará a Timber solo si tiene fotos, pero no devolverá sus fotos.

Uniendo cualquier entidad o tabla

Puedes unir no solo relaciones, sino también otras entidades no relacionadas o tablas. Ejemplos:

const user = await createQueryBuilder("user")
.leftJoinAndSelect(Photo, "photo", "photo.userId = user.id")
.getMany()
const user = await createQueryBuilder("user")
.leftJoinAndSelect("photos", "photo", "photo.userId = user.id")
.getMany()

Funcionalidad de unión y mapeo

Añade profilePhoto a la entidad User, y podrás mapear cualquier dato en esa propiedad usando QueryBuilder:

export class User {
/// ...
profilePhoto: Photo
}
const user = await createQueryBuilder("user")
.leftJoinAndMapOne(
"user.profilePhoto",
"user.photos",
"photo",
"photo.isForProfile = TRUE",
)
.where("user.name = :name", { name: "Timber" })
.getOne()

Esto cargará la foto de perfil de Timber y la asignará a user.profilePhoto. Si quieres cargar y mapear una sola entidad usa leftJoinAndMapOne. Para cargar y mapear múltiples entidades usa leftJoinAndMapMany.

Obteniendo la consulta generada

A veces querrás obtener la consulta SQL generada por QueryBuilder. Para hacerlo, usa getSql:

const sql = createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })
.getSql()

Para depuración puedes usar printSql:

const users = await createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })
.printSql()
.getMany()

Esta consulta devolverá usuarios e imprimirá la sentencia SQL usada en la consola.

Obteniendo resultados en bruto

Hay dos tipos de resultados que puedes obtener con el constructor de consultas SELECT: entidades y resultados en bruto. Normalmente necesitas seleccionar entidades reales de tu base de datos, como usuarios. Para esto usas getOne y getMany. Sin embargo, a veces necesitas datos específicos, como la suma de todas las fotos de usuario. Estos datos no son una entidad, se llaman datos en bruto. Para obtener datos en bruto, usa getRawOne y getRawMany. Ejemplos:

const { sum } = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("SUM(user.photosCount)", "sum")
.where("user.id = :id", { id: 1 })
.getRawOne()
const photosSums = 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 }, ...]

Transmisión de resultados

Puedes usar stream que devuelve un flujo de datos. La transmisión devuelve datos en bruto, y debes manejar manualmente la transformación a entidades:

const stream = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 })
.stream()

Usando paginación

En la mayoría de aplicaciones necesitas funcionalidad de paginación. Esto se usa si tienes componentes de paginación, deslizador de páginas o scroll infinito.

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.take(10)
.getMany()

Esto te dará los primeros 10 usuarios con sus fotos.

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.skip(10)
.getMany()

Esto te dará todos los usuarios excepto los primeros 10, con sus fotos. Puedes combinar estos métodos:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.skip(5)
.take(10)
.getMany()

Esto omitirá los primeros 5 usuarios y tomará 10 usuarios después de ellos.

take y skip pueden parecer equivalentes a limit y offset, pero no lo son. limit y offset pueden no funcionar como esperas en consultas complejas con uniones o subconsultas. Usar take y skip previene estos problemas.

Establecer bloqueo

QueryBuilder soporta bloqueo optimista y pesimista.

Modos de bloqueo

El soporte de modos de bloqueo y las sentencias SQL a las que se traducen se enumeran en la siguiente tabla (las celdas vacías indican que no son compatibles). Cuando un modo de bloqueo especificado no sea compatible, se lanzará un error LockNotSupportedOnGivenDriverError.

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

Para usar el bloqueo pesimista de lectura, utiliza el siguiente método:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("pessimistic_read")
.getMany()

Para usar el bloqueo pesimista de escritura, utiliza el siguiente método:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("pessimistic_write")
.getMany()

Para usar el bloqueo de lectura sucia (dirty read), utiliza el siguiente método:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("dirty_read")
.getMany()

Para usar el bloqueo optimista, utiliza el siguiente método:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("optimistic", existUser.version)
.getMany()

El bloqueo optimista funciona en conjunto con los decoradores @Version y @UpdatedDate.

Bloquear tablas

También puedes bloquear tablas usando el siguiente método:

const users = await dataSource
.getRepository(Post)
.createQueryBuilder("post")
.leftJoin("post.author", "user")
.setLock("pessimistic_write", undefined, ["post"])
.getMany()

Si se proporciona el argumento Lock Tables, solo se especifica la tabla bloqueada en la cláusula FOR UPDATE OF.

setOnLocked

Te permite controlar qué sucede cuando una fila está bloqueada. Por defecto, la base de datos esperará el desbloqueo. Puedes controlar este comportamiento usando setOnLocked.

Para no esperar:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("pessimistic_write")
.setOnLocked("nowait")
.getMany()

Para omitir la fila:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("pessimistic_write")
.setOnLocked("skip_locked")
.getMany()

Soporte de bases de datos para setOnLocked según el modo de bloqueo:

  • Postgres: pessimistic_read, pessimistic_write, for_no_key_update, for_key_share

  • MySQL 8+: pessimistic_read, pessimistic_write

  • MySQL < 8, Maria DB: pessimistic_write

  • Cockroach: pessimistic_write (solo nowait)

Usar índice personalizado

Puedes proporcionar un índice específico para que el servidor de base de datos lo use en algunos casos. Esta característica solo es compatible con MySQL.

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.useIndex("my_index") // name of index
.getMany()

Tiempo máximo de ejecución

Podemos cancelar consultas lentas para evitar saturar el servidor.

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.maxExecutionTime(1000) // milliseconds.
.getMany()

Selección parcial

Si deseas seleccionar solo algunas propiedades de la entidad, puedes usar la siguiente sintaxis:

const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select(["user.id", "user.name"])
.getMany()

Esto solo seleccionará el id y el name de User.

Usar subconsultas

Puedes crear fácilmente subconsultas. Las subconsultas son compatibles en expresiones FROM, WHERE y JOIN. Ejemplo:

const qb = await dataSource.getRepository(Post).createQueryBuilder("post")

const posts = qb
.where(
"post.title IN " +
qb
.subQuery()
.select("user.name")
.from(User, "user")
.where("user.registered = :registered")
.getQuery(),
)
.setParameter("registered", true)
.getMany()

Una forma más elegante de hacer lo mismo:

const posts = await dataSource
.getRepository(Post)
.createQueryBuilder("post")
.where((qb) => {
const subQuery = qb
.subQuery()
.select("user.name")
.from(User, "user")
.where("user.registered = :registered")
.getQuery()
return "post.title IN " + subQuery
})
.setParameter("registered", true)
.getMany()

Alternativamente, puedes crear un constructor de consultas separado y usar su SQL generado:

const userQb = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("user.name")
.where("user.registered = :registered", { registered: true })

const posts = await dataSource
.getRepository(Post)
.createQueryBuilder("post")
.where("post.title IN (" + userQb.getQuery() + ")")
.setParameters(userQb.getParameters())
.getMany()

Puedes crear subconsultas en FROM así:

const userQb = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("user.name", "name")
.where("user.registered = :registered", { registered: true })

const posts = await dataSource
.createQueryBuilder()
.select("user.name", "name")
.from("(" + userQb.getQuery() + ")", "user")
.setParameters(userQb.getParameters())
.getRawMany()

o usando una sintaxis más elegante:

const posts = await dataSource
.createQueryBuilder()
.select("user.name", "name")
.from((subQuery) => {
return subQuery
.select("user.name", "name")
.from(User, "user")
.where("user.registered = :registered", { registered: true })
}, "user")
.getRawMany()

Si quieres agregar una subconsulta como "segundo from", usa addFrom.

También puedes usar subselecciones en sentencias SELECT:

const posts = await dataSource
.createQueryBuilder()
.select("post.id", "id")
.addSelect((subQuery) => {
return subQuery.select("user.name", "name").from(User, "user").limit(1)
}, "name")
.from(Post, "post")
.getRawMany()

Columnas ocultas

Si el modelo que estás consultando tiene una columna con la opción select: false, debes usar la función addSelect para recuperar información de esa columna.

Supongamos que tienes la siguiente entidad:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@Column({ select: false })
password: string
}

Usando un find estándar o una consulta, no recibirás la propiedad password del modelo. Sin embargo, si haces lo siguiente:

const users = await dataSource
.getRepository(User)
.createQueryBuilder()
.select("user.id", "id")
.addSelect("user.password")
.getMany()

Obtendrás la propiedad password en tu consulta.

Consultar filas eliminadas

Si el modelo que estás consultando tiene una columna con el atributo @DeleteDateColumn, el constructor de consultas automáticamente buscará filas que hayan sido "eliminadas lógicamente" (soft deleted).

Supongamos que tienes la siguiente entidad:

import {
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
} from "typeorm"

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@DeleteDateColumn()
deletedAt?: Date
}

Si utilizas un find estándar o una consulta, no recibirás las filas que tengan un valor en esa columna. Sin embargo, si haces lo siguiente:

const users = await dataSource
.getRepository(User)
.createQueryBuilder()
.select("user.id", "id")
.withDeleted()
.getMany()

Obtendrás todas las filas, incluyendo las que están eliminadas.

Expresiones de tabla comunes

Las instancias de QueryBuilder soportan expresiones de tabla comunes si la versión mínima admitida de tu base de datos las soporta. Las expresiones de tabla comunes aún no están soportadas para Oracle.

const users = await connection
.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()

Los valores resultantes de InsertQueryBuilder o UpdateQueryBuilder pueden usarse en Postgres:

const insertQueryBuilder = connection
.getRepository(User)
.createQueryBuilder()
.insert({
name: "John Smith",
})
.returning(["id"])

const users = await connection
.getRepository(User)
.createQueryBuilder("user")
.addCommonTableExpression(insertQueryBuilder, "insert_results")
.where(`user.id IN (SELECT "id" FROM 'insert_results')`)
.getMany()

Consultas de viaje en el tiempo (Time Travel Queries)

Las consultas de viaje en el tiempo actualmente solo están soportadas en la base de datos CockroachDB.

const repository = connection.getRepository(Account)

// create a new account
const account = new Account()
account.name = "John Smith"
account.balance = 100
await repository.save(account)

// imagine we update the account balance 1 hour after creation
account.balance = 200
await repository.save(account)

// outputs { name: "John Smith", balance: "200" }
console.log(account)

// load account state on 1 hour back
account = await repository
.createQueryBuilder("account")
.timeTravelQuery(`'-1h'`)
.getOneOrFail()

// outputs { name: "John Smith", balance: "100" }
console.log(account)

Por defecto, timeTravelQuery() usa la función follower_read_timestamp() si no se pasan argumentos. Para otros argumentos de marca de tiempo soportados e información adicional, consulta la documentación de CockroachDB.

Depuración

Puedes obtener el SQL generado por el constructor de consultas llamando a getQuery() o getQueryAndParameters().

Si solo quieres la consulta, usa getQuery()

const sql = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 })
.getQuery()

Lo cual resulta en:

SELECT `user`.`id` as `userId`, `user`.`firstName` as `userFirstName`, `user`.`lastName` as `userLastName` FROM `users` `user` WHERE `user`.`id` = ?

O si quieres la consulta y los parámetros, puedes obtener un array usando getQueryAndParameters()

const queryAndParams = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 })
.getQueryAndParameters()

Lo cual resulta en:

;[
"SELECT `user`.`id` as `userId`, `user`.`firstName` as `userFirstName`, `user`.`lastName` as `userLastName` FROM `users` `user` WHERE `user`.`id` = ?",
[1],
]