Saltar a contenido

Query

La API de consultas (query) de Fluent te permite crear, leer, actualizar y borrar modelos de la base de datos. Soporta el filtrado de resultados, uniones (joins), fragmentaciones (chunking), agregados y más.

// Un ejemplo de la API de consultas de Fluent.
let planets = try await Planet.query(on: database)
    .filter(\.$type == .gasGiant)
    .sort(\.$name)
    .with(\.$star)
    .all()

Los constructores de consultas (query builders) están ligados a un único tipo de modelo y pueden crearse con el método estático query. También pueden crearse pasando el tipo del modelo al método query en un objeto de base de datos (database).

// También crea un constructor de consultas.
database.query(Planet.self)

Nota

Debes indicar import Fluent en el archivo con tus consultas para que el compilador pueda ver las funciones de ayuda (helpers) de Fluent.

All

El método all() devuelve una colección (array) de modelos.

// Recupera todos los planetas.
let planets = try await Planet.query(on: database).all()

El método all también soporta recuperar un solo campo del conjunto de resultados.

// Recupera todos los nombres de los planetas.
let names = try await Planet.query(on: database).all(\.$name)

First

El método first() devuelve un único modelo opcional. Si el resultado de la consulta es más de un modelo, solo se devuelve el primero. Si la consulta no tiene resultados, se devuelve nil.

// Recupera el primer planeta llamado Earth.
let earth = try await Planet.query(on: database)
    .filter(\.$name == "Earth")
    .first()

Consejo

Si estás usando EventLoopFutures, este método puede combinarse con unwrap(or:) para devolver un modelo no opcional o lanzar un error.

Filtrado

El método filter te permite restringir los modelos incluidos en el conjunto de resultados. Exiten varias sobrecargas para este método.

Filtrado por Valor

El método filter más común acepta una expresión de operador con un valor.

// Un ejemplo de filtrado por valor de campo.
Planet.query(on: database).filter(\.$type == .gasGiant)

Estas expresiones de operador aceptan un keypath de campo en el lado izquierdo y un valor en el derecho. El valor suministrado debe ser del mismo tipo que el del lado izquierdo, y queda atado a la consulta resultante. Las expresiones de filtrado son de tipado estricto, permitiendo el uso de sintaxis "leaning-dot".

Debajo hay una lista con todos los operadores de valor soportados.

Operador Descripción
== Igual que.
!= Distinto de.
>= Mayor o igual que.
> Mayor que.
< Menor que.
<= Menor o igual que.

Filtrado por Campo

El método filter soporta comparar dos campos.

// Todos los usuarios con el mismo nombre y apellido.
User.query(on: database)
    .filter(\.$firstName == \.$lastName)

El filtrado por campo soporta los mismos operadores que el filtrado por valor.

Filtrado por Subconjunto

El método filter soporta comprobar si el valor de un campo existe dentro de un conjunto de valores dados.

// Todos los planetas de tipo gas giant o small rocky.
Planet.query(on: database)
    .filter(\.$type ~~ [.gasGiant, .smallRocky])

El conjunto de valores suministrado puede ser cualquier Collection de Swift cuyo tipo Element sea igual al tipo de valor del campo.

Debajo hay una lista con todos los operadores de subconjuntos soportados.

Operador Descripción
~~ Valor en el conjunto.
!~ Valor fuera del conjunto.

Filtro "Contiene"

El método filter soporta comprobar cuando el valor de un campo de tipo cadena (string) contiene una subcadena dada.

// Todos los planetas que empiecen por M
Planet.query(on: database)
    .filter(\.$name =~ "M")

Estos operadores solo están disponibles en campos con valores de cadena.

Debajo hay una lista con todos los operadores "contiene" soportados.

Operator Description
~~ Contiene subcadena.
!~ No contiene subcadena.
=~ Coinciden los prefijos.
!=~ No coinciden los prefijos.
~= Coinciden los sufijos.
!~= No coinciden los sufijos.

Grupo

