Laravel 编码技巧 - DB 查询

作者: 温新

图书: 【Laravel 编码技巧】

阅读: 470

时间: 2024-07-27 10:33:56

重要或克隆 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() 查询今日的记录,可以直接使用 Carbonnow() 方法,它会自动转换为日期进行查询,而不需要指定 ->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_dateimmutable_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 = [];

你可以在一个地方完成这个操作,只需在 AppServiceProviderboot() 方法中添加以下行:

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 中单个模型的数据库连接属性可以是一个非常强大的技巧。以下是一些你可能会发现特别方便的使用场景:

  1. 多个数据库连接

如果你的应用程序使用多个数据库连接(例如,MySQL、PostgreSQL 或相同数据库的不同实例),你可能需要指定哪个连接应该用于特定的模型。通过覆盖 $connection 属性,你可以轻松管理这些连接并确保你的模型与适当的数据库进行交互。

  1. 数据分片

在将数据分布在多个数据库的情况下,你可能会有不同的模型映射到不同的分片上。在每个模型中覆盖连接属性允许你定义应该使用哪个分片,而不影响其他模型或默认连接。

  1. 第三方集成

当集成提供自己数据库的第三方服务时,你可能需要为表示该服务数据的模型使用特定的连接。在那个模型中覆盖连接属性将确保它连接到正确的数据库,同时保持应用程序的默认设置不变。

  1. 多租户应用程序

在多租户应用程序中,你可能会为每个租户分别拥有独立的数据库。通过动态地在模型中覆盖 $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;
    }
}
请登录后再评论