🦀 Model Relationships in TuskLang for Rust
Model Relationships in TuskLang for Rust
TuskLang's Rust relationship system provides type-safe, async-first model relationships with compile-time guarantees, zero-cost abstractions, and comprehensive relationship management capabilities.
🚀 Why Rust Relationships?
Rust's type system and ownership model make it the perfect language for model relationships:
- Type Safety: Compile-time validation of relationship types - Ownership Safety: Automatic memory management for related objects - Async/Await: Non-blocking relationship loading - Zero-Cost Abstractions: No performance penalty for safety - Relationship Integrity: Guaranteed referential integrity
Basic Relationship Types
use tusk_db::{Model, Relationship, Result};
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
use chrono::{DateTime, Utc};// Define related models
#[derive(Debug, Serialize, Deserialize, Clone)]
struct User {
pub id: Option<i32>,
pub name: String,
pub email: String,
pub is_active: bool,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Post {
pub id: Option<i32>,
pub title: String,
pub content: String,
pub user_id: i32,
pub published: bool,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Comment {
pub id: Option<i32>,
pub content: String,
pub user_id: i32,
pub post_id: i32,
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct UserProfile {
pub id: Option<i32>,
pub user_id: i32,
pub bio: Option<String>,
pub avatar_url: Option<String>,
pub website: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Role {
pub id: Option<i32>,
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct UserRole {
pub id: Option<i32>,
pub user_id: i32,
pub role_id: i32,
pub created_at: Option<DateTime<Utc>>,
}
One-to-Many Relationships
use tusk_db::{HasMany, BelongsTo};// User has many Posts
#[async_trait]
impl User {
// One-to-Many: User has many Posts
async fn posts(&self) -> Result<Vec<Post>> {
@has_many::<Post>(self.id.unwrap(), "user_id").await
}
// One-to-Many with conditions
async fn published_posts(&self) -> Result<Vec<Post>> {
@has_many::<Post>(self.id.unwrap(), "user_id")
.where_eq("published", true)
.order_by("created_at", "DESC")
.await
}
// One-to-Many with eager loading
async fn posts_with_comments(&self) -> Result<Vec<Post>> {
@has_many::<Post>(self.id.unwrap(), "user_id")
.with(&["comments"])
.where_eq("published", true)
.await
}
// Count related records
async fn posts_count(&self) -> Result<i64> {
@has_many::<Post>(self.id.unwrap(), "user_id")
.count()
.await
}
// Check if has related records
async fn has_posts(&self) -> Result<bool> {
@has_many::<Post>(self.id.unwrap(), "user_id")
.exists()
.await
}
}
// Post belongs to User
#[async_trait]
impl Post {
// Belongs To: Post belongs to User
async fn user(&self) -> Result<User> {
@belongs_to::<User>(self.user_id, "id").await
}
// Belongs To with eager loading
async fn user_with_profile(&self) -> Result<User> {
@belongs_to::<User>(self.user_id, "id")
.with(&["profile"])
.await
}
// Belongs To with conditions
async fn active_user(&self) -> Result<Option<User>> {
@belongs_to::<User>(self.user_id, "id")
.where_eq("is_active", true)
.first()
.await
}
}
One-to-One Relationships
use tusk_db::HasOne;// User has one Profile
#[async_trait]
impl User {
// One-to-One: User has one Profile
async fn profile(&self) -> Result<Option<UserProfile>> {
@has_one::<UserProfile>(self.id.unwrap(), "user_id").await
}
// One-to-One with conditions
async fn active_profile(&self) -> Result<Option<UserProfile>> {
@has_one::<UserProfile>(self.id.unwrap(), "user_id")
.where_not_null("bio")
.await
}
// Create or update one-to-one relationship
async fn create_or_update_profile(&self, profile_data: UserProfile) -> Result<UserProfile> {
if let Some(existing_profile) = self.profile().await? {
// Update existing profile
@UserProfile::update(existing_profile.id.unwrap(), profile_data).await
} else {
// Create new profile
let mut new_profile = profile_data;
new_profile.user_id = self.id.unwrap();
@UserProfile::create(new_profile).await
}
}
}
// UserProfile belongs to User
#[async_trait]
impl UserProfile {
async fn user(&self) -> Result<User> {
@belongs_to::<User>(self.user_id, "id").await
}
}
Many-to-Many Relationships
use tusk_db::BelongsToMany;// User belongs to many Roles
#[async_trait]
impl User {
// Many-to-Many: User belongs to many Roles
async fn roles(&self) -> Result<Vec<Role>> {
@belongs_to_many::<Role>(
self.id.unwrap(),
"user_roles",
"user_id",
"role_id"
).await
}
// Many-to-Many with conditions
async fn active_roles(&self) -> Result<Vec<Role>> {
@belongs_to_many::<Role>(
self.id.unwrap(),
"user_roles",
"user_id",
"role_id"
)
.where_eq("roles.is_active", true)
.await
}
// Attach role to user
async fn attach_role(&self, role_id: i32) -> Result<()> {
@belongs_to_many::<Role>(
self.id.unwrap(),
"user_roles",
"user_id",
"role_id"
)
.attach(role_id)
.await
}
// Detach role from user
async fn detach_role(&self, role_id: i32) -> Result<()> {
@belongs_to_many::<Role>(
self.id.unwrap(),
"user_roles",
"user_id",
"role_id"
)
.detach(role_id)
.await
}
// Sync roles (replace all roles)
async fn sync_roles(&self, role_ids: Vec<i32>) -> Result<()> {
@belongs_to_many::<Role>(
self.id.unwrap(),
"user_roles",
"user_id",
"role_id"
)
.sync(role_ids)
.await
}
// Check if user has specific role
async fn has_role(&self, role_name: &str) -> Result<bool> {
@belongs_to_many::<Role>(
self.id.unwrap(),
"user_roles",
"user_id",
"role_id"
)
.where_eq("roles.name", role_name)
.exists()
.await
}
}
// Role belongs to many Users
#[async_trait]
impl Role {
async fn users(&self) -> Result<Vec<User>> {
@belongs_to_many::<User>(
self.id.unwrap(),
"user_roles",
"role_id",
"user_id"
).await
}
// Count users with this role
async fn users_count(&self) -> Result<i64> {
@belongs_to_many::<User>(
self.id.unwrap(),
"user_roles",
"role_id",
"user_id"
)
.count()
.await
}
}
Has Many Through Relationships
use tusk_db::HasManyThrough;// User has many Comments through Posts
#[async_trait]
impl User {
// Has Many Through: User has many Comments through Posts
async fn comments(&self) -> Result<Vec<Comment>> {
@has_many_through::<Comment, Post>(
self.id.unwrap(),
"user_id",
"post_id"
).await
}
// Has Many Through with conditions
async fn recent_comments(&self) -> Result<Vec<Comment>> {
@has_many_through::<Comment, Post>(
self.id.unwrap(),
"user_id",
"post_id"
)
.where_gt("comments.created_at", Utc::now() - chrono::Duration::days(7))
.order_by("comments.created_at", "DESC")
.await
}
// Has Many Through with eager loading
async fn comments_with_posts(&self) -> Result<Vec<Comment>> {
@has_many_through::<Comment, Post>(
self.id.unwrap(),
"user_id",
"post_id"
)
.with(&["post"])
.await
}
}
// Post has many Users through Comments
#[async_trait]
impl Post {
async fn commenters(&self) -> Result<Vec<User>> {
@has_many_through::<User, Comment>(
self.id.unwrap(),
"post_id",
"user_id"
).await
}
// Distinct commenters
async fn distinct_commenters(&self) -> Result<Vec<User>> {
@has_many_through::<User, Comment>(
self.id.unwrap(),
"post_id",
"user_id"
)
.distinct()
.await
}
}
Polymorphic Relationships
use tusk_db::MorphMany;// Polymorphic relationship example
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Activity {
pub id: Option<i32>,
pub subject_type: String,
pub subject_id: i32,
pub action: String,
pub description: String,
pub created_at: Option<DateTime<Utc>>,
}
// User can have many activities
#[async_trait]
impl User {
// Polymorphic: User has many Activities
async fn activities(&self) -> Result<Vec<Activity>> {
@morph_many::<Activity>(
self.id.unwrap(),
"subject_id",
"subject_type",
"User"
).await
}
// Recent activities
async fn recent_activities(&self) -> Result<Vec<Activity>> {
@morph_many::<Activity>(
self.id.unwrap(),
"subject_id",
"subject_type",
"User"
)
.where_gt("created_at", Utc::now() - chrono::Duration::days(30))
.order_by("created_at", "DESC")
.limit(10)
.await
}
}
// Post can also have many activities
#[async_trait]
impl Post {
async fn activities(&self) -> Result<Vec<Activity>> {
@morph_many::<Activity>(
self.id.unwrap(),
"subject_id",
"subject_type",
"Post"
).await
}
}
Eager Loading and Performance
use tusk_db::{EagerLoading, WithClause};// Eager loading relationships
async fn get_users_with_relationships() -> Result<Vec<User>> {
let users = @User::query()
.with(&["posts", "posts.comments", "profile", "roles"])
.where_eq("is_active", true)
.get()
.await?;
Ok(users)
}
// Selective eager loading
async fn get_users_with_selective_relationships() -> Result<Vec<User>> {
let users = @User::query()
.with(&[
("posts", |query| {
query.where_eq("published", true)
.order_by("created_at", "DESC")
.limit(5)
}),
("profile", |query| {
query.select(&["id", "user_id", "bio"])
}),
("roles", |query| {
query.where_eq("is_active", true)
})
])
.get()
.await?;
Ok(users)
}
// Nested eager loading
async fn get_users_with_nested_relationships() -> Result<Vec<User>> {
let users = @User::query()
.with(&[
"posts",
"posts.comments",
"posts.comments.user",
"profile",
"roles"
])
.get()
.await?;
Ok(users)
}
// Lazy loading with caching
async fn get_user_with_cached_relationships(user_id: i32) -> Result<User> {
let cache_key = format!("user:{}:with_relationships", user_id);
let user = @cache::remember(&cache_key, 3600, || async {
@User::find(user_id)
.with(&["posts", "profile", "roles"])
.await
}).await?;
Ok(user)
}
Relationship Constraints and Validation
use tusk_db::{RelationshipConstraint, ValidationError};// Relationship constraints
#[async_trait]
impl User {
// Constrained relationship
async fn active_posts(&self) -> Result<Vec<Post>> {
@has_many::<Post>(self.id.unwrap(), "user_id")
.where_eq("published", true)
.where_eq("is_active", true)
.order_by("created_at", "DESC")
.await
}
// Relationship with validation
async fn create_post(&self, post_data: Post) -> Result<Post> {
// Validate user can create posts
if !self.can_create_posts().await? {
return Err(ValidationError::new("user", "User cannot create posts").into());
}
let mut post = post_data;
post.user_id = self.id.unwrap();
@Post::create(post).await
}
// Check if user can create posts
async fn can_create_posts(&self) -> Result<bool> {
// Business logic validation
let post_count = self.posts_count().await?;
let is_premium = self.has_role("premium").await?;
Ok(is_premium || post_count < 10)
}
}
// Relationship integrity checks
async fn validate_user_relationships(user_id: i32) -> Result<()> {
let user = @User::find(user_id).await?;
// Check if user has required relationships
let profile = user.profile().await?;
if profile.is_none() {
return Err(ValidationError::new("profile", "User must have a profile").into());
}
// Check if user has at least one role
let roles = user.roles().await?;
if roles.is_empty() {
return Err(ValidationError::new("roles", "User must have at least one role").into());
}
Ok(())
}
Relationship Events and Hooks
use tusk_db::{RelationshipEvent, EventHook};// Relationship events
#[async_trait]
impl User {
// Event when user is created
async fn on_created(&self) -> Result<()> {
// Create default profile
let profile = UserProfile {
id: None,
user_id: self.id.unwrap(),
bio: None,
avatar_url: None,
website: None,
};
@UserProfile::create(profile).await?;
// Assign default role
let default_role = @Role::where_eq("name", "user").first().await?;
if let Some(role) = default_role {
self.attach_role(role.id.unwrap()).await?;
}
Ok(())
}
// Event when user is deleted
async fn on_deleted(&self) -> Result<()> {
// Clean up related data
@Post::where_eq("user_id", self.id.unwrap()).delete().await?;
@UserProfile::where_eq("user_id", self.id.unwrap()).delete().await?;
// Detach all roles
let roles = self.roles().await?;
for role in roles {
self.detach_role(role.id.unwrap()).await?;
}
Ok(())
}
}
// Relationship event listeners
async fn register_relationship_events() -> Result<()> {
@User::listen(RelationshipEvent::Created, |user| async {
// Send welcome email
@send_welcome_email(&user.email).await?;
// Create default profile
let profile = UserProfile {
id: None,
user_id: user.id.unwrap(),
bio: None,
avatar_url: None,
website: None,
};
@UserProfile::create(profile).await?;
Ok(())
}).await;
@User::listen(RelationshipEvent::Deleted, |user| async {
// Archive user data
@archive_user_data(user.id.unwrap()).await?;
// Notify administrators
@notify_admin_user_deleted(user.id.unwrap()).await?;
Ok(())
}).await;
Ok(())
}
Relationship Serialization and API Responses
use serde::{Serialize, Deserialize};
use tusk_db::{RelationshipSerializer, ApiResource};// API resources with relationships
#[derive(Debug, Serialize)]
struct UserResource {
id: i32,
name: String,
email: String,
is_active: bool,
created_at: DateTime<Utc>,
posts_count: Option<i64>,
roles: Vec<RoleResource>,
profile: Option<UserProfileResource>,
}
#[derive(Debug, Serialize)]
struct UserDetailResource {
id: i32,
name: String,
email: String,
is_active: bool,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
profile: Option<UserProfileResource>,
posts: Vec<PostResource>,
roles: Vec<RoleResource>,
comments_count: Option<i64>,
}
impl User {
// Convert to API resource with relationships
async fn to_resource(&self) -> Result<UserResource> {
let roles = self.roles().await?;
let profile = self.profile().await?;
let posts_count = self.posts_count().await?;
Ok(UserResource {
id: self.id.unwrap(),
name: self.name.clone(),
email: self.email.clone(),
is_active: self.is_active,
created_at: self.created_at.unwrap(),
posts_count: Some(posts_count),
roles: roles.into_iter().map(|r| r.to_resource()).collect(),
profile: profile.map(|p| p.to_resource()),
})
}
// Convert to detailed resource
async fn to_detail_resource(&self) -> Result<UserDetailResource> {
let profile = self.profile().await?;
let posts = self.posts().await?;
let roles = self.roles().await?;
let comments_count = self.comments().await?.len() as i64;
Ok(UserDetailResource {
id: self.id.unwrap(),
name: self.name.clone(),
email: self.email.clone(),
is_active: self.is_active,
created_at: self.created_at.unwrap(),
updated_at: self.updated_at.unwrap(),
profile: profile.map(|p| p.to_resource()),
posts: posts.into_iter().map(|p| p.to_resource()).collect(),
roles: roles.into_iter().map(|r| r.to_resource()).collect(),
comments_count: Some(comments_count),
})
}
}
// Collection resource with relationships
async fn get_users_with_relationships_resource() -> Result<ApiResource<UserResource>> {
let users = @User::query()
.with(&["roles", "profile"])
.where_eq("is_active", true)
.get()
.await?;
let resources: Vec<UserResource> = futures::future::join_all(
users.into_iter().map(|u| u.to_resource())
).await
.into_iter()
.collect::<Result<Vec<_>>>()?;
Ok(ApiResource {
data: resources,
meta: None,
links: None,
})
}
Testing Relationships
use tusk_db::test_utils::{TestDatabase, TestRelationship};// Test relationships with test database
#[tokio::test]
async fn test_user_relationships() -> Result<()> {
let test_db = @TestDatabase::new().await?;
let mut tx = test_db.begin_transaction().await?;
// Create test user
let user = @User::create(User {
id: None,
name: "Test User".to_string(),
email: "test@example.com".to_string(),
is_active: true,
created_at: None,
updated_at: None,
}).await?;
// Test one-to-many relationship
let post = @Post::create(Post {
id: None,
title: "Test Post".to_string(),
content: "Test content".to_string(),
user_id: user.id.unwrap(),
published: true,
created_at: None,
updated_at: None,
}).await?;
let user_posts = user.posts().await?;
assert_eq!(user_posts.len(), 1);
assert_eq!(user_posts[0].id, post.id);
// Test belongs-to relationship
let post_user = post.user().await?;
assert_eq!(post_user.id, user.id);
// Test many-to-many relationship
let role = @Role::create(Role {
id: None,
name: "admin".to_string(),
description: Some("Administrator".to_string()),
}).await?;
user.attach_role(role.id.unwrap()).await?;
let user_roles = user.roles().await?;
assert_eq!(user_roles.len(), 1);
assert_eq!(user_roles[0].id, role.id);
// Test relationship constraints
let has_admin_role = user.has_role("admin").await?;
assert!(has_admin_role);
tx.rollback().await?;
Ok(())
}
Best Practices for Rust Relationships
1. Use Strong Types: Leverage Rust's type system for relationship safety 2. Async/Await: Use non-blocking operations for relationship loading 3. Eager Loading: Load relationships efficiently to avoid N+1 queries 4. Caching: Cache frequently accessed relationships 5. Validation: Validate relationship integrity 6. Events: Use relationship events for business logic 7. Constraints: Apply appropriate relationship constraints 8. Performance: Monitor relationship query performance 9. Testing: Test all relationship scenarios 10. Documentation: Document complex relationship logic
Related Topics
- database-overview-rust
- Database integration overview
- query-builder-rust
- Fluent query interface
- orm-models-rust
- Model definition and usage
- migrations-rust
- Database schema versioning
- database-transactions-rust
- Transaction handling
---
Ready to build type-safe, performant model relationships with Rust and TuskLang?