Reuse model fields
You have posts and notes. Both need timestamps, but each has its own content. A mixin lets you write the common fields once and keep the differences easy to see.
Start with two ordinary models
mixin Timestamps {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model Post {
use Timestamps
id BigInt @id
title String
}
model Note {
use Timestamps
id BigInt @id
body String
}Both models now have timestamps. You still create a Post with create_post() and a
Note with create_note(). There is no shared parent row to create or load.
Share ownership fields when you need them
The complete example at the end stops at shared timestamps and needs no permissions. Later, both resources may have an author. In the User/Post/Note application, put the repeated author fields into another mixin:
mixin OwnedFields {
id BigInt @id
author_id BigInt
author User @relation(fields: [author_id], references: [id]) @rent.creator
}
model Post {
use OwnedFields
use Timestamps
title String
}
model Note {
use OwnedFields
use Timestamps
body String
}These replace the earlier Post and Note definitions. User is your user model.
@rent.creator assigns the signed-in person as author. The mixins supply fields,
not a permission policy.
Reuse the access policy separately
If both resources should start private and allow read-only sharing, define that once:
permissions profile PrivateOwnedResource {
owner = author
reader = owner or [User]
read = reader
create, update, delete, manage_grants = owner
}
permissions Post {
use PrivateOwnedResource
}
permissions Note {
use PrivateOwnedResource
}The access block opts into permissions as shown in the
permissions concept. OwnedFields shares the storage
shape. PrivateOwnedResource shares the access rules. Each Post and Note keeps its
own author and its own sharing assignments.
You can use model mixins without permissions. See model mixins for the composition rules, or permission profiles to extend the policy.
Complete example
Download this project, or copy the files below into an empty directory.
Replace RENT_REPOSITORY_URL in Cargo.toml with the URL supplied with your preview access. Use the matching Rent CLI release.
Use a separate local database for this example:
export DATABASE_URL='sqlite://lesson.db?mode=rwc'
rent generate
rent migrate dev --name initial
cargo runRun the included test with cargo nextest run. It creates an isolated temporary database.
Cargo.toml
[package]
name = "rent-lesson-39-model-mixins"
version = "0.1.0"
edition = "2024"
[workspace]
[[bin]]
name = "rent-lesson-39-model-mixins"
path = "main.rs"
[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.7.2", default-features = false, features = ["sqlite", "migrate", "extensions"] }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
tempfile = "3.27.0"
# Used by the generated client after `rent generate`.
[package.metadata.cargo-machete]
ignored = ["rent"]rent.toml
schema = "schema.rsl"
generated = "generated"
migrations = "migrations"
[database]
dialect = "sqlite"
backends = ["sqlite"]schema.rsl
datasource db {
provider = "sqlite"
}
mixin Timestamps {
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model Post {
use Timestamps
id BigInt @id @default(autoincrement())
title String
}
model Note {
use Timestamps
id BigInt @id @default(autoincrement())
body String
}main.rs
use anyhow::{Result, ensure};
#[allow(dead_code, unused_imports)]
#[rustfmt::skip]
#[path = "generated/mod.rs"]
mod generated;
use generated::client::Client;
async fn example(url: &str) -> Result<()> {
let client = Client::connect(url).await?;
let post = client.create_post().title("Launch notes").save().await?;
let note = client.create_note().body("Review the launch date").save().await?;
let updated = client.update_post_one(&post).title("Reviewed notes").save_one().await?;
ensure!(updated.created_at == post.created_at);
ensure!(updated.updated_at >= post.updated_at);
ensure!(note.body == "Review the launch date");
println!("Two ordinary models share timestamps.");
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
example(&std::env::var("DATABASE_URL")?).await
}
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests {
#[tokio::test]
async fn example_runs() -> anyhow::Result<()> {
let (_directory, url) = super::test_support::database()?;
super::example(&url).await
}
}test_support.rs
use anyhow::{Context, Result, ensure};
use std::process::Command;
pub fn database() -> Result<(tempfile::TempDir, String)> {
let directory = tempfile::tempdir()?;
std::fs::write(directory.path().join("schema.rsl"), include_str!("schema.rsl"))?;
std::fs::write(directory.path().join("rent.toml"), r#"schema = "schema.rsl""#)?;
let url = format!("sqlite://{}?mode=rwc", directory.path().join("app.db").display());
let executable = std::env::var("RENT_CLI").unwrap_or_else(|_| "rent".into());
let output = Command::new(executable)
.arg("migrate")
.arg("dev")
.arg("--root")
.arg(directory.path())
.arg("--to")
.arg("schema.rsl")
.arg("--database-url")
.arg(&url)
.output()
.context("run the Rent CLI")?;
ensure!(
output.status.success(),
"migration failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok((directory, url))
}