Rails 4 Scope关联查询优化:实现或逻辑的多条件筛选
嘿,这个问题我之前做项目时也踩过坑!核心问题出在你当前用的joins方式上——当你同时调用disease_associated和drug_target两个scope时,ActiveRecord会把这两个关联查询合并成同一个Gene必须同时关联Drug和Disease,但你要的是Interaction的两个Gene中,一个关联Drug另一个关联Disease(或者单个Gene同时满足两者)的情况。
问题根源分析
当前的scope用joins(genes: :diseases)和joins(genes: :drugs),同时调用时生成的SQL会是类似这样的:
SELECT interactions.* FROM interactions INNER JOIN interactions_genes ON interactions.id = interactions_genes.interaction_id INNER JOIN genes ON interactions_genes.gene_id = genes.id INNER JOIN genes_diseases ON genes.id = genes_diseases.gene_id INNER JOIN genes_drugs ON genes.id = genes_drugs.gene_id
这个查询强制要求同一个Gene同时出现在genes_diseases和genes_drugs表中,自然过滤掉了分属不同Gene的符合条件的Interaction。
解决方案:用EXISTS子查询替代JOIN
我们需要重构scope,改用EXISTS子查询来独立检查Interaction的基因集合中是否存在满足条件的记录,这样两个条件可以分别作用于不同的Gene。
修改你的Interaction模型代码:
class Interaction < ActiveRecord::Base has_and_belongs_to_many :genes ... # 筛选存在关联疾病的基因的Interaction scope :disease_associated, -> { where( <<~SQL EXISTS ( SELECT 1 FROM interactions_genes ig JOIN genes_diseases gd ON ig.gene_id = gd.gene_id WHERE ig.interaction_id = interactions.id ) SQL ) } # 筛选存在关联药物的基因的Interaction scope :drug_target, -> { where( <<~SQL EXISTS ( SELECT 1 FROM interactions_genes ig JOIN genes_drugs gd ON ig.gene_id = gd.gene_id WHERE ig.interaction_id = interactions.id ) SQL ) } ... end
为什么这样有效?
当你同时调用两个scope时,生成的SQL会变成:
SELECT interactions.* FROM interactions WHERE EXISTS (...) -- 检查该Interaction是否有基因关联疾病 AND EXISTS (...) -- 检查该Interaction是否有基因关联药物
这两个条件是独立的:只要Interaction的基因集合里至少有一个关联疾病,并且至少有一个关联药物(不管这两个基因是不是同一个),都会被选中,完美匹配你的需求!
控制器代码保持不变
你的控制器逻辑不需要改动,依然可以保留原来的条件判断,只需要加上distinct避免重复记录:
class InteractionsController < ApplicationController ... @interactions = Interaction.all.distinct @interactions = @interactions.disease_associated() if params[:filter_disease].present? @interactions = @interactions.drug_target() if params[:filter_druggable].present? ... end
额外优化(可选)
如果你想让代码更符合Rails风格,也可以用ActiveRecord的关联查询来构建exists条件,避免直接写SQL:
scope :disease_associated, -> { where.exists( Gene.joins(:diseases).where('genes.id IN (SELECT gene_id FROM interactions_genes WHERE interaction_id = interactions.id)') ) } scope :drug_target, -> { where.exists( Gene.joins(:drugs).where('genes.id IN (SELECT gene_id FROM interactions_genes WHERE interaction_id = interactions.id)') ) }
效果和原生SQL版本完全一致,看你个人偏好选择就行。
内容的提问来源于stack exchange,提问作者MrGraeme

