Schema¶
La API de schema de Fluent te permite crear y actualizar el esquema de tu base de datos programáticamente. Normalmente se usa junto a las migraciones para preparar la base de datos para su uso con modelos.
// Un ejemplo de la API schema de Fluent
try await database.schema("planets")
.id()
.field("name", .string, .required)
.field("star_id", .uuid, .required, .references("stars", "id"))
.create()
Para crear un SchemaBuilder
, usa el método schema
en "database". Pásale el nombre de la tabla o colección que quieras afectar. Si estás editando el esquema para un modelo, asegúrate de que este nombre coincida con el schema
del modelo.
Acciones¶
La API de schema soporta crear, actualizar y borrar esquemas. Cada acción soporta un subconjunto de los métodos de la API disponibles.
Crear¶
Llamar a create()
crea una nueva tabla o colección en la base de datos. Todos los métodos para definir nuevos campos y restricciones (constraints) están soportados. Los métodos para actualizaciones y borrados se ignoran.
// Un ejemplo de creación de esquema.
try await database.schema("planets")
.id()
.field("name", .string, .required)
.create()
Si ya existe una tabla o colección con el nombre elegido, se lanzará un error. Para ignorarlo, usa .ignoreExisting()
.
Actualizar¶
Llamar a update()
actualizará una tabla o colección ya existente en la base de datos. Todos los métodos para crear, actualizar y borrar campos y restricciones están soportados.
// Un ejemplo de actualización de esquema.
try await database.schema("planets")
.unique(on: "name")
.deleteField("star_id")
.update()
Borrar¶
Llamar a delete()
borrará una tabla o colección ya existente de la base de datos. Ningún método adicional está soportado.
// Un ejemplo de borrado de esquema.
database.schema("planets").delete()
Campo¶
Pueden añadirse campos cuando se crea o actualiza un esquema.
// Añade un campo nuevo
.field("name", .string, .required)
El primer parámetro es el nombre del campo. Este nombre debería coincidir con la clave usada en la propiedad del modelo. El segundo parámetro es el tipo de dato del campo. Por último, puedes añadir cero o más restricciones.
Tipos de Datos¶
Debajo hay un listado con los tipos de datos soportados.
DataType | Tipo de Swift |
---|---|
.string |
String |
.int{8,16,32,64} |
Int{8,16,32,64} |
.uint{8,16,32,64} |
UInt{8,16,32,64} |
.bool |
Bool |
.datetime |
Date (recomendado) |
.time |
Date (omitiendo día, mes y año) |
.date |
Date (omitiendo hora y minutos del día) |
.float |
Float |
.double |
Double |
.data |
Data |
.uuid |
UUID |
.dictionary |
Ver diccionario |
.array |
Ver array |
.enum |
Ver enum |
Restricciones de Campo¶
Debajo hay un listado con las restricciones de campo (field constraints) soportadas.
FieldConstraint | Descripción |
---|---|
.required |
No permite valores nil . |
.references |
Requiere que el valor del campo coincida con un valor del esquema referenciado. Ver clave externa |
.identifier |
Denota la clave primaria. Ver identificador |
Identificador¶
Si tu modelo usa una propiedad @ID
estándar, puedes usar el helper id()
para crear su campo correspondiente. Esto usa la clave de campo especial .id
y el tipo de valor UUID
.
// Añade campo para identificador por defecto.
.id()
Para tipos de identificador personalizados, necesitarás especificar el campo manualmente.
// Añade campo para identificador personalizado.
.field("id", .int, .identifier(auto: true))
La restricción identifier
debe usarse en un único campo, y denote la clave primaria. La marca (flag) auto
determina si la base de datos deberá generar el valor automáticamente o no.
Actualizar Campo¶
Puedes actualizar el tipo de dato de un campo usando updateField
.
// Actualiza el campo al tipo de dato `double`.
.updateField("age", .double)
Ve a avanzado para más información sobre actualizaciones avanzadas del esquema.
Borrar Campo¶
Puedes borrar un campo de un esquema usando deleteField
.
// Borra el campo "age".
.deleteField("age")
Restricciones¶
Las restricciones (constraints) pueden añadirse al crear o actualizar un esquema. A diferencia de las restricciones de campo, las restricciones de nivel superior (top-level constraints) pueden afectar a varios campos.
Unique¶
Una restricción "unique" requiere que no existan valores duplicados en uno o más campos.
// No permite direcciones de email duplicadas.
.unique(on: "email")
Si varios campos son restringidos, la combinación de los valores de cada campo debe ser única.
// No permite usuarios con el mismo nombre completo.
.unique(on: "first_name", "last_name")
Para borrar una restricción "unique", usa deleteUnique
.
// Elimina la restricción de email duplicado.
.deleteUnique(on: "email")
Nombre de Constraint¶
Fluent generará nombres de restricción únicos por defecto. Sin embargo, puedes querer proporcionar un nombre de restricción personalizado. Puedes hacerlo mediante el parámetro name
.
// No permite direcciones de email duplicadas.
.unique(on: "email", name: "no_duplicate_emails")
Para borrar una restricción con nombre, debes usar deleteConstraint(name:)
.
// Elimina la restricción de email duplicado.
.deleteConstraint(name: "no_duplicate_emails")
Clave Externa¶
Las restricciones de clave externa requieren que el valor de un campo coincida con uno de los valores del campo referenciado. Esto es útil para prevenir el guardado de datos no válidos. Las restricciones de clave externa pueden añadirse como restricciones de campo o de nivel superior.
Para añadir una restricción de clave externa a un campo, usa .references
.
// Ejemplo de añadir una restricción de clave externa de campo.
.field("star_id", .uuid, .required, .references("stars", "id"))
La restricción de arriba requiere que todos los valores en el campo "star_id" deban coincidir con uno de los valores en el campo "id" de "Star".
Esta misma restricción podría añadirse como una de nivel superior usando foreignKey
.
// Ejemplo de añadir una restricción de clave externa de nivel superior.
.foreignKey("star_id", references: "stars", "id")
A diferencia de las restricciones de campo, las de nivel superior pueden añadirse en una actualización del esquema. También pueden nombrarse.
Las restricciones de clave externa soportan las acciones opcionales onDelete
y onUpdate
.
ForeignKeyAction | Descripción |
---|---|
.noAction |
Previene violaciones de clave externa (por defecto). |
.restrict |
Igual que .noAction . |
.cascade |
Propaga los borrados por las claves externas. |
.setNull |
Establece el campo a "null" si la referencia se rompe. |
.setDefault |
Establece el campo a su valor por defecto si la referencia se rompe. |
Debajo hay un ejemplo del uso de acciones de clave externa.
// Ejemplo de añadir una restricción de clave externa de nivel superior.
.foreignKey("star_id", references: "stars", "id", onDelete: .cascade)
Advertencia
Las acciones de clave externa ocurren únicamente en la base de datos, evitando a Fluent. Esto significa que cosas como el middleware de modelo o el borrado no permanente (soft-delete) pueden no funcionar correctamente.
SQL¶
El parámetro .sql
te permite añadir SQL arbitrario a tu esquema. Esto es útil para añadir restricciones o tipos de datos específicos.
Un caso de uso habitual es definir un valor por defecto para un campo:
.field("active", .bool, .required, .sql(.default(true)))
o inclusive un valor por defecto para una marca de tiempo (timestamp):
.field("created_at", .datetime, .required, .sql(.default(SQLFunction("now"))))
Diccionario¶
El tipo de dato diccionario es capaz de guardar valores de diccionario anidados. Esto inclute structs conformadas con Codable
y diccionarios de Swift con un valor Codable
.
Nota
Los conectores de bases de datos SQL de Fluent guardan diccionarios anidados en columnas JSON.
Toma el struct Codable
a continuación.
struct Pet: Codable {
var name: String
var age: Int
}
Como este struct Pet
es Codable
, puede guardarse en un @Field
.
@Field(key: "pet")
var pet: Pet
Este campo puede guardarse usando el tipo de dato .dictionary(of:)
.
.field("pet", .dictionary, .required)
Como los tipos Codable
son diccionarios heterogéneos, no especificamos el parámetro of
.
Si los valores del diccionario fueran homogéneos, por ejemplo [String: Int]
, el parámetro of
especificaría el tipo del valor.
.field("numbers", .dictionary(of: .int), .required)
Las claves de los diccionarios deben ser siempre cadenas (strings).
Array¶
El tipo de dato array es capaz de guardar arrays anidados. Esto incluye arrays de Swift que contengan valores Codable
y tipos Codable
que usen un contenedor sin clave.
Toma el siguiente @Field
que guarda un array de cadenas.
@Field(key: "tags")
var tags: [String]
Este campo puede guardarse usando el tipo de dato .array(of:)
.
.field("tags", .array(of: .string), .required)
Como el array es homogéneo, especificamos el parámetro of
.
Los Array
s de Swift que puedan codificarse siempre tendrán un tipo de valor homogéneo. Los tipos Codable
personalizados que serializan valores heterogéneos a contenedores sin clave son una excepción y deberían usar el tipo de dato .array
.
Enum¶
El tipo de dato enum es capaz de guardar enumeraciones de Swift con representación en cadenas (string backed) de forma nativa. Los enums nativos de bases de datos proporcionan una capa extra de seguridad de tipos a tu base de datos y pueden llegar a ser más eficientes que los enums normales (raw enums).
Para definir un enum nativo de base de datos, usa el método enum
en Database
. Usa case
para definir cada caso del enum.
// Un ejemplo de creación de enums.
database.enum("planet_type")
.case("smallRocky")
.case("gasGiant")
.case("dwarf")
.create()
Una vez se ha creado el enum, puedes usar el método read()
para generar un tipo de dato para el campo de tu esquema.
// Un ejemplo de leer un enum y usarlo para definir un campo nuevo.
database.enum("planet_type").read().flatMap { planetType in
database.schema("planets")
.field("type", planetType, .required)
.update()
}
// O
let planetType = try await database.enum("planet_type").read()
try await database.schema("planets")
.field("type", planetType, .required)
.update()
Para actualizar un enum, llama a update()
. Pueden borrarse los casos de un enum ya existente.
// Un ejemplo de actualización de un enum.
database.enum("planet_type")
.deleteCase("gasGiant")
.update()
Para borrar un enum, llama a delete()
.
// Un ejemplo de borrado de un enum.
database.enum("planet_type").delete()
Acoplamiento de Modelos¶
La construcción de esquemas está desacoplada de los modelos a propósito. A diferencia de la construcción de consultas, la de esquemas no usa keypaths y está escrita totalmente en cadenas. Esto es importante dado que las definiciones de esquemas, especialmente las escritas para migraciones, pueden necesitar referenciar propiedades de modelos que ya no existen.
Para entender esto mejor, examina la migración de ejemplo a continuación.
struct UserMigration: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("users")
.field("id", .uuid, .identifier(auto: false))
.field("name", .string, .required)
.create()
}
func revert(on database: Database) async throws {
try await database.schema("users").delete()
}
}
Asumamos que esta migración ya se ha subido a producción. Ahora asumamos que necesitamos hacer el siguiente cambio al modelo de User.
- @Field(key: "name")
- var name: String
+ @Field(key: "first_name")
+ var firstName: String
+
+ @Field(key: "last_name")
+ var lastName: String
Podemos hacer los ajustes necesarios del esquema de la base de datos con la siguiente migración.
struct UserNameMigration: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("users")
.field("first_name", .string, .required)
.field("last_name", .string, .required)
.update()
// Actualmente no es posible expresar esta actualización sin usar SQL personalizado.
// Esto tampoco intenta dividir el nombre en nombre y apellido,
// ya que eso requiere sintaxis específica de la base de datos.
try await User.query(on: database)
.set(["first_name": .sql(embed: "name"))
.run()
try await database.schema("users")
.deleteField("name")
.update()
}
func revert(on database: Database) async throws {
try await database.schema("users")
.field("name", .string, .required)
.update()
try await User.query(on: database)
.set(["name": .sql(embed: "concat(first_name, ' ', last_name)"))
.run()
try await database.schema("users")
.deleteField("first_name")
.deleteField("last_name")
.update()
}
}
Cabe destacar que para que esta migración funcione, necesitamos poder referenciar tanto el campo name
eliminado como los nuevos campos firstName
y lastName
al mismo tiempo. Es más, la migración UserMigration
original debería continuar siendo válida. Esto no sería posible de hacer mediante keypaths.
Configurando el Espacio del Modelo¶
Para definir el espacio para un modelo, pasa el espacio al schema(_:space:)
cuando vayas a crear la tabla. Por ejemplo:
try await db.schema("planets", space: "mirror_universe")
.id()
// ...
.create()