Por defecto, todos los filtros añadidos a una consulta necesitarán coincidir. Los constructores de consultas soportan crear un grupo de filtros donde solo uno necesitará coincidir.

// Todos los planetas cuyo nombre sea Earth o Mars
Planet.query(on: database).group(.or) { group in
    group.filter(\.$name == "Earth").filter(\.$name == "Mars")
}.all()

El método group soporta combinar filtros mediante lógica de and o or. Estos grupos pueden anidarse indefinidamente. Los filtros de nivel superior ("top-level") pueden considerarse como si estuvieran en un grupo and.

Agregados

Los constructores de consultas soportan varios métodos para realizar cálculos en un conjunto de valores, como contar o hacer la media.

// Número de planetas en la base de datos. 
Planet.query(on: database).count()

Todos los métodos de agregación, salvo count, requieren de un "key path" a un campo.

// Nombre más bajo en orden alfabético.
Planet.query(on: database).min(\.$name)

Debajo hay una lista con todos los operadores de agregación disponibles.

Agregado Descripción
count Número de resultados.
sum Suma de los valores resultado.
average Media de los valores resultado.
min Valor de resultado mínimo.
max Valor de resultado máximo.

Todos los métodos de agregación excepto count devuelven el tipo de valor del campo como resultado. count siempre devuelve un entero.

Fragmentos

Los constructores de consultas soportan devolver un conjunto de valores como fragmentos (chunks) separados. Esto puede ayudarte a controlar el uso de memoria al manejar grandes lecturas de bases de datos.

// Recupera todos los planetas en fragmentos de, como máximo, 64 a la vez.
Planet.query(on: self.database).chunk(max: 64) { planets in
    // Handle chunk of planets.
}

El closure suministrado será llamado cero o más veces dependiendo del número total de resultados. Cada ítem devuelto es un Result que contiene, bien el modelo, bien un error devuelto al intentar decodificar la entrada de la base de datos.

Campos

Por defecto, una consulta leerá todos los campos de un modelo de la base de datos. Puedes seleccionar únicamente un subconjunto de los campos del modelo mediante el método field.

// Solo selecciona los campos id y nombre del planeta
Planet.query(on: database)
    .field(\.$id).field(\.$name)
    .all()

Cualquier modelo no seleccionado durante una consulta quedará como no inicializado. Intentar acceder a campos no inicializados directamente dará lugar a un error fatal. Para comprobar si el valor de un campo del modelo está establecido, usa la propiedad value.

if let name = planet.$name.value {
    // Name se recuperó.
} else {
    // Name no fue recuperado.
    // Acceder a `planet.name` fallará.
}

Unique

El método de constructor de consultas unique causa la devolución de valores no duplicados.

// Devuelve todos los nombres de usuario únicos. 
User.query(on: database).unique().all(\.$firstName)

unique es especialmente útil al recuperar un sólo campo con all. Sin embargo, también puedes seleccionar múltiples campos usando el método field. Como los identificadores del modelo son siempre únicos,deberías evitar seleccionarlos al usar unique.

Rangos

Los métodos de constructor de consultas range te permiten seleccionar un subconjunto de resultados usando rangos de Swift.

// Recupera los 5 primeros planetas.
Planet.query(on: self.database)
    .range(..<5)

Los valores de los rangos son enteros positivos empezando desde cero. Ver más sobre los rangos de Swift.

// Salta los 2 primeros resultados.
.range(2...)

Uniones

El método de constructor de consultas join te permite incluir los campos de otro modelo en tu conjunto de resultados. Puedes unir a tu consulta más de un modelo.

// Recupera todos los planetas con una estrella llamada Sun.
Planet.query(on: database)
    .join(Star.self, on: \Planet.$star.$id == \Star.$id)
    .filter(Star.self, \.$name == "Sun")
    .all()

El parámetro on acepta una expresión de igualdad entre dos campos. Uno de los campos debe existir ya en el conjunto de resultados actual. El otro campo debe existir en el modelo que se está uniendo. Estos campos deben tener el mismo tipo de valor.

