Rustango docs
← Guías

Modelos

Un modelo es una struct de Rust que se asigna a una tabla de base de datos. Añade #[derive(Model)], anota los campos, y Rustango genera el esquema, un punto de entrada de consultas con tipos seguros y los métodos save/find/delete — los modelos de Django o el Eloquent de Laravel, con el compilador verificando tus columnas. Esta es la referencia de declaración: cada tipo de campo, cada opción de clave primaria y cada atributo #[rustango(...)]. Para consultar los modelos una vez declarados, consulta el recetario del ORM.

Modelos en Rustango: una struct #[derive(Model)] asigna tipos de campo Rust a columnas por dialecto, la clave primaria puede ser un Auto<i64> autoincremental o una clave personalizada asignada por la aplicación, y el derive genera SCHEMA + objects() + save/find

¿Un término aquí es nuevo para ti? model, primary key, foreign key, migration, nullable — consulta el glosario.

Fuente: rustango::Model (#[derive(Model)]), rustango::core (el trait Model, ModelSchema, FieldType, Auto, ForeignKey), y las asignaciones de tipos por dialecto en rustango::sql::{dialect, mysql, sqlite} — siempre compilados (elige una característica de backend: postgres / mysql / sqlite).

Versión ejecutable: los round-trips de tipos de campo, la PK personalizada y los fragmentos de SCHEMA están copiados de models_doc.rs (cargo test -p rustango --features sqlite --test models_doc).

Tabla de contenidos


Anatomía de un modelo

use rustango::{Auto, Model};
use chrono::{DateTime, Utc};

#[derive(Model, Clone, Debug)]
#[rustango(table = "posts", display = "title")]   // model-level attributes
pub struct Post {
    #[rustango(primary_key)]
    pub id: Auto<i64>,                            // field-level attributes

    #[rustango(max_length = 200)]
    pub title: String,

    pub body: String,

    #[rustango(fk = "authors", on = "id")]
    pub author_id: i64,

    #[rustango(auto_now_add)]
    pub created_at: Auto<DateTime<Utc>>,
}

A partir de esa única declaración el derive genera:

  • el esquema (Post::SCHEMA — nombre de tabla, columnas, tipos, la PK) que leen las migraciones y el admin;
  • un punto de entrada de consultas, Post::objects(), que devuelve un QuerySet<Post>;
  • constantes de campo tipadas (Post::title, Post::author_id) para filtros verificados en compilación — Post::objects().where_(Post::author_id.eq(42));
  • métodos de fila — save, find, delete y más (consulta la API generada).

El nombre de la tabla toma por defecto el nombre del modelo si omites table; los nombres de columna toman por defecto el nombre del campo en snake_case a menos que establezcas column.


Tipos de campo

El tipo Rust del campo determina su tipo de columna en la base de datos. Rustango asigna cada tipo por dialecto, de modo que el mismo modelo funciona en PostgreSQL, MySQL y SQLite:

Tipo RustPostgreSQLMySQLSQLite
i16SMALLINTSMALLINTINTEGER
i32INTEGERINTINTEGER
i64BIGINTBIGINTINTEGER
f32REALFLOATREAL
f64DOUBLE PRECISIONDOUBLEREAL
boolBOOLEANTINYINT(1)INTEGER (0/1)
StringTEXTTEXTTEXT
String + max_length = NVARCHAR(N)VARCHAR(N)TEXT
chrono::DateTime<Utc>TIMESTAMPTZDATETIME(6)TEXT (ISO-8601)
chrono::NaiveDateDATEDATETEXT
chrono::NaiveTimeTIMETIME(6)TEXT
uuid::UuidUUIDCHAR(36)TEXT
serde_json::ValueJSONBJSONTEXT
rust_decimal::DecimalNUMERICDECIMAL(38,10)NUMERIC
Vec<u8>BYTEALONGBLOBBLOB
Option<T>T NULLT NULLT (nullable)

Option<T> es la forma de hacer que una columna sea nullable — un campo que no es Option es NOT NULL. Todos estos hacen un round-trip a través de savefind, verificado de extremo a extremo:

#[derive(Model, Debug, Clone)]
#[rustango(table = "gadget")]
pub struct Gadget {
    #[rustango(primary_key)]
    pub id: Auto<i64>,
    #[rustango(max_length = 100)]
    pub name: String,
    pub qty: i64,
    pub active: bool,
    pub note: Option<String>,        // nullable
    pub made_at: DateTime<Utc>,
    pub meta: serde_json::Value,     // JSON
}

Precisión decimal. El NUMERIC de PostgreSQL es de precisión arbitraria; MySQL usa DECIMAL(38,10) (38 dígitos, 10 fraccionarios — el ajuste portable más amplio); SQLite usa afinidad NUMERIC. Usa rust_decimal::Decimal para dinero, nunca f64.

Tipos exclusivos de PostgreSQL

Estos se asignan a tipos de columna nativos de PostgreSQL y no tienen equivalente en MySQL/SQLite — el escritor de migraciones emite TEXT allí para mantener la validez, pero leerlos/escribirlos produce un error en tiempo de ejecución en esos backends. Úsalos solo en despliegues de PostgreSQL:

Tipo RustPostgreSQLNotas
Array<T>text[] / integer[] / bigint[]arrays nativos
Range<T>int4range / int8range / numrange / daterange / tstzrangetipos de rango
HStorehstoremapa plano string→string (necesita la extensión)
Vector + #[rustango(vector(dims = N))]vector(N)embeddings de pgvector
Point + #[rustango(geometry(srid = N))]geometry(Point, N)PostGIS

Claves primarias

Cada modelo necesita una clave primaria. Marca un campo con #[rustango(primary_key)]; si no marcas ninguno, el esquema busca una columna llamada id.

La PK por defecto y más común es un entero de 64 bits autoincremental, declarado como Auto<i64>:

#[rustango(primary_key)]
pub id: Auto<i64>,

Semántica de Auto<T>. Un campo Auto<T> es o bien Unset (el valor que asignará la BD) o Set(v). Al insertar, una PK Unset se omite de la lista de columnas para que la base de datos la genere, luego el valor se vuelve a leer (RETURNING en PostgreSQL/SQLite, LAST_INSERT_ID() en MySQL) y se almacena en tu struct:

let mut g = Gadget { id: Auto::default(), /* … */ };   // Unset
g.save_pool(&pool).await?;                              // DB assigns the id
let new_id = g.id.get().copied().unwrap();             // now populated

Los tipos internos de Auto<T> admitidos son i32, i64 y Uuid.

Claves primarias personalizadas

La PK no tiene por qué ser un entero autoincremental. Cualquier tipo que se asigne a una columna puede ser la PK; tú mismo asignas el valor:

Declaración de PKTipo de columnaQuién la asigna
Auto<i64> (por defecto)BIGSERIAL / BIGINT AUTO_INCREMENT / INTEGER … AUTOINCREMENTbase de datos
Auto<i32>SERIAL / INT AUTO_INCREMENT / INTEGER …base de datos
Auto<Uuid> + auto_uuidUUIDlado Rust (uuid v4)
Auto<Uuid> + default_uuid_v7UUIDlado Rust (uuid v7 ordenable)
String + primary_keyVARCHAR(N) / TEXTtú (aplicación)
Uuid + primary_keyUUID / CHAR(36) / TEXTtú (aplicación)
i64 / i32 + primary_keyBIGINT / INTEGERtú (aplicación)

Una clave natural de tipo string (por ejemplo, un código de cupón) — ten en cuenta que proporcionas el valor e insertas con insert_pool, ya que no hay un Auto::Unset que save pueda detectar:

#[derive(Model, Debug, Clone)]
#[rustango(table = "coupon")]
pub struct Coupon {
    #[rustango(primary_key, max_length = 32)]
    pub code: String,        // you assign this
    pub discount: i64,
}

let c = Coupon { code: "SAVE10".into(), discount: 10 };
c.insert_pool(&pool).await?;                       // explicit INSERT
let back = Coupon::find_or_fail("SAVE10".to_string(), &pool).await?;   // look up by the string PK

Renombra la columna de la PK con column (el campo Rust sigue siendo number, la columna SQL es account_no):

#[rustango(primary_key, column = "account_no")]
pub number: i64,
// Introspect it via the schema:
let pk = Account::SCHEMA.primary_key().unwrap();
assert_eq!(pk.name, "number");        // Rust field
assert_eq!(pk.column, "account_no");  // SQL column

Las claves primarias UUID generan el valor en el lado Rust: auto_uuid da un v4 aleatorio, default_uuid_v7 un v7 ordenable por tiempo (mejor para la localidad de índice):

#[rustango(primary_key, auto_uuid)]
pub id: Auto<uuid::Uuid>,

Claves primarias compuestas

Las claves primarias nativas de varias columnas no están soportadas — exactamente un campo puede ser primary_key. El patrón provisto es una PK subrogada Auto<i64> más una restricción de unicidad compuesta declarada, que es equivalente en términos de índice:

#[derive(Model)]
#[rustango(table = "line_item", unique_together = "invoice_id, line_no")]
pub struct LineItem {
    #[rustango(primary_key)]
    pub id: Auto<i64>,        // surrogate PK
    pub invoice_id: i64,
    pub line_no: i32,         // (invoice_id, line_no) is unique together
}

Busca las filas con .where_(LineItem::invoice_id.eq(..)).where_(LineItem::line_no.eq(..)).


Relaciones

Una clave foránea es una columna _id más un accesor tipado opcional:

// Plain FK column — store the parent's id:
#[rustango(fk = "authors", on = "id")]
pub author_id: i64,

// Typed FK — lazy-loads the parent on demand:
pub author: ForeignKey<Author>,

ForeignKey<T> toma por defecto i64 como tipo de clave; si la PK del padre es de un tipo distinto, nómbralo: ForeignKey<User, String>. Uno-a-uno usa #[rustango(o2o)]; muchos-a-muchos es una tabla separada — consulta Recetario del ORM → Muchos-a-muchos. Carga de forma anticipada las filas relacionadas con select_related (también en la guía del ORM).


Atributos de campo comunes

#[rustango(...)] sobre un campo. Los que usarás constantemente:

AtributoEjemploEfecto
primary_key#[rustango(primary_key)]marca la PK
max_length = N#[rustango(max_length = 200)]VARCHAR(N) + comprobación de longitud en escritura
default = "…"#[rustango(default = "'draft'")]valor por defecto de columna en la BD (literal SQL)
unique#[rustango(unique)]restricción de unicidad sobre la columna
choices = "…"#[rustango(choices = "draft:Draft, published:Published")]valores enumerados (value:Label); <select> del admin + validación
auto_now_add#[rustango(auto_now_add)]establecer a ahora en la inserción (en un Auto<DateTime<Utc>>)
auto_now#[rustango(auto_now)]establecer a ahora en cada guardado
column = "…"#[rustango(column = "account_no")]renombrar la columna SQL
null / Option<T>pub note: Option<String>columna nullable
min / max#[rustango(min = 0, max = 100)]validación de rango en escritura
blank / editable#[rustango(editable = false)]comportamiento en formulario/admin
db_comment = "…"#[rustango(db_comment = "cents")]COMMENT de columna

choices, default, auto_now_add y borrado lógico juntos (todos verificados):

#[rustango(max_length = 20, default = "'draft'", choices = "draft:Draft, published:Published")]
pub status: String,
#[rustango(auto_now_add)]
pub created_at: Auto<DateTime<Utc>>,
#[rustango(soft_delete)]
pub deleted_at: Option<DateTime<Utc>>,

Índices y restricciones

Declarados sobre el modelo:

#[rustango(
    table = "posts",
    index("status, published_at"),                 // composite btree index
    unique_together = "author_id, slug",           // multi-column unique
    check(name = "qty_nonneg", expr = "qty >= 0"), // CHECK constraint
)]
  • index(...) — un índice btree por defecto; elige un método para PostgreSQL con index(columns = "body", method = "gin") (también gist, brin, hash, bloom, spgist).
  • unique_together / index_together — de varias columnas único / no único.
  • Índices parcialesunique_when(...) / index_when(...) añaden una condición WHERE.
  • check(name, expr) — una restricción CHECK; exclude(...) es una restricción EXCLUDE de PostgreSQL.

