Laravel: Laravel Eloquent ORM基础

最后更新:2026-08-26

Eloquent 是 Laravel 的"数据库翻译官"——你用 PHP 对象和它对话,它帮你翻译成 SQL 执行。

1. 你将学到


2. 一个全栈开发者的真实故事

(1) 痛点:SQL 字符串拼接导致注入和数据丢失

Bob 早期用原生 PHP 写 ShopMetrics——所有 SQL 都靠字符串拼接:"SELECT * FROM shops WHERE id = " . $_GET['id']。Alice 的店铺 ID 被黑客注入了 1 OR 1=1,全站数据泄露。更日常的问题是,Bob 忘了给 UPDATE 加 WHERE,一条命令把所有商店的 revenue 都清零了,恢复数据花了整整一天。

(2) Eloquent ORM 的解法

Eloquent 用 PHP 对象操作数据库,自动参数绑定防注入,批量赋值保护敏感字段,软删除防止误删。

PHP
// Safe, readable, no SQL injection possible
$shop = Shop::create([
    'name' => 'Alice Store',
    'tenant_id' => 1,
]);

// Mass assignment protection — only $fillable fields allowed
protected $fillable = ['name', 'slug', 'tenant_id'];
// revenue is NOT in $fillable — can't be set via create()

(3) 收益

Bob 用 Eloquent 后,SQL 注入 0 风险,误删数据用软删除一键恢复,代码量从 200 行 SQL 缩减到 30 行 PHP。


3. 模型定义

(1) 创建模型

BASH
php artisan make:model Shop
# Creates: app/Models/Shop.php

# With migration
php artisan make:model Shop -m
# Creates: app/Models/Shop.php + database/migrations/create_shops_table.php

(2) 模型属性配置

PHP
// app/Models/Shop.php
class Shop extends Model
{
    protected $fillable = [
        'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
    ];

    protected $guarded = ['id']; // Alternative: block specific fields

    protected $attributes = [
        'status' => 'active',
        'revenue' => 0,
    ];

    protected $casts = [
        'revenue' => 'decimal:2',
        'is_active' => 'boolean',
        'metadata' => 'json',
        'launched_at' => 'datetime',
    ];
}
属性 作用 推荐方式
$fillable 允许批量赋值的字段 ✅ 白名单
$guarded 禁止批量赋值的字段 ❌ 黑名单
$casts 自动类型转换 必用
$attributes 字段默认值 替代 DB 默认值

(3) Eloquent Model 类关系图

100%
classDiagram
    class Model {
        +save()
        +delete()
        +update(array data)
        +fresh()
        +refresh()
        +toArray()
        +toJson()
    }
    class Shop {
        +array fillable
        +array casts
        +tenant()
        +orders()
        +products()
    }
    class SoftDeletes {
        +forceDelete()
        +restore()
        +trashed()
        +withTrashed()
        +onlyTrashed()
    }
    Model <|-- Shop
    Shop ..|> SoftDeletes : uses trait

▶ 示例:ShopMetrics Shop 模型

PHP
// app/Models/Shop.php
class Shop extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
    ];

    protected $casts = [
        'revenue' => 'decimal:2',
        'metadata' => 'array',
    ];

    protected $attributes = [
        'status' => 'active',
        'revenue' => 0,
    ];

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }

    public function products(): HasMany
    {
        return $this->hasMany(Product::class);
    }

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

输出:

TEXT 📖 仅展示
// 执行成功

4. CRUD 操作

(1) Create

PHP
// Method 1: create() with mass assignment
$shop = Shop::create([
    'tenant_id' => 1,
    'name' => 'Alice Store',
    'slug' => 'alice-store',
]);

// Method 2: new + save
$shop = new Shop();
$shop->tenant_id = 1;
$shop->name = 'Alice Store';
$shop->slug = 'alice-store';
$shop->save();

// Method 3: firstOrCreate — find or create
$shop = Shop::firstOrCreate(
    ['slug' => 'alice-store'],           // search criteria
    ['name' => 'Alice Store', 'tenant_id' => 1], // values if creating
);

// Method 4: updateOrCreate — update or create
$shop = Shop::updateOrCreate(
    ['slug' => 'alice-store'],
    ['name' => 'Alice Store Updated', 'revenue' => 5000],
);

(2) Read

PHP
// Find by primary key
$shop = Shop::find(1);
$shop = Shop::findOrFail(1);  // throws 404 if not found

// Find by column
$shop = Shop::where('slug', 'alice-store')->first();
$shop = Shop::whereSlug('alice-store')->firstOrFail();

// Get all
$shops = Shop::all();
$shops = Shop::active()->get();  // using scope

// Chunk for large datasets
Shop::chunk(200, function ($shops) {
    foreach ($shops as $shop) {
        // Process 200 shops at a time
    }
});

(3) Update

PHP
// Update single model
$shop->update(['name' => 'New Name']);

