Laravel: Laravel Eloquent ORM基础
最后更新:2026-08-26
Eloquent 是 Laravel 的"数据库翻译官"——你用 PHP 对象和它对话,它帮你翻译成 SQL 执行。
1. 你将学到
- 模型创建与属性定义:$fillable/$guarded/$casts/$attributes
- CRUD 全流程:create/all/find/update/delete 与批量赋值
- 查询构造器:where/orderBy/groupBy/子查询
- 集合操作:filter/map/reduce/each 链式处理
- 软删除与恢复:SoftDeletes trait
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 类关系图
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,触发模型事件。需要触发事件时用逐个更新。📖 小节
- Eloquent 用 PHP 对象操作数据库,自动参数绑定防 SQL 注入
- $fillable 白名单保护批量赋值,$casts 自动类型转换
- CRUD 四步:create/read/update/delete,findOrFail 返回 404
- 查询构造器支持 where/orderBy/groupBy/子查询
- Collection 提供比数组更强大的链式操作(filter/map/sortBy)
- 软删除标记 deleted_at 而非真正删除,支持 restore 恢复
📝 作业
-
基础题(⭐):为 ShopMetrics 创建 Product 模型,定义 $fillable 和 $casts,实现完整的 CRUD 操作(创建、查询、更新、删除),使用 tinker 验证每个操作。
-
进阶题(⭐⭐):编写一个查询,获取当前租户下营收最高的 5 个商店及其订单数量,使用 selectSub 子查询和 Collection 的 map 方法格式化输出。
-
挑战题(⭐⭐⭐):实现 Order 模型的软删除 + 级联恢复:删除 Order 时同时软删除其 OrderItems,恢复时一起恢复,使用模型事件监听实现。