Atributos de modelo comunes

#[rustango(...)] sobre la struct:

AtributoEjemploEfecto
table = "…"table = "posts"nombre de tabla (por defecto el nombre de la struct)
display = "…"display = "title"el campo mostrado cuando se referencia una fila (etiquetas FK, admin)
app = "…"app = "blog"agrupa el modelo bajo una app
default_order = "…"default_order = "-created_at"orden por defecto para las consultas
default_permissionsdefault_permissions = "add, change"qué autopermisos crear
soft_delete (campo)#[rustango(soft_delete)] deleted_at: Option<…>habilitar borrado lógico (marcar, no eliminar)
audit(track = "…")audit(track = "title, status")registrar el historial de cambios por fila
scope = "…"scope = "tenant"ámbito de multi-tenancy (registro vs inquilino)
admin(...)admin(list_display = "…")configuración de la UI del admin — consulta el admin

La API generada

#[derive(Model)] implementa el trait Model (Post::SCHEMA) y genera:

  • Post::objects() (alias Post::query()) → un QuerySet<Post> para filtrar, ordenar y obtener (el recetario del ORM cubre la API de consultas).
  • Constantes de campo tipadasPost::title, Post::author_id — usadas en .where_(Post::author_id.eq(42)) para filtros verificados en compilación.
  • Buscadoresfind(pk, &pool)Option<Self>; find_or_fail(pk, &pool)Self (error si no existe); find_many(pks, &pool); find_or_insert(...).
  • Escritoressave/save_pool, save_partial(&["title"], &pool) (actualiza solo algunas columnas), insert_pool (inserción explícita), delete.
  • Borrado lógico (cuando está habilitado) — soft_delete, restore, force_delete; QuerySet::active() / with_trashed() / only_trashed().

