Tutorial 03: Active Record CRUD Operations 🗄️
rullst-orm derives a typed query builder and persistence methods from a Rust
struct. The SQLx-backed model uses an i32 primary key named id; a zero value
means that save() inserts, while a non-zero value means that it updates.
Step 1: Define an Active Record model
In src/models/user.rs:
#![allow(unused)]
fn main() {
use rullst_orm::{FromRow, Orm};
#[derive(Debug, Clone, FromRow, Orm)]
#[orm(table = "users")]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
}
}
The database table must contain matching columns. Run migrations before using the model.
Step 2: Perform CRUD operations
Create
#![allow(unused)]
fn main() {
use rullst_orm::{FromRow, Orm};
#[derive(Debug, Clone, FromRow, Orm)]
#[orm(table = "users")]
struct User { id: i32, name: String, email: String }
async fn create_user() -> Result<(), rullst_orm::Error> {
let mut user = User {
id: 0,
name: "Alice Developer".to_string(),
email: "alice@example.com".to_string(),
};
user.save().await?;
// `user.id` now contains the inserted primary key.
Ok(())
}
}
Read and filter
#![allow(unused)]
fn main() {
use rullst_orm::{FromRow, Orm};
#[derive(Debug, Clone, FromRow, Orm)]
#[orm(table = "users")]
struct User { id: i32, name: String, email: String }
async fn read_users() -> Result<(), rullst_orm::Error> {
let user = User::find(1).await?; // Result<Option<User>, rullst_orm::Error>
let example_users = User::query()
.where_like("email", "%@example.com")
.get()
.await?;
let _ = (user, example_users);
Ok(())
}
}
Builder values are bound as query parameters. Column names are validated as identifiers, but they should still be application-owned constants rather than untrusted request input.
Update
#![allow(unused)]
fn main() {
use rullst_orm::{FromRow, Orm};
#[derive(Debug, Clone, FromRow, Orm)]
#[orm(table = "users")]
struct User { id: i32, name: String, email: String }
async fn update_user() -> Result<(), rullst_orm::Error> {
if let Some(mut user) = User::find(1).await? {
user.name = "Alice Smith".to_string();
user.save().await?;
}
Ok(())
}
}
Delete
#![allow(unused)]
fn main() {
use rullst_orm::{FromRow, Orm};
#[derive(Debug, Clone, FromRow, Orm)]
#[orm(table = "users")]
struct User { id: i32, name: String, email: String }
async fn delete_user() -> Result<(), rullst_orm::Error> {
if let Some(user) = User::find(1).await? {
user.delete().await?;
}
Ok(())
}
}
Soft deletes
A deleted_at: Option<String> field opts the model into the default soft-delete
contract. For a different sentinel, configure it explicitly and ensure the
migration uses the same representation:
#![allow(unused)]
fn main() {
use rullst_orm::{FromRow, Orm};
#[derive(Debug, Clone, FromRow, Orm)]
#[orm(
table = "users",
soft_delete(field = "is_deleted", value = "0", delval = "1")
)]
pub struct SoftUser {
pub id: i32,
pub name: String,
pub is_deleted: i32,
}
}
query() applies the model’s configured scopes. unscoped() is an explicit
administrative escape hatch and should not be used directly from request data.
Key takeaways
save,find,all,query, anddeleteare generated for SQLx-backedOrmmodels.- Creation uses a normal Rust struct, not a JSON map.
- CRUD errors are returned as
rullst_orm::Error; a missing row isOk(None). - Use a caller-owned transaction and the generated
*_with_txmethods when a business operation must commit multiple writes atomically.