如何强制Rails查询每个结果可能返回两个模型?

时间:2017-02-21 03:27:07

标签: ruby-on-rails search activerecord model ruby-on-rails-5

我正在使用Rails 5.我在我的控制器模型中有这个用于根据标准加载某个模型......

  @results = MyObjectTime.joins(:my_object,
                            "LEFT JOIN user_my_object_time_matches on my_object_times.id = user_my_object_time_matches.my_object_time_id #{additional_left_join_clause}")
                     .where( search_criteria.join(' and '), *search_values )
                     .limit(1000) 
                     .order("my_objects.day DESC, my_objects.name")
                     .paginate(:page => params[:page])

我的问题是,如何重写上述内容,以便@results数组同时包含" MyObjectTime"和任何潜在的" UserMyObjectTimeMatch"它链接到?

6 个答案:

答案 0 :(得分:3)

你做不到。至少不使用ActiveRecord或Rails'默认界面。 ActiveRecord查询方法的设计方式使它们只返回调用Model的对象。

例如,如果您查询

MyObjectTime.joins(:time).where(parent_id: 5)

它仅返回MyObjectTime的对象。但是,由于join,可能还会获取关联time中的记录,但不会返回。所以,你可以利用它。特别是当您使用includes代替joins时,会获取关联的模型,您可以通过引用关联记录/对象来使用它们。

构建结果对的说明

通过创建具有所需结果的哈希,可以轻松完成此操作。

例如,考虑具有Mark关联的模型answer_sheet

您可以使用:answer_sheetincludes方式获取标记。 我在示例中提取了20个。

marks = Mark.limit(20).includes(:answer_sheet);

这会获取可以通过mark检索的answer_sheet,因此,以这种方式构建哈希

h = {}
marks.each do |mark|
  h[mark.id] = {}
  h[mark.id][:mark] = mark
  h[mark.id][:answer_sheet] = mark.answer_sheet
end

现在,您的哈希通过mark.id键准备好markanswer_sheet对象。