// Update via query
Shop::where('status', 'suspended')->update(['status' => 'active']);

// Increment/Decrement
$shop->increment('revenue', 1500);
Shop::whereId(1)->decrement('stock', 5);

(4) Delete

PHP
// Soft delete (sets deleted_at)
$shop->delete();

// Force delete (permanent)
$shop->forceDelete();

// Restore soft-deleted
$shop->restore();

// Query with trashed
Shop::withTrashed()->where('id', 1)->first();
Shop::onlyTrashed()->get();

▶ 示例:ShopMetrics 完整 CRUD 流程

PHP
// Create a shop with products
$shop = Shop::create([
    'tenant_id' => 1,
    'name' => 'Bob Electronics',
    'slug' => 'bob-electronics',
]);

$shop->products()->createMany([
    ['name' => 'Widget A', 'sku' => 'W-001', 'price' => 29.99],
    ['name' => 'Widget B', 'sku' => 'W-002', 'price' => 49.99],
]);

// Read with eager loading
$shop = Shop::with('products')->whereSlug('bob-electronics')->firstOrFail();

// Update shop and product
$shop->update(['revenue' => 15000]);
$shop->products()->whereSku('W-001')->update(['price' => 34.99]);

// Soft delete and restore
$shop->delete();
Shop::withTrashed()->whereSlug('bob-electronics')->first()->restore();

输出:

TEXT 📖 仅展示
// 执行成功

5. 查询构造器

(1) 条件查询

PHP
$shops = Shop::where('status', 'active')
    ->where('revenue', '>', 1000)
    ->orWhere(function ($query) {
        $query->where('status', 'new')
              ->where('created_at', '>', now()->subDays(7));
    })
    ->get();

// Dynamic where
$shops = Shop::whereStatus('active')
    ->whereRevenueGreaterThan(1000)
    ->get();

(2) 排序、分组与分页

PHP
// OrderBy
$shops = Shop::orderBy('revenue', 'desc')->get();

// GroupBy with having
$revenueByStatus = Shop::select('status', DB::raw('SUM(revenue) as total'))
    ->groupBy('status')
    ->having('total', '>', 1000)
    ->get();

// Pagination
$shops = Shop::where('tenant_id', 1)->paginate(15);
$shops = Shop::where('tenant_id', 1)->simplePaginate(15);
$shops = Shop::where('tenant_id', 1)->cursorPaginate(15);
分页方法 执行查询 适用场景
paginate() COUNT + SELECT 需要总页数
simplePaginate() 只 SELECT 不需要总页数
cursorPaginate() 只 SELECT 用 WHERE 大数据集最高效

(3) 子查询

PHP
// Subquery in select
$shops = Shop::select('shops.*')
    ->selectSub(
        Order::selectRaw('SUM(total)')
            ->whereColumn('shop_id', 'shops.id'),
        'orders_total'
    )
    ->get();

// Subquery in where
$latestOrders = Shop::where('created_at', function ($query) {
    $query->selectRaw('MAX(created_at)')
          ->from('orders')
          ->whereColumn('shop_id', 'shops.id');
})->get();

▶ 示例:ShopMetrics 复杂查询

PHP
// Top 10 shops by revenue in current tenant, with order count
$topShops = Shop::select('shops.*')
    ->selectSub(
        Order::selectRaw('COUNT(*)')
            ->whereColumn('shop_id', 'shops.id')
            ->where('created_at', '>=', now()->subDays(30)),
        'recent_orders_count'
    )
    ->where('tenant_id', tenant()->id)
    ->where('status', 'active')
    ->orderBy('revenue', 'desc')
    ->take(10)
    ->get();

输出:

TEXT 📖 仅展示
// 执行成功

6. 集合操作

Eloquent get() 返回 Collection 对象,提供比数组更强大的链式操作方法。

方法 作用 类似 SQL
filter() 过滤 WHERE
map() 映射转换 SELECT 转换
sortBy() 排序 ORDER BY
groupBy() 分组 GROUP BY
sum() 求和 SUM()
count() 计数 COUNT()
pluck() 提取列 SELECT one column
unique() 去重 DISTINCT
each() 遍历执行
reduce() 累积计算

▶ 示例:ShopMetrics 集合链式操作

PHP
// Get all shops for a tenant, filter and transform
$topShops = Shop::where('tenant_id', 1)
    ->with('products')
    ->get()
    ->filter(fn ($shop) => $shop->revenue > 1000)
    ->sortByDesc('revenue')
    ->map(fn ($shop) => [
        'name' => $shop->name,
        'revenue' => $shop->revenue,
        'product_count' => $shop->products->count(),
    ])
    ->take(10);

// Group shops by status and count
$shopsByStatus = Shop::where('tenant_id', 1)
    ->get()
    ->groupBy('status')
    ->map(fn ($group) => $group->count());