La mayoría de métodos de constructores de consultas, como filter y sort, soportan modelos unidos. Si un método soporta modelos unidos, aceptará el modelo unido como el primer parámetro.

// Ordenado por el campo "name" unido al modelo Star.
.sort(Star.self, \.$name)

Las consultas que usan uniones seguirán devolviendo una colección (array) del modelo base. Para acceder al modelo unido, usa el metodo joined.

// Accediendo al modelo unido desde el resultado de la consulta.
let planet: Planet = ...
let star = try planet.joined(Star.self)

Alias de Modelo

Los alias de modelo te permiten unir el mismo modelo a una consulta varias veces. Para declarar alias de modelo, crea uno o más tipos conformados a ModelAlias.

// Ejemplo de alias de modelo.
final class HomeTeam: ModelAlias {
    static let name = "home_teams"
    let model = Team()
}
final class AwayTeam: ModelAlias {
    static let name = "away_teams"
    let model = Team()
}

Estos tipos referencian al modelo al que se le ha dado el alias mediante la propiedad model. Una vez creados, puedes usar los alias de modelo como modelos normales en un constructor de consultas.

// Recupera todas las coincidencias donde el nombre del equipo local es Vapor
// y ordena por el nombre del equipo invitado.
let matches = try await Match.query(on: self.database)
    .join(HomeTeam.self, on: \Match.$homeTeam.$id == \HomeTeam.$id)
    .join(AwayTeam.self, on: \Match.$awayTeam.$id == \AwayTeam.$id)
    .filter(HomeTeam.self, \.$name == "Vapor")
    .sort(AwayTeam.self, \.$name)
    .all()

Todos los campos del modelo son accesibles por el tipo del alias de modelo mediante @dynamicMemberLookup.

// Accede al modelo unido desde el resultado.
let home = try match.joined(HomeTeam.self)
print(home.name)

Actualizar

Los constructores de consultas soportan actualizar más de un modelo a la vez usando el método update.

// Actualiza todos los planetas llamados "Pluto"
Planet.query(on: database)
    .set(\.$type, to: .dwarf)
    .filter(\.$name == "Pluto")
    .update()

update soporta los métodos set, filter y range.

Borrar

Los constructores de consultas soportan borrar más de un modelo a la vez usando el método delete

// Borra todos los planetas llamados "Vulcan"
Planet.query(on: database)
    .filter(\.$name == "Vulcan")
    .delete()

delete soporta el método filter.

Paginar

La API de consultas de Fluent soporta la paginación automática de resultados usando el método paginate.

// Ejemplo de paginación basada en peticiones.
app.get("planets") { req in
    try await Planet.query(on: req.db).paginate(for: req)
}

El método paginate(for:) usará los parámetros page y per disponibles en el URI de la petición para devolver el conjunto de resultados deseados. Los metadatos acerca de la página actual y el número total de resultados están incluidos en la clave metadata.

GET /planets?page=2&per=5 HTTP/1.1

La petición de arriba produciría una respuesta estructurada de la siguiente manera.

{
    "items": [...],
    "metadata": {
        "page": 2,
        "per": 5,
        "total": 8
    }
}

El número de página empieza en 1. También puedes hacer una petición de página de forma manual.

// Ejemplo de paginación manual.
.paginate(PageRequest(page: 1, per: 2))

Ordenar

Los resultados de una consulta pueden ordenarse según los valores de los campos usando el método sort.

// Recupera los planetas ordenados por su nombre.
Planet.query(on: database).sort(\.$name)

Pueden requerirse ordenaciones adicionales como recurso ("fallback") en caso de empate. Los fallbacks se usarán en el orden en que se añadieron al constructor de consultas.

// Recupera los usuarios ordenados por su nombre. Si dos usuarios tienen el mismo nombre, se ordenan por su edad.
User.query(on: database).sort(\.$name).sort(\.$age)