use Illuminate\Database\Query\Builder;
Builder::macro('addSubSelect', function ($column, $query) {
if (is_null($this->columns)) {
$this->select($this->from.'.*');
return $this->selectSub($query->limit(1), $column);
关于 Macro 的简单使用,可以看 使用宏(Macro)来扩展 Laravel 的数据库请求构建器
原文作者针对这个 Macro 写了一个 Composer 包 reinink/advanced-eloquent
。
使用 Macro 后 我们可以把代码改成下面的样子:
$users = User::addSubSelect('last_login_at', Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
)->get();
使用 Scope
更近一步,我们可以使用 scope 来进一步优化代码
class User extends Model
public function scopeWithLastLoginDate($query)
$query->addSubSelect('last_login_at', Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
$users = User::withLastLoginDate()->get();
构建动态模型关系
现在到了最有趣的部分,我们已经能够使用子查询来获得上次登录时间,但是如果我们需要获得更多其他的信息呢?比如,我们想要获得上次登录时的IP地址。我们应当怎么做呢?
一个简单的方法是创建第二条子查询
$users = User::withLastLoginDate()->withLastLoginIpAddress()->get();
{{ $user->last_login_at->format('M j, Y \a\t g:i a') }} ({{ $user->last_login_ip_address }})
当然,这肯定是行得通的。但是如果我们能够获得 Login
实例就更好了。特别是模型有其他方法的情况,比如模型关联和访问器方法。
$users = User::withLastLogin()->get();
{{ $user->lastLogin->created_at->format('M j, Y \a\t g:i a') }} ({{ $user->lastLogin->ip_address }})
首先,我们定义一个 lastLogin
方法,返回模型关系。在之前的例子中我们介绍了在 users
表中加入 last_login_id
的方法。但是,这里我们使用子查询来进行构造。
class User extends Model
public function lastLogin()
return $this->belongsTo(Login::class);
public function scopeWithLastLogin($query)
$query->addSubSelect('last_login_id', Login::select('id')
->whereColumn('user_id', 'users.id')
->latest()
)->with('lastLogin');
$users = User::withLastLogin()->get();
<table>
<th>Name</th>
<th>Email</th>
<th>Last Login</th>
@foreach ($users as $user)
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
@if ($user->lastLogin)
{{ $user->lastLogin->created_at->format('M j, Y \a\t g:i a') }}
@else
Never
@endif
@endforeach
</table>
上面的代码执行后,只有两条查询语句,第一条的查询如下:
select
"users".*,
select "id" from "logins"
where "user_id" = "users"."id"
order by "created_at" desc
limit 1
) as "last_login_id"
from "users"
这里我们基本上实现了 last_login_id
字段的功能,但是并不会真正的创建这个字段。现在让我们看看第二条查询语句:
select * from "logins" where "logins"."id" in (1, 3, 5, 13, 20 ... 676, 686)
我们的子查询只会返回上次的登录信息。并且由于我们使用了标准的 Laravel 模型关联,我们能够继续使用 Login
模型中的相关方法。非常棒。
懒加载动态模型关系
值得注意的是我们无法向下面那样直接使用模型关联,这是因为我们的 scope 并没有默认加载。
$lastLogin = User::first()->lastLogin; // 会返回null
如果你想要懒加载功能,可以加入到全局的scope中:
class User extends Model
protected static function boot()
parent::boot();
static::addGlobalScope(function ($query) {
$query->withLastLogin();
上面的内容能否使用 HasOne 来替代?
你也许会好奇我们能否使用 HasOne 来进行处理? 就结论而言,是不行的。让我们看看是为什么。
public function lastLogin()
return $this->hasOne(Login::class)->latest();
首先上面的代码确实能够返回我们所需要的数据。但是如果我们查看查询语句的话就会发现问题。
select * from "logins" where "logins"."user_id" in (1, 2, 3...99, 100) order by "created_at" desc
同样会导致我们之前提到的问题。那么我们加上limit会怎样呢?
public function lastLogin()
return $this->hasOne(Login::class)->latest()->limit(1);
查询语句如下
select * from "logins" where "logins"."user_id" in (1, 2, 3...99, 100) order by "created_at" desc limit 1
这会导致只有最后登录的用户返回时间,其他所有用户返回null
。
本作品采用《CC 协议》,转载必须注明作者和本文链接