// ['active' => 15, 'suspended' => 2, 'closed' => 1]

// Pluck IDs for bulk operation
$shopIds = Shop::where('status', 'active')->pluck('id');
// [1, 2, 5, 8, 12]

输出:

TEXT 📖 仅展示
// 执行成功

7. 软删除

(1) 启用软删除

PHP
// Model
class Shop extends Model
{
    use SoftDeletes;

    protected $casts = [
        'deleted_at' => 'datetime',
    ];
}

// Migration
$table->softDeletes(); // adds deleted_at TIMESTAMP NULL

(2) 软删除操作

PHP
// Delete (soft — sets deleted_at)
$shop->delete();

// Check if trashed
$shop->trashed(); // true if soft-deleted

// Include trashed records
Shop::withTrashed()->get();

// Only trashed records
Shop::onlyTrashed()->get();

// Restore
$shop->restore();

// Permanent delete
$shop->forceDelete();

▶ 示例:ShopMetrics 软删除恢复场景

PHP
// Alice accidentally deleted a shop
$shop = Shop::whereSlug('alice-store')->first();
$shop->delete();

// Bob can still find it in trashed records
$trashed = Shop::onlyTrashed()->whereSlug('alice-store')->first();

// Restore the shop with all relationships intact
if ($trashed) {
    $trashed->restore();
    // $trashed->products still exist — they weren't deleted
}

输出:

TEXT 📖 仅展示
// 执行成功

8. 综合示例:ShopMetrics 订单分析

PHP
// ============================================
// Comprehensive: ShopMetrics Order Analytics
// Covers: CRUD, queries, collections, soft delete, scopes
// ============================================

// app/Models/Order.php
class Order extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'tenant_id', 'shop_id', 'user_id', 'order_number',
        'subtotal', 'discount', 'total', 'status', 'metadata',
    ];

    protected $casts = [
        'total' => 'decimal:2',
        'metadata' => 'array',
        'deleted_at' => 'datetime',
    ];

    public function shop(): BelongsTo
    {
        return $this->belongsTo(Shop::class);
    }

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

    public function scopeThisMonth(Builder $query): Builder
    {
        return $query->whereBetween('created_at', [
            now()->startOfMonth(), now()->endOfMonth(),
        ]);
    }
}

// Analytics query — monthly revenue report
$monthlyReport = Order::where('tenant_id', tenant()->id)
    ->completed()
    ->thisMonth()
    ->with('shop')
    ->get()
    ->groupBy('shop.name')
    ->map(fn ($orders) => [
        'shop' => $orders->first()->shop->name,
        'order_count' => $orders->count(),
        'revenue' => $orders->sum('total'),
        'avg_order' => $orders->avg('total'),
    ])
    ->sortByDesc('revenue')
    ->values();

❓ 常见问题

Q $fillable 和 $guarded 有什么区别?
A $fillable 是白名单(只允许这些字段批量赋值),$guarded 是黑名单(禁止这些字段)。推荐用 $fillable 白名单,更安全——必须显式声明允许赋值的字段。
Q 什么时候用 findOrFail?
A 当找不到记录时需要返回 404 页面时用 findOrFail。如果找不到是正常业务逻辑(如搜索无结果),用 find + 判空。
Q Collection 的方法和查询构造器的方法有什么区别?
A 查询构造器在数据库层面执行(如 where 在 SQL 中过滤),Collection 在内存中执行(如 filter 在 PHP 中过滤)。大数据集应在查询层面过滤,小结果集可以用 Collection 方法。
Q 软删除的模型关联怎么办?
A 软删除只标记 deleted_at,关联数据仍在数据库中。恢复父模型后,关联立即可用。如果需要级联软删除,可以在模型的 boot() 中监听 deleting 事件。
Q Eloquent 和 Query Builder 怎么选?
A 需要模型实例(调用关联、使用 trait)时用 Eloquent;只需简单查询结果不需要模型时用 Query Builder(DB::table())。Eloquent 底层就是 Query Builder 的封装。
Q 批量更新和逐个更新有什么区别?
A Shop::where(...)->update([...]) 执行一条 SQL 更新所有匹配记录,高效;$shops->each->update([...]) 逐条执行 SQL,触发模型事件。需要触发事件时用逐个更新。

📖 小节


📝 作业

  1. 基础题(⭐):为 ShopMetrics 创建 Product 模型,定义 $fillable 和 $casts,实现完整的 CRUD 操作(创建、查询、更新、删除),使用 tinker 验证每个操作。

  2. 进阶题(⭐⭐):编写一个查询,获取当前租户下营收最高的 5 个商店及其订单数量,使用 selectSub 子查询和 Collection 的 map 方法格式化输出。

  3. 挑战题(⭐⭐⭐):实现 Order 模型的软删除 + 级联恢复:删除 Order 时同时软删除其 OrderItems,恢复时一起恢复,使用模型事件监听实现。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