Skip to content

ORM 基础

ORM(Object-Relational Mapping,对象关系映射)是一种将数据库表映射为 PHP 对象的技术。ORM 消除了手动编写 SQL 语句的需要,通过操作对象来间接操作数据库。现代 PHP 框架的 ORM(如 Laravel Eloquent、Doctrine、ThinkPHP ORM)都遵循 ActiveRecord 或 Data Mapper 模式。本节将系统介绍 ORM 的核心概念、设计模式和各种操作方式。

前置知识

阅读本节前,建议先了解:

基础概念

什么是 ORM

ORM 的核心思想是:将数据库表映射为类,将行映射为对象实例,将列映射为对象属性。

数据库表 users           → PHP 类 User
┌────┬──────────┬─────────┐
│ id │ name     │ email   │
├────┼──────────┼─────────┤
│  1 │ Alice    │ a@b.com │
│  2 │ Bob      │ b@c.com │
└────┴──────────┴─────────┘
       ↓ ORM 映射 ↓

class User {
    public int $id;
    public string $name;
    public string $email;
}

$user = User::find(1);  // SELECT * FROM users WHERE id = 1
$user->name;             // Alice

ORM 模式对比

模式代表特点
ActiveRecordLaravel Eloquent、Yii AR模型即表,模型承担 CRUD
Data MapperDoctrine ORM模型是纯数据对象,由 Repository 操作
Row Data GatewayZend_Db表网关模式

详细说明

1. 模型定义

php
<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class User extends Model
{
    // 关联的表名(默认为类名的蛇形复数)
    protected $table = 'users';

    // 可批量赋值的字段
    protected $fillable = [
        'name',
        'email',
        'password',
        'is_active',
    ];

    // 隐藏字段(序列化时不输出)
    protected $hidden = [
        'password',
        'remember_token',
    ];

    // 类型转换
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
        'is_active' => 'boolean',
        'settings' => 'array',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    // 默认属性值
    protected $attributes = [
        'is_active' => true,
    ];

    // 日期字段
    protected $dates = ['created_at', 'updated_at', 'deleted_at'];

    // 一对多关系
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    // 属于关系
    public function department(): BelongsTo
    {
        return $this->belongsTo(Department::class);
    }

    // 多对多关系
    public function roles()
    {
        return $this->belongsToMany(Role::class)->withTimestamps();
    }

    // 作用域
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    public function scopeRecent($query, int $days = 7)
    {
        return $query->where('created_at', '>=', now()->subDays($days));
    }
}

2. 基本 CRUD 操作

php
<?php
declare(strict_types=1);

use App\Models\User;

// ====== Create(创建)======

// 方式一:使用 create
$user = User::create([
    'name' => 'Alice',
    'email' => 'alice@example.com',
    'password' => 'secret',
]);

// 方式二:实例化后保存
$user = new User();
$user->name = 'Bob';
$user->email = 'bob@example.com';
$user->password = 'secret';
$user->save();

// 方式三:firstOrCreate
$user = User::firstOrCreate(
    ['email' => 'alice@example.com'],   // 查找条件
    ['name' => 'Alice', 'password' => 'secret']  // 不存在时创建的属性
);

// 方式四:firstOrNew
$user = User::firstOrNew(
    ['email' => 'alice@example.com'],
    ['name' => 'Alice']
);
$user->save();

// ====== Read(查询)======

// 通过主键查找
$user = User::find(1);

// 查找所有
$users = User::all();

// 条件查询
$activeUsers = User::where('is_active', true)->get();

// 链式查询
$users = User::where('is_active', true)
    ->where('created_at', '>=', '2024-01-01')
    ->orderBy('name', 'asc')
    ->limit(20)
    ->get();

// 使用作用域
$recentActiveUsers = User::active()->recent(30)->get();

// 分页
$users = User::active()->paginate(20);

// 获取单个结果
$user = User::where('email', 'alice@example.com')->first();
$user = User::where('email', 'alice@example.com')->firstOrFail();

// pluck(获取单列)
$names = User::pluck('name', 'id');

// 查找或失败
$user = User::findOrFail(42);

// 聚合函数
$count = User::where('is_active', true)->count();
$max = User::max('id');
$avg = User::avg('score');
$sum = User::sum('balance');
$exists = User::where('email', 'alice@example.com')->exists();

// ====== Update(更新)======

// 通过属性修改
$user = User::find(1);
$user->name = 'Updated Name';
$user->save();

// 批量更新
User::where('is_active', false)->update(['is_active' => true]);
User::where('department_id', 5)->increment('login_count');
User::where('id', 1)->decrement('balance', 100);

// ====== Delete(删除)======

// 通过实例删除
$user->delete();

// 通过主键删除
User::destroy(1);
User::destroy([1, 2, 3]);

// 条件删除
User::where('is_active', false)->delete();

// 软删除
// 需要在模型中使用 SoftDeletes trait
User::where('id', 1)->delete();  // deleted_at 字段被设置
User::onlyTrashed()->get();        // 仅查询已软删除的记录
User::withTrashed()->get();        // 包含已软删除的记录
User::where('id', 1)->restore();  // 恢复软删除的记录
User::where('id', 1)->forceDelete(); // 永久删除

3. 关联关系

php
<?php
declare(strict_types=1);

use App\Models\User;
use App\Models\Post;
use App\Models\Comment;

