/ Relations

Relations

Miko ORM provides powerful relationship management. Define HasOne, HasMany, BelongsTo, and BelongsToMany relationships between your models.


Relationship Types Summary

Relationship Method Description
HasOnehasOne()One-to-one (User → Profile)
HasManyhasMany()One-to-many (User → Orders)
BelongsTobelongsTo()Inverse relation (Order → User)
BelongsToManybelongsToMany()Many-to-many (User ↔ Roles)
Eager Loadingwith()Prevents N+1 query problem
Lazy Loadingload()Load on demand

HasOne - One-to-One Relationship

Used when a user has exactly one profile.

Model Definition

class User extends Model
{
    protected static string $table = 'users';
    protected string $primaryKey = 'Id';
    
    /**
     * Get user's profile
     */
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class, 'UserId', 'Id');
    }
}

class Profile extends Model
{
    protected static string $table = 'profiles';
    
    /**
     * Get profile's owner
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class, 'UserId', 'Id');
    }
}

Usage

// Get user's profile
$user = User::find(1);
$profile = $user->profile;

echo $profile->Bio;
echo $profile->Avatar;

// Access user through profile
$profile = Profile::find(1);
$user = $profile->user;

echo $user->Name;

HasMany - One-to-Many Relationship

Used when a user has multiple orders.

Model Definition

class User extends Model
{
    /**
     * Get user's orders
     */
    public function orders(): HasMany
    {
        return $this->hasMany(Order::class, 'UserId', 'Id');
    }
    
    /**
     * Get user's comments
     */
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class, 'UserId');
    }
}

Usage

// Get all user's orders
$user = User::find(1);
$orders = $user->orders;

foreach ($orders as $order) {
    echo $order->OrderNumber . ' - ' . $order->TotalAmount;
}

// Create new record through relationship
$user->orders()->create([
    'OrderNumber' => 'ORD-001',
    'TotalAmount' => 150.00,
    'Status' => 'pending'
]);

// Save existing model to relationship
$order = new Order(['OrderNumber' => 'ORD-002']);
$user->orders()->save($order);

BelongsTo - Inverse Relationship

Inverse of HasOne or HasMany relationship.

Model Definition

class Order extends Model
{
    protected static string $table = 'orders';
    
    /**
     * Get order's owner
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class, 'UserId', 'Id');
    }
    
    /**
     * Get order's category
     */
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class, 'CategoryId');
    }
}

Usage

// Get order's user
$order = Order::find(1);
$user = $order->user;

echo "Order owner: " . $user->Name;

// Associate - Link to parent
$user = User::find(5);
$order->user()->associate($user);
$order->save();

// Dissociate - Remove link
$order->user()->dissociate();
$order->save();

BelongsToMany - Many-to-Many Relationship

Used for users and roles type relationships.

Database Structure

users          user_roles       roles
------         ----------       ------
Id             UserId           Id
Name           RoleId           Name

Model Definition

class User extends Model
{
    /**
     * Get user's roles
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(
            Role::class,      // Related model
            'user_roles',     // Pivot table
            'UserId',         // This model's foreign key
            'RoleId'          // Related model's foreign key
        );
    }
}

class Role extends Model
{
    /**
     * Get users with this role
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'user_roles', 'RoleId', 'UserId');
    }
}

Usage

// Get user's roles
$user = User::find(1);
$roles = $user->roles;

foreach ($roles as $role) {
    echo $role->Name;
}

// Attach role
$user->roles()->attach(1);              // Single role
$user->roles()->attach([1, 2, 3]);      // Multiple roles

// Detach role
$user->roles()->detach(1);              // Single role
$user->roles()->detach([1, 2]);         // Multiple roles
$user->roles()->detach();               // All roles

// Sync - Only specified roles remain
$user->roles()->sync([1, 2, 3]);

// Toggle - Attach if not exists, detach if exists
$user->roles()->toggle([1, 2]);

Eager Loading - N+1 Query Problem

The Problem

// N+1 query problem - BAD
$users = User::all();

foreach ($users as $user) {
    echo $user->profile->Bio;  // Separate query each loop!
}
// 1 (users) + N (profiles) = N+1 queries

Solution: with()

// With eager loading - GOOD
$users = User::with('profile')->get();

foreach ($users as $user) {
    echo $user->profile->Bio;  // No query, already loaded
}
// Only 2 queries: SELECT * FROM users + SELECT * FROM profiles WHERE UserId IN (...)

Multiple Relations

// Load multiple relations
$users = User::with('profile', 'orders', 'roles')->get();

// Nested relations
$users = User::with('orders.items.product')->get();

// Conditional eager loading
$users = User::with(['orders' => function($query) {
    $query->where('Status', 'completed')
          ->orderBy('CreatedDate', 'desc');
}])->get();

Lazy Loading - load()

Load relations on already retrieved models.

$user = User::find(1);

// Load relation later
$user->load('orders');
$user->load('profile', 'roles');

// Conditional lazy loading
$user->load(['orders' => function($query) {
    $query->where('TotalAmount', '>', 100);
}]);

Relationship Queries

has() - Records With Relationship

// Users with at least one order
$users = User::has('orders')->get();

// Users with more than 5 orders
$users = User::has('orders', '>', 5)->get();

// Orders with at least one comment
$orders = Order::has('comments')->get();

whereHas() - Conditional Relationship Query

// Users with completed orders
$users = User::whereHas('orders', function($query) {
    $query->where('Status', 'completed');
})->get();

// Users with orders over $1000
$users = User::whereHas('orders', function($query) {
    $query->where('TotalAmount', '>', 1000);
})->get();

withCount() - Relationship Count

// Get users with order count
$users = User::withCount('orders')->get();

foreach ($users as $user) {
    echo $user->Name . ': ' . $user->orders_count . ' orders';
}

// Multiple relationship counts
$users = User::withCount(['orders', 'comments', 'reviews'])->get();

Complete Example: E-Commerce Models

// User.php
class User extends Model
{
    use HasTimestamps, SoftDeletes;
    
    protected static string $table = 'users';
    
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class, 'UserId');
    }
    
    public function orders(): HasMany
    {
        return $this->hasMany(Order::class, 'UserId');
    }
    
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'user_roles', 'UserId', 'RoleId');
    }
}

// Order.php
class Order extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class, 'UserId');
    }
    
    public function items(): HasMany
    {
        return $this->hasMany(OrderItem::class, 'OrderId');
    }
}

// Usage
$user = User::with(['profile', 'orders.items.product'])->find(1);

echo "User: " . $user->Name;
echo "Profile: " . $user->profile->Bio;

foreach ($user->orders as $order) {
    echo "Order: " . $order->OrderNumber;
    
    foreach ($order->items as $item) {
        echo "  - " . $item->product->Name . " x " . $item->Quantity;
    }
}