Laravel 编码技巧 - DB 查询
重要或克隆 query()
通常情况下,我们需要从过滤后的查询中进行更多次查询。因此,大多数时候我们使用 query() 方法,让我们编写一个查询,获取今天创建的活动和非活动产品。
$query = Product::query();
$today = request()->q_date ?? today();
if ($today) {
$query->where('created_at', $today);
}
// 让我们获取可用和不可用的产品
$activeProducts = $query->where('status', 1)->get(); // 这一行 修改了$query 对象变量
$inactiveProducts = $query->where('status', 0)->get(); // 所以这里我们将获取不到任何不可用产品
但是,在获得 $activeProducts
产品后 $query
将被修改。因此,$inactiveProducts
不会从 $query
中找到任何非活动产品,并且每次都会返回空白集合。原因,这将尝试从 $activeProducts
中查找非活动产品( $query
将仅返回活动产品)。
为了解决这个问题,我们可以通过重用这个 $query
对象来多次查询。因此,在执行任何 $query
修改操作之前,我们需要克隆它 $query
。
$activeProducts = $query->clone()->where('status', 1)->get(); // 它不会修改 $query
$inactiveProducts = $query->clone()->where('status', 0)->get(); // 这样我们就可以从 $query 中获取不活动的产品
为什么要使用 clone() 方法?
在原始的代码中,通过链式调用 where 方法来添加筛选条件。然而,由于使用了同一个查询对象 $query,当执行第一个
where('status', 1)->get()
后,查询对象的状态已经被修改了。这会导致后续的where('status', 0)->get()
也只会返回状态为 1 的产品。为了避免这种情况,可以使用
clone
方法创建一个新的查询对象副本,然后分别对副本进行不同的筛选条件设置。这样每个查询对象都是独立的,互不干扰。
记住在原始查询中使用绑定
您可以将绑定数组传递给大多数原始查询方法以避免SQL注入。
$fullname = request('full_name');
// 这容易受到SQL注入的攻击
$user1 = User::whereRaw('CONCAT(first_name, last_name) = ?', $fullname)->get();
// 使用绑定
$user2 = User::whereRaw('CONCAT(first_name, last_name) = :name', ['name' => $fullname])->get();
使用 Laravel 在 MySQL 上进行全文搜索的小抄
迁移文件
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->fullText(['title', 'description']);
$table->timestamps();
});
自然语言
搜索 voluptatem
$comment1 = Comment::whereFullText(['title', 'description'], 'voluptatem')->get();
具有查询扩展的自然语言
搜索 voluptatem
并使用结果来执行更大的查询
$comment2 = Comment::whereFullText(['title', 'description'], 'voluptatem', ['expanded' => true])->get();
['expanded' => true]
:表示搜索结果需要被展开,即返回所有匹配的记录,而不仅仅是部分匹配的记录。
布尔模式
搜索 voluptatem
和排除 else
$comment3 = Comment::whereFullText(['title', 'description'], 'voluptatem', ['mode' => 'boolean'])->get();
['mode' => 'boolean']
:表示查询结果需要以布尔值的形式返回,即如果找到匹配的记录则返回true,否则返回false。
合并 eloquent collections
Eloquent 集合的 merge 方法使用 id 来避免重复的模型。
但是,如果您要合并不同模型的集合,则可能会导致意外结果。
请改用基本集合方法。
$products = Product::all();
$comments = Comment::all();
//如果 Product 与 Comment 具有相同的 id,它们将被替换
//你会丢失 Product
$allProducts = $products->merge($comments);
// 在你的 Laravel 集合中调用 toBase()方法来使用合并。
$allProducts = $products->toBase()->merge($comments);
在不修改 updated_at 字段的情况下执行操作
如果你想在不修改模型的 updated_at
时间戳的情况下执行模型操作,你可以在给 withoutTimestamps
方法的闭包中对模型进行操作。
可从 Laravel 9.31 开始使用。
// 方法一
$user = User::find(2);
$user->update(['name' => '王美丽'], ['timestamps' => false]);
// 方法二
$user = User::first();
// updated_at 未更改...
User::withoutTimestamps(fn() =>
$user->update(['name' => '王大丽'])
);
你可以编写事务感知的代码
使用 DB::afterCommit()
方法,你可以编写仅在事务提交时执行的代码,并在事务回滚时丢弃。
如果没有交易,代码将立即执行。
DB::transaction(function () {
User::create([
'name' => '王大丽丽',
'first_name' => '王',
'last_name' => '大丽',
'email' => 'wangdal11i@173.com',
'password' => '123456'
]);
Product::create(['name' => '王大丽']);
});
class User extends Model
{
protected static function booted()
{
static::created(function ($user) {
// 只在事务提交后才发送邮件
DB::afterCommit(function () use ($user) {
Log::info('发送邮件');
});
});
}
}
DB::afterCommit()
方法的作用是在数据库事务成功提交后执行指定的回调函数。
在模型关联中使用 scopes 查询
你可以在定义模型关联时使用 Eloquent scopes 查询
app/Models/Lesson.php:
public function scopePublished($query)
{
return $query->where('is_published', true);
}
app/Models/Course.php:
// 课程和章节之间的关系
public function lessons(): HasMany
{
return $this->hasMany(Lesson::class);
}
// 查询已发布的章节
public function publidhedLessons(): HasMany
{
return $this->lessons()->published();
}
app/Http/Controllers/CourseController:
public function index()
{
return Course::query()->with('publidhedLessons')->first();
}
结果
{
"id": 1,
"name": "Clement Strosin",
"created_at": "2023-11-09T08:53:06.000000Z",
"updated_at": "2023-11-09T08:53:06.000000Z",
"publidhed_lessons": [
{
"id": 43,
"name": "Berniece Feest",
"course_id": 1,
"is_published": 1,
"created_at": "2023-11-09T08:53:06.000000Z",
"updated_at": "2023-11-09T08:53:06.000000Z"
}
]
}
app/Models/Course.php
文件中定义了两个方法:lessons()
和publishedLessons()
。
lessons()
方法返回与当前课程关联的所有课时,而publishedLessons()
方法则调用lessons()
方法并传入published()
方法作为参数,以获取所有已发布的课时。
Laravel 9.37 之后的新 rawValue() 方法
Laravel 9.37 新增了一个 rawValue() 方法,用于从 SQL 表达式中获取一个值。以下是来自拉取请求的一些示例:
$first = Product::orderBy('created_at', 'asc')->rawValue('year(`created_at`)'); // 2023
$last = Product::orderBy('created_at', 'desc')->rawValue('year(`created_at`)'); // 2023
$fullename = User::where('id', 1)->rawValue('concat(`first_name`, " ", `last_name`)'); // 王 大丽
当目标值为整数时,加载数据的速度更快
当目标值为整数时,不要使用 𝘄𝗵𝗲𝗿𝗲𝗜𝗻() 方法加载大范围的数据,而应该使用比 𝘄𝗵𝗲𝗿𝗲𝗜𝗻() 更快的 𝘄𝗵𝗲𝗿𝗲𝗜𝗻𝘁𝗲𝗴𝗲𝗿𝗜𝗻𝗥𝗮𝘄() 方法。
// 而不是使用whereIn
$products1 = Product::whereIn('id', range(1, 50))->get();
// 使用 WhereIntegerInRaw 方法以加快加载速度
$products2 = Product::query()->whereIntegerInRaw('id', range(1, 50))->get();
在两个时间戳之间完成加载数据
$products = Product::query()->whereBetween('created_at', [
// 加载在两个时间戳之间完成的任务
request()->from ?? '2023-11-08',
request()->to ?? '2023-11-10'
])->get();
传递原始查询以对结果进行排序
您可以传递原始查询来对结果进行排序。
例如,按任务完成截止日期前的时间对任务进行排序。
$products = Product::query()->whereNotNull('created_at')->orderByRaw('year(`created_at`) desc')->get();
根据日期查询的模型方法
在 Eloquent 中,使用函数 whereDay()
、 、 whereMonth()
whereYear()
whereDate()
和 whereTime()
检查日期。
$products = Product::whereDate('created_at', '2023-01-31')->get();
$products = Product::whereMonth('created_at', '2')->get();
$products = Product::whereDay('created_at', '3')->get();
$products = Product::whereYear('created_at', date('Y'))->get();
$products = Product::whereTime('created_at', '=', '14:13:58')->get();
递增和递减
如果要递增某个表中的某个 DB 列,只需使用 increment() 函数即可。对了,你不仅可以按 1 递增,还可以按某个数字递增,比如 50。
Post::find($post_id)->increment('view_count');
Post::find($post_id)->increment('view_count', 50);
无时间戳列
如果你的数据库表不包含时间戳字段 created_at 和 updated_at,您可以使用 $timestamps = false 属性指定 Eloquent 模型不使用它们。
class Company extends Model
{
public $timestamps = false;
}
软删除:多次恢复
在使用软删除时,您可以在一个句子中恢复多行。
Post::onlyTrashed()->where('author_id', 1)->restore();
模型 all 方法获取指定列
当调用 Eloquent 的 all()
方法时,你可以指定返回哪些列。
$products = Product::all(['id', 'name']);
失败与否
除了 findOrFail(),还有一个 Eloquent 方法 firstOrFail(),如果没有找到查询记录,该方法将返回 404 页面。
$product = Product::query()->where('id', 1000)->firstOrFail();
为字段指定别名
在 Eloquent Query Builder 中,你可以指定 “as” 以返回具有不同名称的任何列,就像在普通 SQL 查询中一样。
$product = Product::query()->select('id', 'name as product_name')->first();
结果集映射
在 Eloquent 查询后,您可以使用集合中的 map()
函数修改行。
$products = Product::query()->where('id', '<', 100)->get()
->map(function (Product $product) {
return ['id'=>$product->id, 'name'=>$product->name];
});
更改默认时间戳字段名
如果您使用的是非 Laravel 数据库,并且您的时间戳列命名不同,该怎么办?也许,你已经create_time和update_time。幸运的是,您也可以在模型中指定它们:
class Role extends Model
{
const CREATED_AT = 'create_time';
const UPDATED_AT = 'update_time';
}
按创建时间快速排序
而不是
User::orderBy('created_at', 'desc')->get();
你可以做得更快:
User::latest()->get();
默认情况下, latest()
将按 created_at
排序。
有一种相反的方法 oldest()
,可以按 created_at
升序排序:
User::oldest()->get();
此外,还可以指定另一列进行排序。例如,如果要使用 updated_at
,可以执行以下操作:
$lastUpdatedUser = User::latest('updated_at')->first();
创建记录时自动生成列值
如果您想在创建记录时生成某些数据库列值,请将其添加到模型的 boot() 方法中。例如,如果您有一个字段 "position",并想为新记录分配下一个可用的位置(如 Country::max('position') + 1
),请这样做:
class Country extends Model {
protected static function boot()
{
parent::boot();
Country::creating(function($model) {
$model->position = Country::max('position') + 1;
});
}
}
DB Raw 查询计算运行速度更快
使用SQL原始查询,如whereRaw()
方法,可以在查询中直接进行一些特定于数据库的计算,而不是在Laravel中,通常结果会更快。例如,如果你想获取在注册后 30 天仍然活跃的用户,可以使用以下代码:
$users = DB::table('users')
->whereRaw("DATEDIFF(NOW(), registration_date) >= 30")
->get();
不止一个范围
您可以在 Eloquent 中组合和链接查询范围,在一个查询中使用多个范围。
模型:
public function scopeActive($query) {
return $query->where('active', 1);
}
public function scopeRegisteredWithinDays($query, $days) {
return $query->where('created_at', '>=', now()->subDays($days));
}
某个控制器
$users = User::registeredWithinDays(30)->active()->get();
无需转换 Carbon
如果你正使用 whereDate()
查询今日的记录,可以直接使用 Carbon
的 now()
方法,它会自动转换为日期进行查询,而不需要指定 ->toDateString()
。
// 不用
$todayUsers = User::whereDate('created_at', now()->toDateString())->get();
// 不用做转换 只需要用 now()
$todayUsers = User::whereDate('created_at', now())->get();
根据首字母分组
你可以用任意自定义条件对 Eloquent 结果进行分组,下面的示例是由用户名的第一个单词进行分组:
$users = User::all()->groupBy(function ($item) {
return $item->name[0];
});
永不更新某个字段
如果你有一个数据库列,你只想设置一次并且永远不再更新它,你可以在Eloquent模型中使用一个修改器来设置这个限制:
- Laravel 9 及更高版本
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected function email(): Attribute
{
return Attribute::make(
set: fn ($value, $attributes) => $attributes['email'] ?? $value,
);
}
}
查找更多
模型方法 find()
可以接受多个参数,然后返回找到的所有记录集合。而不仅仅是一个模型:
// 返回一个模型
$user = User::find(1);
// 返回模型集合
$users = User::find([1,2,3]);
return Product::whereIn('id', $this->productIDs)->get();
// 你可以这样做
return Product::find($this->productIDs)
对于整数,请仅使用具有有限数据范围的 whereIn
,而不是使用比 whereIn 更快的 whereIntegerInRaw
。
Product::whereIn('id', range(1, 50))->get();
// 你可以这样做
Product::whereIntegerInRaw('id', range(1, 50))->get();
查找数据并返回多个列
Eloquent 方法可以 find()
接受多个参数,然后它返回一个 Collection 包含使用指定列找到的所有记录,而不是模型的所有列:
// 只返回包含 first_name, email 字段的模型
$user = User::find(1, ['first_name', 'email']);
// 只返回包含 first_name, email 字段的模型集合
$users = User::find([1,2,3], ['first_name', 'email'])
根据 key 查找
你也可以使用 whereKey()
方法来查找多条记录,该方法负责哪个字段是你的主键(id 是默认值,但你可以在 Eloquent 模型中覆盖它):
$users = User::query()->whereKey([1,2,3])->get();
使用 UUID 替代自增主键
你不想在你的模型中使用自增 ID?
- Laravel 9 及以上版本
迁移文件
Schema::create('uuids', function (Blueprint $table) {
$table->uuid('id')->unique();
$table->string('title')->index();
$table->timestamps();
});
模型文件
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class Uuid extends Model
{
use HasUuids;
protected $guarded = [];
}
控制器
$uuid = Uuid::create(['title'=>'使用 UUID 作为主键']);
$uuid->id;
Laravel 中的子查询
从 Laravel 6 开始,您可以在 Eloquent 语句中使用 addSelect(),并对添加的列进行一些计算。
return Destination::addSelect(['last_flight' => Flight::select('name')
->whereColumn('destination_id', 'destinations.id')
->orderBy('arrived_at', 'desc')
->limit(1)
])->get();
隐藏指定列
在进行 Eloquent 查询时,如果你想隐藏特定的字段,使其不被返回,最快的方法之一是在 Collection result
上添加 ->makeHidden()
。
$users = User::query()->get()->makeHidden(['id', 'password']);
捕获 DB 错误
如果你想捕捉 Eloquent 查询异常,使用特定的 QueryException 而不是默认的 Exception 类,你将能够获得错误的精确 SQL 代码。
try {
// 查询不存在的字段
$user = User::query()->select('ss')->get();
} catch (QueryException $e) {
// 捕获错误查询
$e->getCode(); // 错误代码
$e->getMessage(); // 错误信息
$e->getSql(); // 错误的 SQL 语句
$e->getBindings(); // 绑定的参数
$e->getFile(); // 抛出异常的文件
$e->getLine(); // 抛出异常的行号
}
查询构建器中的软删除
不要忘记,当你使用 Eloquent 时,软删除将排除条目,但如果使用查询构建器则不起作用。
// 排除软删除的数据
$users = User::all();
// 不排除软删除的数据
$users = User::withTrashed()->get();
// 不排除软删除的数据
$users = DB::table('users')->get();
原生 SQL 查询
如果需要执行一个简单的 SQL 查询,而不需要得到任何结果,比如更改数据库模式中的某些内容,可以直接执行 DB::statement()。
DB::statement('DROP TABLE users');
DB::statement('ALTER TABLE projects AUTO_INCREMENT=123');
使用数据库事务
如果你执行了两个 DB 操作,而第二个操作可能会出错,那么您应该回滚第一个操作,对吗?
为此,我建议使用数据库事务(DB Transactions),这在 Laravel 中非常简单:
DB::transaction(function () {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
});
更新或创建
如果您需要检查记录是否存在,然后更新它,或者在记录不存在时创建一个新的记录,您可以使用一句代码来实现 - 使用 Eloquent 方法updateOrCreate()
:
// 不要这样做
$flight = Flight::where('departure', 'Oakland')
->where('destination', 'San Diego')
->first();
if ($flight) {
$flight->update(['price' => 99, 'discounted' => 1]);
} else {
$flight = Flight::create([
'departure' => 'Oakland',
'destination' => 'San Diego',
'price' => 99,
'discounted' => 1
]);
}
// 可以这样做
$flight = Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
保存数据时清除缓存
如果你有这样的 posts
缓存键,并且你想在新的存储或更新时删除那个缓存键,你可以在你的模型上调用静态 saved
函数:
class Post extends Model
{
public static function boot()
{
parent::boot();
static::saved(function () {
// Post 保存后删除缓存
Cache::forget('posts');
});
}
}
将数组类型存储到 JSON 中
如果你有一个接受数组的输入字段,并且你必须将其存储为 JSON,则可以在模型中使用 $casts
属性。在这里,images
是一个JSON属性。
protected $casts = [
'images' => 'array',
];
因此,你可以将其存储为 JSON,但当从 DB 检索时,它可以用作数组。
复制模型
如果有两个非常相似的 Models(如送货地址和账单地址),需要将其中一个复制到另一个,可以使用 replicate() 方法,然后更改一些属性。
官方文档中的示例
$shipping = Address::create([
'type' => 'shipping',
'line_1' => '123 Example Street',
'city' => 'Victorville',
'state' => 'CA',
'postcode' => '90001',
]);
$billing = $shipping->replicate()->fill([
'type' => 'billing'
]);
$billing->save();
减少内存
有时我们需要将大量数据加载到内存中。例如:
// 此时结果是模型集合
// Illuminate\Database\Eloquent\Collection
$users = User::all();
但如果数据量非常大,这种方法可能会比较慢,因为 Laravel 会准备模型类的对象。在这种情况下,Laravel 提供了一个方便的函数 toBase()
// 此时结果是集合
// Illuminate\Support\Collection
$users = User::query()->toBase()->get();
调用该方法后,将从数据库中获取数据,但不会准备模型类。请记住,向 get 方法传递一个字段数组通常是个好主意,这样可以避免从数据库中获取所有字段。
不考虑 $fillable/$guarded 进行强制查询
如果你创建一个 Laravel 的样板作为其他开发者的“启动器”,并且你无法控制他们后续填充 Model 的 $fillable/$guarded,你可以使用 forceFill() 方法。
$team->update(['name' => $request->name]
如果 name
不在 Team 模型的 $fillable
中怎么办?或者如果没有 $fillable/$guarded 呢?
$team->forceFill(['name' => $request->name])
这将“忽略”该查询, $fillable
并且无论如何都会执行。
多层级父子查询
如果你具有父子结构的 3 级结构(例如电子商店中的类别),并且想要在第三级显示产品数量,则可以使用 with('yyy.yyy')
,然后添加 withCount()
为条件
class HomeController extend Controller
{
public function index()
{
$categories = Category::query()
->whereNull('category_id')
->with(['subcategories.subcategories' => function($query) {
$query->withCount('products');
}])->get();
}
}
class Category extends Model
{
public function subcategories()
{
return $this->hasMany(Category::class);
}
public function products()
{
return $this->hasMany(Product::class);
}
}
<ul>
@foreach($categories as $category)
<li>
{{ $category->name }}
@if ($category->subcategories)
<ul>
@foreach($category->subcategories as $subcategory)
<li>
{{ $subcategory->name }}
@if ($subcategory->subcategories)
<ul>
@foreach ($subcategory->subcategories as $subcategory)
<li>{{ $subcategory->name }} ({{ $subcategory->product_count }})</li>
@endforeach
</ul>
@endif
</li>
@endforeach
</ul>
@endif
</li>
@endforeach
</ul>
失败时执行任何操作
在查找记录时,如果找不到记录,您可能希望执行一些操作。除了 ->firstOrFail()
只抛出 404 之外,您可以在失败时执行任何操作,只需执行 ->firstOr(function() { ...})
$user = User::query()->where('id', 1100)->firstOr(function () {
// 处理
return '没有这条数据';
});
检查记录是否存在或显示 404
不要使用 find()
方法,然后检查记录是否存在。请使用 findOrFail()
方法。
$user = User::query()->find(1);
if (! $user) {
abort(404);
}
语法糖
$product = Product::findOrFail($id);
abort_if ($product->user_id != auth()->user()->id, 403)
将数据持久保存到数据库时自动填充列
如果要在将数据保存到数据库时自动填充列(例如:slug),请使用模型观察器,而不是每次都对其进行硬编码。
use Illuminate\Support\Str;
class Article extends Model
{
...
protected static function boot()
{
parent:boot();
static::saving(function ($model) {
$model->slug = Str::slug($model->title);
});
}
}
有关查询的额外信息
可以在查询上调用该 explain()
方法,以了解有关查询的额外信息。
User::query()->find(1)->explain()->dd();
结果
array:1 [▼ // app/Http/Controllers/UserController.php:99
0 => {#1404 ▼
+"id": 1
+"select_type": "SIMPLE"
+"table": "users"
+"partitions": null
+"type": "ALL"
+"possible_keys": null
+"key": null
+"key_len": null
+"ref": null
+"rows": 102
+"filtered": 100.0
+"Extra": null
}
]
在 Laravel 中使用 doesntExist() 方法
// 这样
if ( 0 === $model->where('status', 'pending')->count() ) {
}
// 但是我并不关心数量,只关心是否存在
// Laravel 的 exists() 方法更简洁
if ( ! $model->where('status', 'pending')->exists() ) {
}
// 但我发现,上面这句话中的 "!" 很容易被忽略
// doesntExist() 方法使语句更加清晰
if ( $model->where('status', 'pending')->doesntExist() ) {
}
要添加到几个模型中的特性,以便自动调用它们的boot()
方法。
如果您想将一个 Trait 添加到几个模型中,以自动调用它们的 boot() 方法,您可以以 boot[TraitName] 的形式调用 Trait 的方法。
class Transaction extends Model
{
use MultiTenantModelTrait;
}
trait MultiTenantModelTrait
{
// 方法的名称是 boot[TraitName]
// 它将被自动调用为 Transaction/Task 的 boot()函数。
public static function bootMultiTenantModelTrait()
{
static::creating(function ($model) {
if (!$isAdmin) {
$isAdmin->created_by_id = auth()->id();
}
})
}
}
在 Laravel 中有两种常见的方法来确定表是否为空
在 Laravel 中,有两种常见的方法来确定一个表是否为空。直接在模型上调用 exists()
或 count()
。
一个返回一个严格的 真/假
布尔值,另一个返回一个整数,你可以在条件中使用它作为一个假值。
public function index()
{
if (\App\Models\User::exists()) {
// 如果表中有已保存的记录,则返回布尔值 true 或 false
}
if (\App\Models\User::count()) {
// 返回表中的行数
}
}
如何防止“非对象属性”错误
// 属于默认模型
// 假设你有一个 Post 属于 Author,然后 Blade 代码为:
$post->author->name;
// 当然,你可以这样防止它:
$post->author->name ?? ''
// 或
@$post->author->name
// 但是你可以在Eloquent关系级别上执行此操作:
// 如果没有附加到帖子的作者,则此关系将返回一个空的 App\Author 模型。
public function author() {
return $this->belongsTo(Author::class)->withDefault();
}
// 或
public function author() {
return $this->belongsTo(Author::class)->withDefault([
'name' => 'Guest Author'
]);
}
更改 Eloquent 记录后获取原始属性
在修改 Eloquent 记录后获取原始属性,你可以通过调用 getOriginal()
来获取原始属性。
$user = App\User::first();
$user->name; // John
$user->name = "Peter"; // Peter
$user->getOriginal('name'); // John
$user->getOriginal(); // 原始记录
一种简单的填充数据库的方法。
使用 .sql
转储文件在 Laravel 中填充数据库的一种简单方法。
DB::unprepared(
file_get_contents(__DIR__ . './dump.sql')
);
查询构造函数的 crossJoinSub 方法
使用 CROSS JOIN 子查询
use Illuminate\Support\Facades\DB;
$totalQuery = DB::table('orders')->selectRaw('SUM(price) as total');
DB::table('orders')
->select('*')
->crossJoinSub($totalQuery, 'overall')
->selectRaw('(price / overall.total) * 100 AS percent_of_total')
->get();
Belongs to Many Pivot table naming
为了确定关系中间表的表名,Eloquent 将按字母顺序连接两个相关的模型名。
这意味着 Post 和 Tag 之间的连接可以像这样添加:
class Post extends Model
{
public $table = 'posts';
public function tags()
{
return $this->belongsToMany(Tag::class);
}
}
但是,你可以自由地覆盖此约定,并且需要在第二个参数中指定连接表。
class Post extends Model
{
public $table = 'posts';
public function tags()
{
return $this->belongsToMany(Tag::class, 'posts_tags');
}
}
如果你想明确主键,你也可以提供这些作为第三和第四个参数。
class Post extends Model
{
public $table = 'posts';
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id');
}
}
按中间表字段排序
BelongsToMany::orderByPivot()
允许你直接对 BelongsToMany
关系查询的结果进行排序。
class Tag extends Model
{
public $table = 'tags';
}
class Post extends Model
{
public $table = 'posts';
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id')
->using(PostTagPivot::class)
->withTimestamps()
->withPivot('flag');
}
}
class PostTagPivot extends Pivot
{
protected $table = 'post_tag';
}
// 控制器
public function getPostTags($id)
{
return Post::findOrFail($id)->tags()->orderByPivot('flag', 'desc')->get();
}
从数据库中查找单个记录
sole()
方法只返回一条符合条件的记录。如果没有找到这样的条目,则会抛出 NoRecordsFoundException
。如果找到多个记录,则会抛出MultipleRecordsFoundException
。
$user = User::query()->where('id', 10000)->sole();
自动记录分块
类似于 each()
方法,但更容易使用。自动将结果拆分为部分(块)。
return User::orderBy('name')->chunkMap(fn ($user) => [
'id' => $user->id,
'name' => $user->name,
]), 25);
更新模型时不调度事件
有时你需要更新模型而不发送任何事件。我们现在可以使用 updateQuietly()
方法来实现这一点,该方法在后台使用 saveQuietly()
方法。
$flight->updateQuietly(['departed' => false]);
定期从模型中清理过期的数据
定期从模型中清理过期的数据。
使用此特性,Laravel 将自动完成此操作,您只需要在Kernel类中调整 model:prune
命令的频率。
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class Flight extends Model
{
use Prunable;
/**
* Get the prunable model query.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function prunable()
{
return static::where('created_at', '<=', now()->subMonth());
}
}
此外,在这个方法中,你可以设置在删除模型之前必须执行的操作:
protected function pruning()
{
// 删除附加资源、
// 与模型相关联。例如,文件。
Storage::disk('s3')->delete($this->filename);
}
不可变日期和向它们的转换
Laravel 8.53引入了 immutable_date
和 immutable_datetime
类型转换器,将日期转换为不可变对象。
而不是常规的 Carbon 实例,将其转换为 CarbonImmutable。
class User extends Model
{
public $casts = [
'date_field' => 'immutable_date',
'datetime_field' => 'immutable_datetime',
];
}
findOrFail 方法也接受一个 id 列表
findOrFail 方法还接受一个 id 列表。如果找不到其中任何一个 id,则该方法 "失败"。
如果你需要检索一组特定的模型,又不想检查你得到的计数是否是你期望的计数,那么这个方法就很不错
User::create(['id' => 1]);
User::create(['id' => 2]);
User::create(['id' => 3]);
// 检索用户...
$user = User::findOrFail(1);
// 因为用户不存在,所以抛出 404...
User::findOrFail(99);
// 检索所有 3 个用户...
$users = User::findOrFail([1, 2, 3]);
// 由于无法找到*所有*用户而抛出
User::findOrFail([1, 2, 3, 99]);
Prunable trait 可以自动从数据库中删除模型
Laravel 8.50 中的新功能:您可以使用 Prunable trait 自动从数据库中删除模型。例如,在几天后永久删除软删除的模型。
class File extends Model
{
use SoftDeletes;
// 引入 Prunable trait
use Prunable;
public function prunable()
{
// 匹配此查询的文件将被删除
return static::query()->where('deleted_at', '<=', now()->subDays(14));
}
protected function pruning()
{
// 在删除模型之前,请从 S3 中删除该文件。
Storage::disk('s3')->delete($this->filename);
}
}
// 将 PruneCommand 添加到胸的计划任务中(app/Console/Kernel.php)。
$schedule->command(PruneCommand::class)->daily();
withAggregate 方法
在 Eloquent 中,withAvg、withCount、withSum 等方法实际上是使用了 withAggregate
方法。你可以使用这个方法来根据关系添加一个子查询。
// Eloquent Model
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
// 而不是急切地加载所有用户...
$posts = Post::with('user')->get();
// 你可以添加一个子查询,只检索用户的姓名...
$posts = Post::withAggregate('user', 'name')->get();
// 这将向 Post 实例添加一个'user_name'属性:
$posts->first()->user_name;
日期约定
在 Laravel 模型中使用 something_at 约定,而不是仅仅使用布尔值,可以让您了解标志何时更改——例如产品上线时。
// 迁移文件
Schema::table('products', function (Blueprint $table) {
$table->datetime('live_at')->nullable();
});
// 模型
public function live()
{
return !is_null($this->live_at);
}
// 模型属性
protected $dates = [
'live_at'
];
Eloquent multiple upserts
upsert()方法将插入或更新多个记录。
- 第一个数组:要插入或更新的值
- 第二:SELECT语句中使用的唯一标识符列
- 第三:如果记录存在,则要更新的列
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150],
], ['departure', 'destination'], ['price']);
在过滤结果后检索查询构建器
在过滤结果后检索查询构建器:你可以使用 ->toQuery()
。
该方法内部使用集合中的第一个模型和一个 whereKey
比较。
// 检索所有登录用户
$loggedInUsers = User::where('logged_in', true)->get();
// 使用集合方法或 php 筛选
$nthUsers = $loggedInUsers->nth(3);
// 你不能在集合中这样操作
$nthUsers->update(/* ... */);
// 但您可以使用 ->toQuery() 来检索生成器
if ($nthUsers->isNotEmpty()) {
$nthUsers->toQuery()->update(/* ... */);
}
Custom casts
您可以创建自定义类型转换,让 Laravel 自动格式化 Eloquent 模型数据。
下面是一个在检索或更改用户名时将其大写的示例。
class CapitalizeWordsCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return ucwords($value);
}
public function set($model, string $key, $value, array $attributes)
{
return ucwords($value);
}
}
class User extends Model
{
protected $casts = [
'name' => CapitalizeWordsCast::class,
'email' => 'string',
];
}
根据相关模型的平均值或总数排序
您是否需要根据相关模型的平均值或总数进行订购?
在 Eloquent 中这很容易!
public function bestBooks()
{
Book::query()
->withAvg('ratings as average_rating', 'rating')
->orderByDesc('average_rating');
}
返回事务处理结果
如果您有一个 DB 事务并希望返回其结果,至少有两种方法,请参见示例
// 1. 您可以通过引用传递参数
$invoice = NULL;
DB::transaction(function () use (&$invoice) {
$invoice = Invoice::create(...);
$invoice->items()->attach(...);
})
// 2. 或更短:只返回 Trasaction 结果
$invoice = DB::transaction(function () {
$invoice = Invoice::create(...);
$invoice->items()->attach(...);
return $invoice;
});
从查询中删除多个全局作用域
当使用 Eloquent 全局作用域时,您不仅可以使用 MULTIPLE 作用域,还可以在不需要某些作用域时删除它们,方法是将数组提供给withoutGlobalScopes()
// 删除所有全局作用域...
User::withoutGlobalScopes()->get();
// 删除一些全局作用域...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
Order JSON 列属性
使用 Eloquent,你可以按 JSON 列属性对结果进行排序
// JSON column example:
// bikes.settings = {"is_retired": false}
$bikes = Bike::where('athlete_id', $this->athleteId)
->orderBy('name')
->orderByDesc('settings->is_retired')
->get();
从第一个结果中获取单列值
可以使用 value()
方法从查询的第一个结果中获取单个列的值
// 这样
Integration::where('name', 'foo')->first()->active;
// 你可以使用
Integration::where('name', 'foo')->value('active');
// 如果没有找到记录,则抛出异常
Integration::where('name', 'foo')->valueOrFail('active')';
检查更改值是否改变了键
你想知道你是否已经修改了模型的某个键的值,那么 originalIsEquivalent
就派上用场了。
$user = User::first(); // ['name' => "John"]
$user->name = 'John';
$user->originalIsEquivalent('name'); // 返回 true
$user->name = 'David'; // 直接设置
$user->fill(['name' => 'David']); // 或者通过 fill 设置
$user->originalIsEquivalent('name'); // 返回 false
定义访问器和转换器的新方法
在 Laravel 8.77 中,定义属性访问器和修改器的新方法如下:
// 之前的方法,两步法
public function setTitleAttribute($value)
{
$this->attributes['title'] = strtolower($value);
}
public function getTitleAttribute($value)
{
return strtoupper($value);
}
// 新的方法
protected function title(): Attribute
{
return new Attribute(
get: fn ($value) => strtoupper($value),
set: fn ($value) => strtolower($value),
);
}
另一种实现访问器和转换器的方法
如果你在很多模型中都需要使用相同的访问器和修改器,你可以使用自定义类型转换。
只需创建一个实现 CastsAttributes 接口的类。该类应具有两个方法,第一个方法是 get,用于指定如何从数据库中检索模型,第二个方法是 set,用于指定值将如何存储在数据库中。
<?php
namespace App\Casts;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class TimestampsCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return Carbon::parse($value)->diffForHumans();
}
public function set($model, string $key, $value, array $attributes)
{
return Carbon::parse($value)->format('Y-m-d h:i:s');
}
}
然后你可以在模型类中实现类型转换。
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Casts\TimestampsCast;
use Carbon\Carbon;
class User extends Authenticatable
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'updated_at' => TimestampsCast::class,
'created_at' => TimestampsCast::class,
];
}
在搜索第一条记录时,可以执行一些操作
当查找第一条记录时,如果找不到,你希望执行一些操作。firstOrFail() 会抛出一个 404 异常。
你可以使用 firstOr(function() {})
代替。Laravel 已经为你处理了这个问题。
$book = Book::whereCount('authors')
->orderBy('authors_count', 'DESC')
->having('modules_count', '>', 10)
->firstOr(function() {
// The Sky is the Limit ...
// 在这里执行任何操作
});
直接将 created_at 时间转换为人类可读格式
你知道可以使用 diffForHumans() 函数直接将 created_at 日期转换为人类可读的格式,如你知道可以使用 diffForHumans() 函数直接将 created_at 日期转换为人类可读的格式,如 1 分钟前、1 个月前等。Laravel eloquent 默认在 created_at 字段上启用 Carbon 实例。
$post = Post::whereId($id)->first();
$result = $post->created_at->diffForHumans();
/* 输出 */
// 根据创建时间,输出1分钟前、2周前等。
使用 Eloquent 访问器进行排序
使用 Eloquent 访问器进行排序!是的,这是可行的。我们不是在数据库级别上通过访问器进行排序,而是在返回的集合上通过访问器进行排序。
class User extends Model
{
// ...
protected $appends = ['full_name'];
// 自Laravel 9起
protected function full_name(): Attribute
{
return Attribute::make(
get: fn ($value, $attributes) => $attributes['first_name'] . ' ' . $attributes['last_name'],),
);
}
// Laravel 8及更低版本
public function getFullNameAttribute()
{
return $this->attribute['first_name'] . ' ' . $this->attributes['last_name'];
}
// ..
}
class UserController extends Controller
{
// ..
public function index()
{
$users = User::all();
// 按照 full_name 降序
$users->sortByDesc('full_name');
// or
// 按照 full_name 升序
$users->sortBy('full_name');
// ..
}
// ..
}
sortByDesc 和 sortBy 是集合上的方法
检查是否已创建或找到特定模型
$user = User::create([
'name' => 'Oussama',
]);
// 返回布尔值
return $user->wasRecentlyCreated;
// 最近创建为 true
// false 表示已找到(已存在于您的数据库中)
Laravel Scout 数据库驱动
使用 laravel v9,您可以使用带有数据库驱动程序的 Laravel Scout (搜索)。不再有喜欢的地方!
$companies = Company::search(request()->get('search'))->paginate(15);
在查询生成器上使用 value 方法
在查询构建器上使用value方法,当你只需要检索一列时,可以执行更高效的查询。
// 之前(获取行上的所有列)
Statistic::where('user_id', 4)->first()->post_count;
// 之后(只获取`post_count`)
Statistic::where('user_id', 4)->value('post_count');
将数组传递给 where 方法
Laravel 中,你可以将数组传递给 where 方法。
// 而不是这样
JobPost::where('company', 'laravel')
->where('job_type', 'full time')
->get();
// 你可以将数组传递
JobPost::where(['company' => 'laravel',
'job_type' => 'full time'])
->get();
从模型集合返回主键
你知道 modelsKeys()
的集合方法吗?它从模型集合中返回主键。
$users = User::active()->limit(3)->get();
$users->modelsKeys(); // [1, 2, 3]
强制 Laravel 使用预先加载
如果你想防止你的应用中出现延迟加载,你只需要在 boot()
你的 AppServiceProvider
Model::preventLazyLoading();
但是,如果您只想在本地开发中启用此功能,则可以更改上面的代码:
Model::preventLazyLoading(!app()->isProduction());
让你的所有模型都可以批量赋值。
出于安全原因,不建议使用这种方法,但这是可能的。
当你想这样做时,你不需要为每个模型设置一个空的$guarded数组,如下所示:
protected $guarded = [];
你可以在一个地方完成这个操作,只需在 AppServiceProvider
的 boot()
方法中添加以下行:
Model::unguard();
现在,你的所有模型都可以进行批量赋值。
在 select all 语句中隐藏列
如果你使用的是 Laravel v8.78 和 MySQL 8.0.23 及更高版本,你可以将选定的列定义为 "不可见"。定义为不可见的列将从 select *
语句中隐藏。
但是,要实现这一点,我们必须在迁移中使用 invisible()
方法,如下所示:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('table', function (Blueprint $table) {
$table->string('secret')->nullable()->invisible();
});
就是这样!这将使选定的列从select *语句中隐藏。
JSON Where 子句
Laravel 提供了查询支持 JSON 列的辅助函数。
目前,MySQL 5.7+、PostgreSQL、SQL Server 2016 和 SQLite 3.9.0(使用 JSON1 扩展)
// 要查询一个json列,可以使用->操作符
$users = User::query()
->where('preferences->dining->meal', 'salad')
->get();
// 可以检查一个JSON数组是否包含一组值
$users = User::query()
->whereJsonContains('options->languages', [
'en', 'de'
])
->get();
// 还可以通过JSON数组的长度进行查询
$users = User::query()
->whereJsonLength('options->languages', '>', 1)
->get();
获取表的所有列名
DB::getSchemaBuilder()->getColumnListing('users');
/*
returns [
'id',
'name',
'email',
'email_verified_at',
'password',
'remember_token',
'created_at',
'updated_at',
];
*/
比较两列的值
可以使用 whereColumn
方法比较两列的值。
return Task::whereColumn('created_at', 'updated_at')->get();
// 传递比较运算符
return Task::whereColumn('created_at', '>', 'updated_at')->get();
访问器缓存
从 Laravel 9.6 开始,如果您有计算密集型访问器,则可以使用 shouldCache 方法。
public function hash(): Attribute
{
return Attribute::make(
get: fn($value) => bcrypt(gzuncompress($value)),
)->shouldCache();
}
New scalar() method
在Laravel 9.8.0中,添加了 scalar() 方法,允许你从查询结果中检索第一行的第一列。
// 之前
DB::selectOne("SELECT COUNT(CASE WHEN food = 'burger' THEN 1 END) AS burgers FROM menu_items;")->burgers
// 现在
DB::scalar("SELECT COUNT(CASE WHEN food = 'burger' THEN 1 END) FROM menu_items;")
选择指定列
要选择模型上的特定列,可以使用 select 方法,或者直接将数组传递给 get 方法!
// 从所有员工中选择指定的列
$employees = Employee::select(['name', 'title', 'email'])->get();
// 从所有员工中选择指定的列
$employees = Employee::get(['name', 'title', 'email']);
将条件子句链接到查询,而不编写 if-else 语句
"when" 是查询构建器中的一个帮助函数,它可以帮助你在不编写 if-else 语句的情况下链接条件子句。
这使得你的查询非常清晰。
class RatingSorter extends Sorter
{
function execute(Builder $query)
{
$query
->selectRaw('AVG(product_ratings.rating) AS avg_rating')
->join('product_ratings', 'products.id', '=', 'product_ratings.product_id')
->groupBy('products.id')
->when(
$this->direction === SortDirections::Desc,
fn () => $query->orderByDesc('avg_rating')
fn () => $query->orderBy('avg_rating'),
);
return $query;
}
}
重写模型中的连接属性
覆盖 Laravel 中单个模型的数据库连接属性可以是一个非常强大的技巧。以下是一些你可能会发现特别方便的使用场景:
- 多个数据库连接
如果你的应用程序使用多个数据库连接(例如,MySQL、PostgreSQL 或相同数据库的不同实例),你可能需要指定哪个连接应该用于特定的模型。通过覆盖 $connection 属性,你可以轻松管理这些连接并确保你的模型与适当的数据库进行交互。
- 数据分片
在将数据分布在多个数据库的情况下,你可能会有不同的模型映射到不同的分片上。在每个模型中覆盖连接属性允许你定义应该使用哪个分片,而不影响其他模型或默认连接。
- 第三方集成
当集成提供自己数据库的第三方服务时,你可能需要为表示该服务数据的模型使用特定的连接。在那个模型中覆盖连接属性将确保它连接到正确的数据库,同时保持应用程序的默认设置不变。
- 多租户应用程序
在多租户应用程序中,你可能会为每个租户分别拥有独立的数据库。通过动态地在模型中覆盖 $connection 属性,你可以根据当前用户轻松切换租户数据库,确保数据隔离和资源管理的合理性。
要覆盖模型中的连接属性,请在类中定义 $connection 属性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CustomModel extends Model
{
protected $connection = 'your_custom_connection';
}
在 Where 子句中使用列名(动态 Where 子句)
可以在 where 子句中使用列名来创建动态 where 子句。在下面的例子中,我们使用 whereName('John')
而不是 where('name','John')
。
<?php
namespace App\Http\Controllers;
use App\Models\User;
class UserController extends Controller
{
public function example()
{
return User::whereName('John')->get();
}
}
使用 firstOrCreate
您可以使用 firstOrCreate()
来查找与属性匹配的第一条记录,或者在它不存在时创建它。
示例场景
假设您正在导入一个 CSV 文件,如果类别不存在,您希望创建一个类别。
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
public function example(Request $request)
{
// instead of
$category = Category::where('name', $request->name)->first();
if (!$category) {
$category = Category::create([
'name' => $request->name,
'slug' => Str::slug($request->name),
]);
}
// you can use
$category = Category::firstOrCreate([
'name' => $request->name,
], [
'slug' => Str::slug($request->name),
]);
return $category;
}
}