Laravel 9多对多关联中间表查询结果与预期不符原因排查
Laravel 9 多对多关联查询结果与中间表数据不符的排查方案
问题背景
在Laravel 9项目中,已创建articles与votes表的多对多关联,中间表为article_vote,相关迁移、模型关联代码及查询逻辑如下:
迁移代码
public function up() { Schema::create('article_vote', function (Blueprint $table) { $table->id(); $table->foreignId('article_id')->references('id')->on('articles')->onUpdate('RESTRICT')->onDelete('CASCADE'); $table->foreignId('vote_id')->references('id')->on('votes')->onUpdate('RESTRICT')->onDelete('CASCADE'); $table->boolean('active')->default(false); $table->date('expired_at')->nullable(); $table->integer('supervisor_id')->nullable()->unsigned(); $table->foreign('supervisor_id')->references('id')->on('users')->onDelete('CASCADE'); $table->mediumText('supervisor_notes')->nullable(); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->nullable(); $table->unique(['vote_id', 'article_id'], 'article_vote_vote_id_article_id_index'); $table->index(['vote_id', 'article_id', 'active', 'expired_at'], 'article_vote_vote_id_article_id_active_expired_at_index'); $table->index([ 'expired_at', 'active',], 'article_vote_expired_at_active_index'); $table->index(['created_at'], 'article_vote_created_at_index'); }); Artisan::call('db:seed', array('--class' => 'articleVotesWithInitData')); }
Vote模型关联代码
public function articles(): BelongsToMany { return $this->belongsToMany(Article::class, 'article_vote', 'vote_id') ->withTimestamps() ->withPivot(['active', 'expired_at', 'supervisor_id', 'supervisor_notes']); }
Article模型关联代码
public function votes(): BelongsToMany { return $this->belongsToMany(Vote::class, 'article_vote', 'article_id') ->withTimestamps() ->withPivot(['active', 'expired_at', 'supervisor_id', 'supervisor_notes']); }
查询代码及生成的SQL
查询代码:
$article = Article::getById(2) ->firstOrFail(); $articleVotes = $article->votes;
生成的SQL:
SELECT `votes`.*, `article_vote`.`article_id` AS `pivot_article_id`, `article_vote`.`vote_id` AS `pivot_vote_id`, `article_vote`.`created_at` AS `pivot_created_at`, `article_vote`.`updated_at` AS `pivot_updated_at`, `article_vote`.`active` AS `pivot_active`, `article_vote`.`expired_at` AS `pivot_expired_at`, `article_vote`.`supervisor_id` AS `pivot_supervisor_id`, `article_vote`.`supervisor_notes` AS `pivot_supervisor_notes` FROM `votes` INNER JOIN `article_vote` on `votes`.`id` = `article_vote`.`vote_id` WHERE `article_vote`.`article_id` = 2
但查询返回的4条vote_id结果与article_vote表中的实际数据不符,可从以下方向排查:
排查方向
1. 缓存干扰
- 执行以下命令清除Laravel缓存,避免旧数据残留:
php artisan cache:clear php artisan config:clear php artisan route:clear php artisan view:clear - 检查
Article或Vote模型是否使用了缓存相关的Trait(如第三方缓存扩展),若有,临时禁用后重新查询。
2. 直接验证SQL结果
- 将生成的SQL语句直接在数据库客户端(如MySQL Workbench、phpMyAdmin)中执行,对比返回结果与
article_vote表中article_id=2的记录:- 如果SQL返回结果与数据库中
article_vote数据一致:说明问题出在代码层面,比如模型的属性隐藏、访问器修改了数据,或后续对集合进行了过滤操作。 - 如果SQL返回结果与
article_vote数据不符:检查是否有未提交的事务,或其他进程/代码修改了article_vote表的数据。
- 如果SQL返回结果与数据库中
3. 模型全局作用域限制
- 检查
Vote模型是否存在全局作用域,比如默认过滤active=true或未过期的投票,导致关联查询结果被过滤。查看模型的booted方法:protected static function booted() { static::addGlobalScope('active', function (Builder $builder) { $builder->where('active', true); }); } - 临时禁用全局作用域测试:
$articleVotes = $article->votes()->withoutGlobalScopes()->get();
4. 模型主键与关联字段匹配问题
- 确认
Vote和Article模型的主键是否为id(Laravel默认主键),若自定义了主键,需在模型中指定:protected $primaryKey = 'custom_id'; - 检查中间表外键是否与对应表主键匹配,确保
article_vote.vote_id关联的是votes.id,article_vote.article_id关联的是articles.id。
5. 数据填充或修改逻辑问题
- 检查种子文件
articleVotesWithInitData的填充逻辑是否正确,是否插入了预期的article_id与vote_id关联数据。 - 查看项目中是否有其他代码(如定时任务、批量操作)修改了
article_vote表的数据,可通过数据库操作日志(若开启)追溯数据变更记录。
6. 集合后续处理干扰
- 直接打印查询结果的原始数组,排查是否后续代码对
$articleVotes集合进行了过滤、映射等操作:dd($articleVotes->toArray());
内容的提问来源于stack exchange,提问作者mstdmstd
相关产品推荐
相关产品推荐

