通过查询合并has_many和has_many

时间:2016-03-21 09:50:01

标签: ruby-on-rails ruby activerecord arel

这似乎是一个典型的问题,但很难搜索。

我想通过has_many选择用户拥有的项目,并通过has_many through选择与用户相关联的项目。

考虑以下模型:

class User < ActiveRecord::Base
  has_many :projects,
           inverse_of: :owner

  has_many :project_associations,
           class_name: 'ProjectUser',
           inverse_of: :user

  has_many :associated_projects,
           through: :project_associations,
           source: :project
end

class Project < ActiveRecord::Base
  belongs_to :owner,
             class_name: 'User',
             foreign_key: :owner_id,
             inverse_of: :projects

  has_many :user_associations,
           class_name: 'ProjectUser',
           inverse_of: :project

  has_many :associated_users,
           through: :user_associations,
           source: :user
end

class ProjectUser < ActiveRecord::Base
  belongs_to :project,
             inverse_of: :user_associations

  belongs_to :user,
             inverse_of: :project_associations
end

使用多个查询执行此操作非常简单:

user = User.find(1)
all_projects = user.projects + user.associated_projects

但我怀疑可以使用Arel将其优化到单个查询中。

修改

我第一次尝试使用find_by_sql方法的解决方案是:

Project.find_by_sql([
  'SELECT "projects".* FROM "projects" ' \
  'INNER JOIN "project_users" ' \
  'ON "projects"."id" = "project_users"."project_id" ' \
  'WHERE "project_users"."user_id" = :user_id ' \
  'OR "projects"."owner_id" = :user_id',
  { user_id: 1 }
])

这会产生我期望的结果,但我想避免使用find_by_sql而是让Arel构建SQL。

3 个答案:

答案 0 :(得分:2)

这应该有效:

AsyncTask

您可以将其放在名为Project.joins(:user_associations) .where("projects.owner_id = ? OR project_users.user_id = ?", id, id) 的方法中。

您也可以使用UNION,虽然这里看起来有些过分。

只有一个警告:根据我的经验,连接模型的两个备用路径的结构,其中一个路径是连接表而另一个是直接外键,会导致很多麻烦。您将不得不一直处理这两种情况,而在复杂的查询中,User#all_projects可能会混淆数据库查询计划程序。如果您能够,您可能需要重新考虑。如果您删除OR列并在owner_id上标识role列的所有者,我认为您的头痛会更少。

编辑或在Arel:

project_users

答案 1 :(得分:0)

您可以使用includes指令,如下所示:

User.includes(:projects, :associated_projects).find(1)

检查http://apidock.com/rails/ActiveRecord/QueryMethods/includes

然后,如果您在找到的实例上调用projectsassociated_projects,则不会触发其他查询。

答案 2 :(得分:0)

我有我的解决方案,模型结构保持不变,我在名为User的{​​{1}}模型上添加了一个方法。

all_projects

调用class User < ActiveRecord::Base has_many :projects, inverse_of: :owner has_many :project_associations, class_name: 'ProjectUser', inverse_of: :user has_many :associated_projects, through: :project_associations, source: :project def all_projects query = Project.arel_table[:owner_id].eq(id).or(ProjectUser.arel_table[:user_id].eq(id)) Project.joins(:user_associations).where(query) end end 版本并执行此查询:

#all_projects

解决方案并不像我想的那样优雅,理想情况下我希望将SELECT "projects".* FROM "projects" INNER JOIN "project_users" ON "project_users"."project_id" = "products"."id" WHERE ("projects"."owner_id" = '1' OR "project_users"."user_id" = '1') 替换为Project.arel_table[:owner_id].eq(id)关联构建的查询,并将has_many projects添加到其中,但现在效果很好。