这只会在首次提取时最多执行两次查询,并且迭代不会触发任何进一步的查询。在我的系统中,只有两个必需的查询是(使用includes

SELECT  "marks".* FROM "marks" LIMIT 20
  AnswerSheet Load (0.9ms)  SELECT "answer_sheets".* FROM "answer_sheets" WHERE "answer_sheets"."mark_id" IN (877, 3035, 3036, 878, 879, 880, 881, 561, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893)

您甚至可以使用标记对象本身作为键。然后构建过程变得更加简单

h = {}
marks.each do |mark|
  h[mark] = mark.answer_sheet
end

现在,只要您想访问与answer_sheet相关联的mark,您就需要使用h[mark]来获取它。

答案 1 :(得分:3)

我建议您搜索替代方案,但要了解您提供的信息并希望避免评论中提到的潜在10001查询,如果您有has_many' user_my_object_time_matches'您可以做的设置:

@results = MyObjectTime.joins(:my_object,
                            "LEFT JOIN user_my_object_time_matches on my_object_times.id = user_my_object_time_matches.my_object_time_id #{additional_left_join_clause}")
        .where( search_criteria.join(' and '), *search_values )
        .limit(1000) 
        .order("my_objects.day DESC, my_objects.name")
        .paginate(:page => params[:page])
        .includes(:user_my_object_time_matches)
        .map{|t| [t, t.user_my_object_time_matches]}

答案 2 :(得分:2)

您可以使用ActiveRecord连接执行原始SQL查询,该连接允许您根据需要包含尽可能多的表中的列。您可以在一个查询中获取所有内容。您需要确保为不明确的列名称添加别名(例如,就像我在示例中为name列所做的那样)

我不知道你的模特是什么样的,但这里有一个简单的父/兄弟示例来展示:

创建模型和迁移

# testmigration.rb
class Testtables < ActiveRecord::Migration[5.0]
  def change
    create_table :parents do |t|
      t.string :name
      t.timestamps
    end

    create_table :siblings do |t|
      t.string :name
      t.references :parent
      t.timestamps
    end
  end
end

# parent.rb
class Parent < ApplicationRecord
  has_many :siblings
end

# sibling.rb
class Sibling < ApplicationRecord
end

创建测试数据

> rails c
> Parent.new(name: "Parent A").save!
> Parent.new(name: "Parent B").save!
> Sibling.new(name: "Sibling 1 - Parent A", parent_id: 1).save!
> Sibling.new(name: "Sibling 2 - Parent A", parent_id: 1).save!
> Sibling.new(name: "Sibling 1 - Parent B", parent_id: 2).save!
> Sibling.new(name: "Sibling 2 - Parent B", parent_id: 2).save!
> Sibling.new(name: "Sibling 3 - Parent B", parent_id: 2).save!

运行自定义查询,其中包括来自Parent和Sibling模型的列(name和created_at)

> sql_query = "SELECT p.name as parent_name, p.created_at as parent_created, s.name as sibling_name, s.created_at as sibling_created FROM public.parents p INNER JOIN public.siblings s on s.parent_id = p.id;"
> result = ActiveRecord::Base.connection.execute(sql_query)

检查结果

> result[0]['parent_name']
  => "Parent A" 
> result[0]['sibling_name']
  => "Sibling 1 - Parent A"
> result[1]['parent_created']
  => "2017-03-04 18:31:54.661714"

答案 3 :(得分:1)

无法评论,所以要问这样。

问题是,你为什么要这样做?

为什么你认为使用关系对你不起作用?

# I already have some results pulled the way you did and I wanna use them
  @results.each do |r|
    self.foo(r, r.user_my_object_time_match);
  end

#or if I really wanted a double array, then I could do
  @results = MyObjectTime.<clauses>().collect { |mot| [mot, mot.user_my_object_time_match] }

如果这些不适合您,请说明问题所在,因为此时此问题看起来有XY问题(https://meta.stackoverflow.com/tags/xy-problem/info

答案 4 :(得分:1)

您可以在此处使用eagerloading,而不是将整个结果合并到一个数组中,如:

@results = MyObjectTime.joins(:my_object,
                            "LEFT JOIN user_my_object_time_matches on my_object_times.id = user_my_object_time_matches.my_object_time_id #{additional_left_join_clause}")
                     .where( search_criteria.join(' and '), *search_values )
                     .limit(1000) 
                     .order("my_objects.day DESC, my_objects.name")
                     .paginate(:page => params[:page]).includes(:user_my_object_time_matches)

一旦你使用了包含它就不会激发额外的查询。

@first_my_object_time = @results.first
@user_my_object_time_matches = @first_my_object_time.user_my_object_time_matches

如果你想要它在同一个数组中,你可以使用ActiveRecord Select方法直接从sql中选择:

@results = MyObjectTime.joins(:my_object,
                        "LEFT JOIN user_my_object_time_matches on my_object_times.id = user_my_object_time_matches.my_object_time_id #{additional_left_join_clause}")
                 .where( search_criteria.join(' and '), *search_values )
                 .limit(1000) 
                 .order("my_objects.day DESC, my_objects.name")
                 .paginate(:page => params[:page]).select("my_object_time.*, user_my_object_time_matches.*").as_json

答案 5 :(得分:1)

如果没有关于模型或数据库的完整信息,我在下面做了一些假设。大多数情况下,通过AR模型中的正确关联和/或范围,您可以在不编写任何原始SQL的情况下完成:

class MyObjectTime < ApplicationRecord
  has_many :my_objects, ->(args){ where(args) }

  scope :top_1000, ->{ limit(1000) }
  scope :order_by_my_objects, ->{ order(my_objects: { day: :desc, name: :asc }) }
end

class UserMyObjectTimeMatches < ApplicationRecord
  belongs_to :my_object_time
end

MyObjectTime.my_objects(params[:search_args])
  .order_by_my_objects.top_1000
  .include(:my_object_time).paginate(page: params[:page])

如果我有完整的代码,我可以设置模型和测试 - 所以这段代码没有经过测试,很可能需要调整。