save vs insert

Esto confunde a la gente, así que vale la pena decirlo con claridad:

MétodoComportamiento
save_pool(&mut self, &pool)INSERT si la PK Auto está Unset, de lo contrario UPDATE
insert_pool(&self, &pool)siempre INSERT

Para la PK Auto<i64> por defecto, save_pool hace lo correcto automáticamente. Para una PK asignada por la aplicación (un String/Uuid que estableces tú mismo), no hay un estado Unset — así que save_pool haría un UPDATE de una fila (posiblemente inexistente). Usa insert_pool para insertar una fila totalmente nueva con una PK personalizada (verificado en el test de respaldo).


Referencia completa de atributos

Cada clave #[rustango(...)] que acepta el derive. Las comunes se cubren arriba; esta es la lista completa, incluyendo las avanzadas/específicas de PostgreSQL.

A nivel de modelo (sobre la struct)

AtributoValorEfecto
table"name"nombre de tabla
display"field"etiqueta legible para una fila
app"name"agrupación por app
default_order"-field"orden por defecto
default_permissions"add, change, delete, view"autopermisos a crear
default_related_name"posts"nombre del accesor inverso en el padre
base_manager_name"all_objects"nombre del manager base (sin filtrar)
manager(ext = "Trait")ruta del traitgenerar un trait de extensión de manager personalizado
manager_fn"published"añadir un accesor de manager más allá de objects()
get_latest_by"created_at"columna por defecto para latest()/earliest()
order_with_respect_to"parent"ordenación relativa al padre de Django
index(...)columns, method, nameíndice secundario (btree/gin/gist/brin/hash/bloom/spgist)
unique_together"a, b"restricción de unicidad compuesta
index_together"a, b"índice no único compuesto
unique_when(...) / index_when(...)columnas + conditioníndice parcial (condicional)
check(...)name, exprrestricción CHECK
exclude(...)especificación de operadorrestricción EXCLUDE de PostgreSQL
audit(track = "…")lista de camposhistorial de cambios por fila
scope"tenant" / "registry"ámbito de multi-tenancy
proxyflagmodelo proxy (comparte la tabla de otro)
global_scope(name, apply = fn)nombre + fnfiltro aplicado automáticamente a todas las consultas
through(...)especificación de relaciónaccesor de relación through personalizado
reverse_has(...) / generic_has(...)especificación de relaciónaccesor inverso has-many / inverso de FK genérica
required_db_features / required_db_vendorlista / proveedorrestricciones de validación de despliegue
db_table_comment"…"COMMENT de tabla
admin(...)opciones de adminconfiguración de la UI del admin (consulta admin.md)

