# Modelos Los modelos representan datos guardados en tablas o colecciones en tu base de datos. Los modelos tienen uno o más campos que guardan valores codificables. Todos los modelos tienen un identificador único. Los empaquetadores de propiedades (property wrappers) se usan para denotar identificadores, campos y relaciones. A conntinuación hay un ejemplo de un modelo simple con un campo. Cabe destacar que los modelos no describen el esquema completo (restricciones, índices, claves foráneas...) de la base de datos. Los esquemas se definen en [migraciones](migration.md). Los modelos se centran en representar los datos guardados en los esquemas de tu base de datos. ```swift final class Planet: Model { // Nombre de la tabla o colección. static let schema = "planets" // Identificador único para este Planet. @ID(key: .id) var id: UUID? // El nombre del Planet. @Field(key: "name") var name: String // Crea un nuevo Planet vacío. init() { } // Crea un nuevo Planet con todas sus propiedades establecidas. init(id: UUID? = nil, name: String) { self.id = id self.name = name } } ``` ## Esquema Todos los modelos requieren una propiedad estática de sólo lectura (get-only) llamada `schema`. Esta cadena hace referencia al nombre de la tabla o colección que el modelo representa. ```swift final class Planet: Model { // Nombre de la tabla o colección. static let schema = "planets" } ``` Al consultar este modelo, los datos serán recuperados de y guardados en el esquema llamado `"planets"`. !!! tip "Consejo" El nombre del esquema típicamente es el nombre de la clase en plural y en minúsculas. ## Identificador Todos los modelos deben tener una propiedad `id` definida usando el property wrapper `@ID`. Este campo identifica instancias de tu modelo unívocamente. ```swift final class Planet: Model { // Identificador único para este Planet. @ID(key: .id) var id: UUID? } ``` Por defecto la propiedad `@ID` debería usar la clave especial `.id`, que se resuelve en una clave apropiada para el conector (driver) de la base de datos subyacente. Para SQL es `"id"` y para NoSQL es `"_id"`. El `@ID` también debería ser de tipo `UUID`. Este es el único valor de identificación soportado por todos los conectores de bases de datos. Fluent generará nuevos identificadores UUID automáticamente en la creación de modelos. `@ID` tiene un valor opcional dado que los modelos no guardados pueden no tener un identificador. Para obtener el identificador o lanzar un error, usa `requireID`. ```swift let id = try planet.requireID() ``` ### Exists `@ID` tiene una propiedad `exists` que representa si el modelo existe o no en la base de datos. Cuando inicializas un modelo, el valor es `false`. Después de guardar un modelo o recuperarlo de la base de datos, el valor `true`. Esta propiedad es mutable. ```swift if planet.$id.exists { // Este modelo existe en la base de datos. } ``` ### Identificador Personalizado Fluent soporta claves y tipos de identificadores personalizados mediante la sobrecarga `@ID(custom:)`. ```swift final class Planet: Model { // Identificador único para este Planet. @ID(custom: "foo") var id: Int? } ``` El ejemplo anterior usa un `@ID` con la clave personalizada `"foo"` y el tipo identificador `Int`. Esto es compatible con bases de datos SQL que usen claves primarias de incremento automático, pero no es compatible con NoSQL. Los `@ID`s personalizados permiten al usuario especificar cómo debería generarse el identificador usando el parámetro `generatedBy`. ```swift @ID(custom: "foo", generatedBy: .user) ``` El parámetro `generatedBy` soporta estos casos: |Generated By|Descripción| |-|-| |`.user`|Se espera que la propiedad `@ID` sea establecida antes de guardar un nuevo modelo.| |`.random`|El tipo del valor `@ID` debe conformarse a `RandomGeneratable`.| |`.database`|Se espera que la base de datos genere el valor durante el guardado.| Si el parámetro `generatedBy` se omite, Fluent intentará inferir un caso apropiado basado en el tipo de valor del `@ID`. Por ejemplo, `Int` usará por defecto la generación `.database` si no se especifica lo contrario. ## Inicializador Los modelos deben tener un método inicializador (init) vacío. ```swift final class Planet: Model { // Crea un nuevo Planet vacío. init() { } } ``` Fluent necesita este método internamente para inicializar modelos devueltos por consultas. También es usado para el espejado (reflection). Puedes querer añadir a tu modelo un inicializador de conveniencia que acepte todas las propiedades. ```swift final class Planet: Model { // Crea un nuevo Planet con todas las propiedades establecidas. init(id: UUID? = nil, name: String) { self.id = id self.name = name } } ``` El uso de inicializadores de conveniencia facilita añadir nuevas propiedades al modelo en un futuro. ## Campo Los modelos pueden tener o no propiedades `@Field` para guardar datos. ```swift final class Planet: Model { // El nombre del Planet. @Field(key: "name") var name: String } ``` Los campos requieren que la clave de la base de datos sea definida explícitamente. No es necesario que sea igual que el nombre de la propiedad. !!! tip "Consejo" Fluent recomienda usar `snake_case` para claves de bases de datos y `camelCase` para nombres de propiedades. Los valores de los campos pueden ser de cualquier tipo conformado a `Codable`. Guardar estructuras y colecciones (arrays) anidados en `@Field` está soportado, pero las operaciones de filtrado son limitadas. Ve a [`@Group`](#group) para una alternativa. Para campos que contengan un valor opcional, usa `@OptionalField`. ```swift @OptionalField(key: "tag") var tag: String? ``` !!! warning "Advertencia" Un campo no opcional que tenga una propiedad observadora `willSet` que referencie su valor actual o una propiedad observadora `didSet` que referencie a `oldValue` dará un error fatal. ## Relaciones Los modelos pueden tener ninguna o más propiedades relacionales referenciando otros modelos, `@Parent`, `@Children` y `@Siblings`. Aprende más sobre las relaciones en la sección de [relaciones](relations.md). ## Timestamp `@Timestamp` es un tipo especial de `@Field` que guarda un `Foundation.Date`. Las timestamps (marcas de tiempo) son establecidas automáticamente por Fluent dependiendo del disparador (trigger) seleccionado. ```swift final class Planet: Model { // Cuando este Planet fue creado. @Timestamp(key: "created_at", on: .create) var createdAt: Date? // Cuando este Planet fue actualizado por última vez. @Timestamp(key: "updated_at", on: .update) var updatedAt: Date? } ``` `@Timestamp` soporta los siguiente triggers. |Trigger|Descripción| |-|-| |`.create`|Establecido cuando una nueva instancia de modelo es guardada en la base de datos.| |`.update`|Establecido cuando una instancia de modelo ya existente es guardada en la base de datos.| |`.delete`|Establecido cuando un modelo es eliminado de la base de datos. Ver [soft delete](#soft-delete).| El valor de fecha de los `@Timestamp`s es opcional y debería establecer a `nil` al inicializar un nuevo modelo. ### Formato de Timestamp Por defecto `@Timestamp` usará una codificación eficiente para `datetime` basada en el controlador de tu base de datos. Puedes personalizar cómo la marca de tiempo se guarda en la base de datos usando el parámetro `format`. ```swift // Guarda un timestamp formateado según ISO 8601 representando // cuando este modelo fue actualizado por última vez. @Timestamp(key: "updated_at", on: .update, format: .iso8601) var updatedAt: Date? ``` Cabe destacar que la migración asociada al ejemplo con `.iso8601` necesitaría ser guardado en formato `.string`. ```swift .field("updated_at", .string) ``` A continuación hay un listado de los formatos disponibles para timestamp. |Formato|Descripción|Tipo| |-|-|-| |`.default`|Usa codificación eficiente de `datetime` específica para la base de datos.|Date| |`.iso8601`|Cadena [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). Soporta parámetro `withMilliseconds`.|String| |`.unix`|Segundos desde época Unix incluyendo fracción.|Double| Puedes acceder el valor puro (raw value) del timestamp directamente usando la propiedad `timestamp`. ```swift // Establece manualmente el timestamp en este // @Timestamp formateado con ISO 8601. model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00" ``` ### Soft Delete Añadir un `@Timestamp` que use el disparador `.delete`a tu modelo habilitará el borrado no permanente (soft-deletion). ```swift final class Planet: Model { // Cuando este Planet fue borrado. @Timestamp(key: "deleted_at", on: .delete) var deletedAt: Date? } ``` Los modelos eliminados de manera no permanente siguen existiendo en la base de datos después de su borrado, pero no serán devueltos en las consultas. !!! tip "Consejo" Puedes establecer manualmente un timestamp de borrado con una fecha en el futuro. Puede usarse como una fecha de caducidad. Para forzar el borrado de un modelo de eliminación no permanente en la base de datos, usa el parámetro `force` en `delete`. ```swift // Elimina el modelo de la base de datos // inclusive si es soft-deletable model.delete(force: true, on: database) ``` Para restaurar un modelo eliminado no permanentemente, usa el método `restore`. ```swift // Limpia en timestamp de borrado del modelo // para que pueda devolverse en consultas. model.restore(on: database) ``` Para incluir modelos eliminados no permanentemente en una consulta, usa `withDeleted`. ```swift // Recupera todos los planetas, incluidos los eliminados no permanentemente. Planet.query(on: database).withDeleted().all() ``` ## Enum `@Enum` es un tipo especial de `@Field` para guardar tipos representables mediante cadenas como enumeraciones nativas de bases de datos. Las enumeraciones nativas de bases de datos proporcionan una capa añadida de seguridad de tipos a tu base de datos y pueden ser más óptimas que las enumeraciones en bruto (raw enums). ```swift // Enum Codable y representable mediante String para tipos de animales. enum Animal: String, Codable { case dog, cat } final class Pet: Model { // Guarda el tipo de animal como un enum nativo de base de datos. @Enum(key: "type") var type: Animal } ``` Solo los tipos conformados a `RawRepresentable` donde el `RawValue` es `String` son compatibles con `@Enum`. Los enums soportados por `String` cumplen este requisito por defecto. Para guardar una enumeración opcional, usa `@OptionalEnum`. La base de datos debe prepararse para manejar enumeraciones via una migración. Ver [enum](schema.md#enum) para más información. ### Raw Enums Cualquier enumeración respaldada por una tipo de dato `Codable`, como `String` o `Int`, puede guardarse en `@Field`. Se guardará en la base de datos como el dato en bruto. ## Grupo `@Group` te permite guardar un grupo anidado de campos en tu modelo como una única propiedad. Al contrario que las structs conformadas con Codable guardadas en un `@Field`, los campos en un `@Group` son consultables. Fluent consigue esto guardando `@Group` como una estructura plana en la base de datos. Para usar un `@Group`, primero define la estructura anidada que quieres guardar usando el protocolo `Fields`. Es muy similar a `Model`, pero no requiere identificador ni nombre de esquema. Aquí puedes guardar muchas de las propiedades soportadas por `Model`, como `@Field`, `@Enum`, o inclusive otro `@Group`. ```swift // Una mascota (pet) con nombre y tipo de animal. final class Pet: Fields { // El nombre de la mascota. @Field(key: "name") var name: String // El tipo de la mascota. @Field(key: "type") var type: String // Crea un nuevo Pet vacío. init() { } } ``` Después de crear la definición de la estructura anidada (fields), puedes usarla como valor de una propiedad `@Group`. ```swift final class User: Model { // La mascota anidada al usuario. @Group(key: "pet") var pet: Pet } ``` Los campos de un `@Group` son accesibles mediante la sintaxis de punto (dot-syntax). ```swift let user: User = ... print(user.pet.name) // String ``` Puedes consultar campos anidados de la manera habitual usando sintaxis de punto en los empquetadores de propiedad (property wrappers). ```swift User.query(on: database).filter(\.$pet.$name == "Zizek").all() ``` En la base de datos, `@Group` se guarda como una estructura plana con claves unidas mediante `_`. A continuación hay un ejemplo de cómo se vería `User` en la base de datos. |id|name|pet_name|pet_type| |-|-|-|-| |1|Tanner|Zizek|Cat| |2|Logan|Runa|Dog| ## Codable Los modelos se conforman a `Codable` por defecto. Esto te permite usar tus modelos con la [API de contenido](../basics/content.es.md) de Vapor añadiendo la conformidad al protocolo `Content`. ```swift extension Planet: Content { } app.get("planets") { req async throws in // Devuelve un array de todos los planetas. try await Planet.query(on: req.db).all() } ``` Al serializar desde/hacia `Codable`, las propiedades del modelo usarán sus nombres de variable en lugar de las claves. Las relaciones serán serializadas como estructuras anidadas y cualquier carga previa (eager loading) de datos será incluida. !!! info "Información" Recomendamos que para casi todos los casos utilices un DTO en lugar de un modelo para tus respuestas de API y los cuerpos de tus solicitudes. Consulta [Objeto de transferencia de datos](#data-transfer-object) para obtener más información. ### Data Transfer Object La conformidad por defecto del modelo a `Codable` puede facilitar el prototipado y usos simples. Sin embargo, expone la información subyacente de la base de datos a la API. Esto generalmente no es deseable desde un punto de vista de seguridad —devolver campos sensibles como el hash de la contraseña de un usuario es una mala idea— y desde una perspectiva de usabilidad. Hace difícil cambiar el esquema de la base de datos sin romper la API, aceptar o devolver datos en un formato diferente, o agregar o eliminar campos de la API. En la mayoría de los casos, deberías usar un DTO o data transfer object (objeto de transferencia de datos) en lugar de un modelo (también conocido como domain transfer object). Un DTO es un tipo `Codable` separado que representa la estructura de datos que deseas codificar o decodificar. Esto desacopla tu API del esquema de la base de datos y te permite realizar cambios en tus modelos sin romper la API pública de tu aplicación, tener diferentes versiones y hacer que tu API sea más agradable de usar para tus clientes. Toma como base el siguiente modelo de `User` para los ejemplos a continuación. ```swift // Modelo de usuario reducido como referencia. final class User: Model { @ID(key: .id) var id: UUID? @Field(key: "first_name") var firstName: String @Field(key: "last_name") var lastName: String } ``` Un caso de uso común de DTOs es la implementación de peticiones (request) `PATCH`. Estas peticiones solo incluyen valores para los campos que deberían actualizarse. La decodificación de un `Model` directamente de esa petición fallaría si cualquiera de los campos no opcionales faltase. En el siguiente ejemplo puedes ver cómo se usa un DTO para decodificar datos de la petición y actualizar un modelo. ```swift // Structure of PATCH /users/:id request. struct PatchUser: Decodable { var firstName: String? var lastName: String? } app.patch("users", ":id") { req async throws -> User in // Decodificar los datos de la petición. let patch = try req.content.decode(PatchUser.self) // Recuperar el usuario deseado de la base de datos. guard let user = try await User.find(req.parameters.get("id"), on: req.db) else { throw Abort(.notFound) } // Si se diera un nombre, se actualiza. if let firstName = patch.firstName { user.firstName = firstName } // Si se diera un apellido, se actualiza. if let lastName = patch.lastName { user.lastName = lastName } // Guarda el usuario y lo devuelve. try await user.save(on: req.db) return user } ``` Otro uso común de DTO es personalizar el formato de las respuestas de tu API. El ejemplo a continuación muestra cómo puede usarse un DTO para añadir un campo computado a una respuesta. ```swift // Estructura de la respuesta GET /users. struct GetUser: Content { var id: UUID var name: String } app.get("users") { req async throws -> [GetUser] in // Recuperar todos los usuarios de la base de datos. let users = try await User.query(on: req.db).all() return try users.map { user in // Convertir cada usuario al tipo de devolución para GET. try GetUser( id: user.requireID(), name: "\(user.firstName) \(user.lastName)" ) } } ``` Otro caso de uso común es al trabajar con relaciones, como relaciones de padre o de hijos. Consulta [la documentación de Parent](relations.es.md#codificación-y-decodificación-de-relaciones-parent) para ver un ejemplo de cómo usar un DTO para facilitar la decodificación de un modelo con una relación `@Parent`. Incluso si la estructura del DTO es idéntica a la conformidad `Codable` del modelo, tenerlo como un tipo separado puede ayudar a mantener organizados los proyectos grandes. Si alguna vez necesitas realizar un cambio en las propiedades de tus modelos, no tendrás que preocuparte por romper la API pública de tu aplicación. También puedes considerar colocar tus DTOs en un paquete separado que pueda ser compartido con los consumidores de tu API y agregar conformidad a `Content` en tu aplicación Vapor. ## Alias El protocolo `ModelAlias` te permite identificar de manera unívoca un modelo unido varias veces en una consulta. Para más información, consulta [uniones](query.md#join). ## Guardar Para guardar un modelo en la base de datos, usa el método `save(on:)`. ```swift planet.save(on: database) ``` Este método llamará internamente a `create` o `update` dependiendo de si el modelo ya existe o no en la base de datos. ### Crear Puedes llamar al método `create` para guardar un modelo nuevo en la base de datos. ```swift let planet = Planet(name: "Earth") planet.create(on: database) ``` `create` también está disponible en una colección (array) de modelos. Con esto puedes guardar en la base de datos todos los modelos en una única remesa (batch) / consulta (query). ```swift // Ejemplo de batch create. [earth, mars].create(on: database) ``` !!! warning "Advertencia" Los modelos que usen [`@ID(custom:)`](#custom-identifier) con el generador `.database` (normalmente `Int`s de incremento automático) no tendrán sus identificadores recién creados después del batch create. Para situaciones en las que necesites acceder a los identificadores llama a `create` en cada modelo. Para crear una colección de modelos individualmente usa `map` + `flatten`. ```swift [earth, mars].map { $0.create(on: database) } .flatten(on: database.eventLoop) ``` Si estás usando `async`/`await` puedes usar: ```swift await withThrowingTaskGroup(of: Void.self) { taskGroup in [earth, mars].forEach { model in taskGroup.addTask { try await model.create(on: database) } } } ``` ### Actualizar Puedes llamar al método `update` para guardar un modelo recuperado de la base de datos. ```swift guard let planet = try await Planet.find(..., on: database) else { throw Abort(.notFound) } planet.name = "Earth" try await planet.update(on: database) ``` Para actualizar una colección de modelos usa `map` + `flatten`. ```swift [earth, mars].map { $0.update(on: database) } .flatten(on: database.eventLoop) // TOOD ``` ## Consultar Los modelos exponen un método estático llamado `query(on:)` que devuelve un constructor de consultas (query builder). ```swift Planet.query(on: database).all() ``` Aprende más sobre las consultas en la sección de [consulta](query.md). ## Encontrar Los modelos tienen un método estático llamado `find(_:on:)` para encontrar una instancia del modelo por su identificador. ```swift Planet.find(req.parameters.get("id"), on: database) ``` Este método devuelve `nil` si no se ha encontrado ningún modelo con ese identificador. ## Ciclo de vida Los middleware de modelo te permiten engancharte a los eventos del ciclo de vida de tu modelo. Están soportados los siguientes eventos de ciclo de vida (lifecycle): |Método|Descripción| |-|-| |`create`|Se ejecuta antes de crear un modelo.| |`update`|Se ejecuta antes de actualizar un modelo.| |`delete(force:)`|Se ejecuta antes de eliminar un modelo.| |`softDelete`|Se ejecuta antes de borrar de manera no permanente (soft-delete) un modelo.| |`restore`|Se ejecuta antes de restaurar un modelo (opuesto de soft delete).| Los middleware de modelo se declaran usando los protocolos `ModelMiddleware` o `AsyncModelMiddleware`. Todos los eventos de ciclo de vida tienen una implementación por defecto, así que solo necesitas implementar aquellos que necesites. Cada método acepta el modelo en cuestión, una referencia a la base de datos y la siguiente acción en la cadena. El middleware puede elegir devolver pronto, devolver un futuro fallido o llamar a la siguiente acción para continuar de manera normal. Si usas estos métodos, puedes llevar a cabo acciones tanto antes como después de que un evento en específico se complete. Puedes llevar a cabo acciones después de que el evento se complete si asignas el futuro devuelto por el siguiente respondedor. ```swift // Middleware de ejemplo que capitaliza nombres. struct PlanetMiddleware: ModelMiddleware { func create(model: Planet, on db: Database, next: AnyModelResponder) -> EventLoopFuture { // El modelo puede alterarse aquí antes de ser creado. model.name = model.name.capitalized() return next.create(model, on: db).map { // Una vez se haya creado el planeta // el código que haya aquí se ejecutará print ("Planet \(model.name) was created") } } } ``` o si usas `async`/`await`: ```swift struct PlanetMiddleware: AsyncModelMiddleware { func create(model: Planet, on db: Database, next: AnyAsyncModelResponder) async throws { // El modelo puede alterarse aquí antes de ser creado. model.name = model.name.capitalized() try await next.create(model, on: db) // Una vez se haya creado el planeta // el código que haya aquí se ejecutará print ("Planet \(model.name) was created") } } ``` Una vez hayas creado tu middleware, puedes activarlo usando `app.databases.middleware`. ```swift // Ejemplo de middleware de configuración de modelo. app.databases.middleware.use(PlanetMiddleware(), on: .psql) ``` ## Espacio de BBDD Fluent soporta establecer un espacio para un `Model`, lo que permite la partición de modelos individuales de Fluent entre esquemas de PostgreSQL, bases de datos MySQL, y varias bases de datos SQLite adjuntas. MongoDB no soporta los espacios en el momento de la redacción. Para poner un modelo en un espacio distinto al usado por defecto, añade una nueva propiedad estática al modelo: ```swift public static let schema = "planets" public static let space: String? = "mirror_universe" // ... ``` Fluent usará esto para la construcción de todas las consultas a la base de datos.