
在使用 Laravel Eloquent ORM 定义 hasMany 关系时,有时会遇到一个令人困惑的现象:即使已经通过 with() 方法进行了预加载(Eager Loading),尝试通过属性(例如 $city-youjiankuohaophpcncitizens)访问关联数据时,结果却为空。然而,如果通过方法(例如 $city->citizens()->get())调用关系并执行查询,却能正常获取到数据。
例如,假设我们有两个模型:City(城市)和 Citizen(公民),一个城市可以有多个公民。在 City 模型中,我们定义了 citizens 关系:
// City.php
class City extends Model
{
// ... 其他属性和方法 ...
public function citizens()
{
return $this->hasMany(Citizen::class, 'city_id', 'id');
}
}在尝试获取城市及其公民时,我们可能会这样写:
$cities = City::with('citizens')->get();
foreach ($cities as $city) {
// 预期能获取到公民,但实际可能为空
$citizens = $city->citizens; // 此时 $citizens 可能是空的
// dd($city->citizens->count()); // => 0
}而如果通过方法调用,却能正常工作:
$cities = City::all(); // 注意这里没有 with('citizens')
foreach ($cities as $city) {
// 每次迭代都会执行新的数据库查询
$citizens = $city->citizens()->get(); // 此时 $citizens 包含数据
// dd($city->citizens()->count()); // => 5
}这种行为尤其令人费解,因为 with('citizens') 的目的正是为了预加载数据,使其可以通过属性直接访问,从而避免 N+1 查询问题。
为了更好地理解问题,我们来看一下原始的 City 和 Citizen 模型定义:
// City.php
class City extends Model
{
use Searchable;
protected $table = 'cities';
public $incrementing = false;
protected $perPage = 20;
protected $fillable = [
'name',
'unique_code',
'extra_attributes'
];
protected $casts = [
'id' => 'string',
'codes' => 'array',
'extra_attributes' => SchemalessAttributes::class,
];
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$model->id = $model->id ?: Str::orderedUuid();
});
}
public function toSearchableArray(): array
{
return [
'name' => $this->name,
];
}
public function citizens()
{
return $this->hasMany(Citizen::class, 'city_id', 'id');
}
}
// Citizen.php
class Citizen extends Model
{
public $incrementing = false;
protected $perPage = 20;
protected $table = "citizens";
protected $fillable = [
'user_id',
'level_id',
'city_id',
];
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$model->id = $model->id ?: Str::orderedUuid();
});
}
public function user() {
return $this->hasOne(User::class, 'id', 'user_id')->withTrashed();
}
public function city() {
// !!! 问题所在:此处定义为 hasOne
return $this->hasOne(City::class, 'id', 'city_id');
}
}仔细观察 Citizen 模型中的 city() 方法定义,它被定义为 hasOne(City::class, 'id', 'city_id')。
问题症结在于 Citizen 模型中 city() 关系的错误定义。
在 Laravel 中:
hasOne 用于表示“我拥有一个关联模型,这个关联模型的外键指向我”。例如,User 模型中定义 hasOne(Phone::class),表示 User 拥有一个 Phone,而 Phone 模型中会有一个 user_id 字段指向 User 的主键。
belongsTo 用于表示“我属于一个父模型,我的外键指向那个父模型”。例如,Citizen 模型中定义 belongsTo(City::class),表示 Citizen 属于一个 City,而 Citizen 模型中会有一个 city_id 字段指向 City 的主键。
在我们的例子中,Citizen 模型拥有 city_id 字段,这个字段是 City 模型的主键。这意味着 Citizen 应该“属于”一个 City,因此其反向关系应该是 belongsTo。
当使用 City::with('citizens')->get() 进行预加载时,Laravel 会执行两个查询:一个获取所有 City,另一个获取所有与这些 City 相关的 Citizen。然后,Laravel 会尝试将这些预加载的 Citizen 模型实例正确地“挂载”到它们所属的 City 模型实例的 citizens 属性上。这个挂载过程依赖于模型之间定义的正确关系。
如果 Citizen 模型中的 city() 关系被错误地定义为 hasOne,Laravel 在尝试匹配和分配预加载数据时可能会遇到内部逻辑上的不一致。尽管数据已经被查询出来,但由于反向关系的定义错误,Eloqunet 无法正确地将这些数据关联到父模型的属性上,导致 $city->citizens 属性看起来是空的。而 $city->citizens() 方法则会创建一个新的查询构建器,绕过了预加载的数据,直接根据当前 City 实例的 ID 重新查询数据库,因此能够获取到正确的结果。
解决这个问题非常简单,只需要将 Citizen 模型中 city() 方法的关系类型从 hasOne 修正为 belongsTo 即可。
// Citizen.php (修正后)
class Citizen extends Model
{
// ... 其他属性和方法 ...
public function city() {
// 修正为 belongsTo
return $this->belongsTo(City::class, 'city_id');
}
}在 belongsTo 方法中,第二个参数 'city_id' 是可选的,如果外键命名符合 Laravel 约定(即 relationship_name_id,在这里是 city_id),则可以省略。
修正后,当我们再次执行 City::with('citizens')->get() 并通过 $city->citizens 访问时,预加载的数据将能够正确地被访问到。
这个案例强调了在 Laravel 中正确定义 Eloquent 模型关系的重要性,尤其是在定义反向关系时。错误的定义不仅可能导致数据访问异常,还会影响预加载机制的有效性,从而可能引发不必要的数据库查询,降低应用性能。
关键点:
通过遵循这些最佳实践,可以确保 Eloquent 关系按预期工作,充分利用 Laravel 的预加载功能,提升应用的性能和开发效率。
以上就是解决 Laravel hasMany 关系属性访问失效问题的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号