// 一对一
$user = User::find(1);
$profile = $user->profile;  // 返回 Profile 模型或 null

// 一对多
$user = User::find(1);
$posts = $user->posts;  // 返回 Post Collection

// 创建关联
$user->posts()->create([
    'title' => 'First Post',
    'content' => 'Hello World',
]);

// 属于
$post = Post::find(1);
$user = $post->user;  // 返回 User 模型

// 多对多
$user = User::find(1);
$roles = $user->roles;  // 返回 Role Collection

$user->roles()->attach($roleId);        // 添加关联
$user->roles()->detach($roleId);        // 移除关联
$user->roles()->sync([$role1Id, $role2Id]); // 同步关联

// 预加载(解决 N+1 查询问题)
$users = User::with('posts', 'roles')->get();

foreach ($users as $user) {
    $user->posts;  // 不会产生额外查询
}

// 延迟预加载
$users = User::all();
$users->load('posts', 'roles');

// 嵌套预加载
$users = User::with('posts.comments')->get();

// 预加载计数
$users = User::withCount('posts')->get();

4. 查询构建器

php
<?php
declare(strict_types=1);

use App\Models\User;

// where 子句
User::where('age', '>=', 18)->get();
User::where('name', 'like', '%Alice%')->get();
User::where('status', ['active', 'pending'])->get();
User::whereBetween('age', [18, 30])->get();
User::whereNotIn('role', ['admin', 'moderator'])->get();
User::whereNull('deleted_at')->get();
User::whereNotNull('email_verified_at')->get();
User::whereDate('created_at', '2024-01-01')->get();

// 排序
User::orderBy('name', 'asc')->get();
User::latest()->get();             // 按创建时间降序
User::inRandomOrder()->first();      // 随机

// 分组与聚合
User::selectRaw('department_id, COUNT(*) as count')
    ->groupBy('department_id')
    ->having('count', '>', 10)
    ->get();

// 子查询
User::whereIn('id', fn($q) => $q->select('user_id')
    ->from('posts')
    ->groupBy('user_id')
)->get();

// Join
User::join('departments', 'users.department_id', '=', 'departments.id')
    ->select('users.*', 'departments.name as dept_name')
    ->get();

// chunk 分块处理(大量数据处理)
User::chunk(200, function ($users) {
    foreach ($users as $user) {
        // 处理 200 条记录
    }
});

实战示例

场景一:复杂查询封装

php
<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    public function scopePending($query): Builder
    {
        return $query->where('status', 'pending');
    }

    public function scopeCompleted($query): Builder
    {
        return $query->where('status', 'completed');
    }

    public function scopeByDateRange($query, string $from, string $to): Builder
    {
        return $query->whereBetween('created_at', [$from, $to]);
    }

    public function scopeWithTotal($query): Builder
    {
        return $query->withSum('items as total_amount', 'price * quantity');
    }

    // 组合作用域
    public static function findPendingInLastWeek(): Builder
    {
        return static::pending()->byDateRange(
            now()->subWeek()->toDateString(),
            now()->toDateString()
        )->withTotal();
    }
}

场景二:模型事件

php
<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    // 模型创建后
    protected static function booted(): void
    {
        static::created(function (User $user) {
            // 发送欢迎邮件
            \App\Jobs\SendWelcomeEmail::dispatch($user->id);
        });

        static::updated(function (User $user) {
            // 记录变更日志
        });
    }

    // 单个模型事件
    protected static function boot(): void
    {
        parent::boot();

        static::creating(function (User $user) {
            $user->api_token = bin2hex(random_bytes(32));
        });

        static::deleting(function (User $user) {
            // 删除前检查
            if ($user->is_admin) {
                throw new \RuntimeException('Cannot delete admin user');
            }
        });
    }
}

注意事项

1. N+1 查询问题

php
<?php
// ❌ N+1 查询问题
$users = User::all();       // 1 次查询
foreach ($users as $user) {
    echo $user->posts->count(); // N 次查询(每个用户 1 次)
}
// 总共 1 + N 次查询

// ✅ 使用预加载
$users = User::with('posts')->get();  // 1 次查询(JOIN 或额外查询)
foreach ($users as $user) {
    echo $user->posts->count();     // 0 次额外查询
}
// 总共 2 次查询

2. 大量数据的内存管理

php
<?php
// ❌ 内存溢出风险
$users = User::all();  // 加载所有记录到内存

// ✅ 使用 cursor 或 chunk
foreach (User::cursor() as $user) {
    // 逐行读取,内存占用低
}

// ✅ 使用 chunk
User::chunk(100, function ($users) {
    foreach ($users as $user) {
        // 处理 100 条
    }
});

最佳实践

1. 使用模型工厂和填充

php
<?php
// database/factories/UserFactory.php
namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    protected $model = User::class;

    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'password' => 'password',
            'is_active' => true,
        ];
    }
}

// 使用
User::factory()->count(100)->create();

2. 使用查询范围封装条件

php
<?php
// ✅ 推荐:使用作用域
class User extends Model
{
    public function scopeActive($query) { return $query->where('is_active', true); }
}

$activeUsers = User::active()->get();

// ❌ 不推荐:到处重复条件
$users = User::where('is_active', true)->get();

下一节

继续学习:Laravel 框架 — 了解如何在实际框架中使用 ORM。

参考链接