From 13c8287ad03ae711c9e371d2bc40345b866213e7 Mon Sep 17 00:00:00 2001 From: Jorge Acosta <104621044+jacostaf10@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:31:30 +0200 Subject: [PATCH] Add Spanish translation for model.md (#905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `model.md` Spanish translation on `fluent` folder --------- Co-authored-by: Ale Mohamad ⌘ --- docs/fluent/model.es.md | 596 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 596 insertions(+) create mode 100644 docs/fluent/model.es.md diff --git a/docs/fluent/model.es.md b/docs/fluent/model.es.md new file mode 100644 index 000000000..2543a49c6 --- /dev/null +++ b/docs/fluent/model.es.md @@ -0,0 +1,596 @@ +# 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, the value is `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. + +### Data Transfer Object + +La conformidad por defecto del modelo a `Codable` puede facilitar el prototipado y usos simples. Sin embargo, no se ajusta a todos los casos de uso. Para ciertas situaciones necesitarás usar un data transfer object (DTO). + +!!! tip "Consejo" + Un DTO es un tipo `Codable` aparte que representa la estructura de datos que quieres codificar/descodificar. + +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)" + ) + } +} +``` + +Aunque la estructura del DTO sea idéntica a la del modelo conformado con `Codable`, tenerlo como tipo aparte puede ayudar a mantener los proyectos grandes ordenados. Si necesitaras hacer un cambio en las propiedades de tu modelo, no tienes que preocuparte de si rompes la API pública de tu app. Puedes considerar agrupar tus DTO en un package aparte que puedas compartir con los consumidores de tu API. + +Por estas razones, recomendamos encarecidamente usar DTO siempre que sea posible, especialmente en proyectos grandes. + +## 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.