Expand description
Model-inheritance patterns — Django’s Meta.abstract,
multi-table, and proxy shapes adapted to Rust idioms. See
inheritance for the canonical worked mappings. Issue #51.
Model-inheritance patterns — Django’s Meta.abstract, multi-table,
and proxy shapes adapted to Rust. Issue #51.
Django ships three flavors of model inheritance:
- Abstract base classes (
Meta.abstract = True) — share fields + methods across child models. The base has no DB table; every child gets its own table with the base’s fields physically copied in. - Multi-table inheritance — child gets its OWN table with an
implicit
OneToOneFK back to the parent. Queries against the child JOIN to the parent. - Proxy models (
Meta.proxy = True) — same table as the parent, but a different Manager / Meta / methods.
§Rust mappings
Rust doesn’t have class inheritance, but every Django use case for model inheritance has a clean Rust-idiomatic counterpart that the framework already supports today:
§1. Abstract base classes → traits + composition
Django’s abstract base is about sharing behavior (methods) and fields (schema). In Rust:
- Share behavior: define a trait with the shared methods,
implit on each model. - Share fields: not natively possible without a proc-macro.
The pragmatic shape is to either (a)
derivea helper proc-macro per shared block, or (b) accept the duplication on the field declarations and share the BEHAVIOR via trait.
use chrono::{DateTime, Utc};
/// Shared "timestamps" behavior. Implemented on every model that
/// wants the `created_at` / `updated_at` audit pair.
pub trait Timestamped {
fn created_at(&self) -> DateTime<Utc>;
fn updated_at(&self) -> DateTime<Utc>;
}
#[derive(rustango::Model)]
pub struct Article {
#[rustango(primary_key)]
pub id: rustango::core::Auto<i64>,
#[rustango(max_length = 200)]
pub title: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Timestamped for Article {
fn created_at(&self) -> DateTime<Utc> { self.created_at }
fn updated_at(&self) -> DateTime<Utc> { self.updated_at }
}The field declarations live on each model (Rust requirement),
but every shared helper / query method that takes <T: Timestamped>
works polymorphically — same dispatch shape Django gets from the
abstract base class.
§2. Multi-table inheritance → explicit OneToOne FK
Already supported. #[rustango(o2o = "ParentTable", on = "parent_id")]
declares an implicit-OneToOne FK to the parent table:
#[derive(rustango::Model)]
#[rustango(table = "place")]
pub struct Place {
#[rustango(primary_key)]
pub id: rustango::core::Auto<i64>,
#[rustango(max_length = 200)]
pub name: String,
#[rustango(max_length = 200)]
pub address: String,
}
#[derive(rustango::Model)]
#[rustango(table = "restaurant")]
pub struct Restaurant {
/// One-to-one back to Place — semantically "Restaurant IS-A Place".
#[rustango(primary_key)]
#[rustango(o2o = "place", on = "id")]
pub place_id: i64,
pub serves_hot_dogs: bool,
}Django’s Restaurant.objects.all() returns rows with both place
and restaurant fields joined; in rustango the equivalent is
Restaurant::objects().select_related("place_id") (or fetch
the parent rows separately via place_ids = restaurants.iter().map(|r| r.place_id)).
§3. Proxy models → extension trait
Django’s proxy is “different Manager / Meta on the same table.”
The Rust shape is an extension trait on QuerySet<T> that adds
domain-specific shortcuts — fully documented in the
crate::manager module. Same physical table, multiple
“personalities.”
pub trait PublishedArticleExt: Sized {
fn only_published(self) -> Self;
}
impl PublishedArticleExt for rustango::query::QuerySet<Article> {
fn only_published(self) -> Self {
use rustango::core::Column as _;
self.where_(Article::published.eq(true))
}
}
// Acts as a "PublishedArticle" proxy:
Article::objects().only_published().fetch(&pool).await?;§Summary table
| Django shape | Rust idiom | Framework support |
|---|---|---|
| Abstract base class | trait + per-model field declarations | trait dispatch (Rust built-in) |
| Multi-table inheritance | explicit #[rustango(o2o)] FK | shipped |
| Proxy model | extension trait on QuerySet<T> | see crate::manager |
§Why no #[rustango(abstract)] proc-macro attribute?
True field-inlining (parent fields physically copied into child
at macro time) would need the Model derive to consult a
parent struct’s field list. Doable but a substantial
proc-macro lift, and the trait-based “share behavior” approach
is strictly more flexible — multiple traits can layer onto
one struct, where a single abstract parent can’t.
File a follow-up if your project repeatedly hand-copies the same 5+ fields across N models — at that point a proc-macro pays for itself.