A nivel de campo (sobre un campo)

AtributoValorEfecto
primary_keyflagmarca la PK
column"name"renombrar la columna SQL
max_lengthNVARCHAR(N) + validación de longitud
default"sql literal"DEFAULT de columna
nullflagnullable (o usa Option<T>)
uniqueflagrestricción de unicidad
choices"v:Label, …"valores enumerados
min / maxnúmerovalidación de rango
blankflagpermitir vacío en formularios/admin
editabletrue/falseeditabilidad en formulario/admin
auto_nowflagestablecer a ahora en cada guardado
auto_now_addflagestablecer a ahora en la inserción
auto_uuidflagUUID v4 del lado Rust (en Auto<Uuid>)
default_uuid_v7flagUUID v7 ordenable del lado Rust
fk + on"table", "col"columna de clave foránea
cascadeflagON DELETE CASCADE
o2oflagrelación uno-a-uno
fk_composite(...) / generic_fk(...)especificaciónFK compuesta / FK genérica (content-type)
generated_as"expr"columna computada (generada) por la BD
citextflagtexto insensible a mayúsculas (CITEXT de PostgreSQL)
vector(dims = N)Ndimensión de pgvector
geometry(srid = N)Nid de referencia espacial de PostGIS
db_comment"…"COMMENT de columna

Véase también

  • Recetario del ORM — consultas, filtros, agregaciones, joins, transacciones (qué hacer con un modelo una vez declarado).
  • Serializadores — dar forma a un modelo en JSON para una API.
  • El admin — el bloque admin(...) y la UI generada.
  • Scaffolding · CLI manage — generar un modelo y su migración.