【问题标题】:How do I force a Rails query to return potentially two models per result?如何强制 Rails 查询每个结果可能返回两个模型?
【发布时间】:2017-07-10 12:44:57
【问题描述】:

我正在使用 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”?

【问题讨论】:

  • 您的 @results 变量将包含 MyObjectTime 实例的可枚举。没有一个对象是 MyObjectTime 的实例和 UserMyObjectTimeMatch 的任何关联实例。发出 1000 + 1 个查询的性能问题通过预先加载关联解决,使用 #includes(:user_my_object_time_matches)
  • “#includes(:user_my_object_time_matches)”会急切地加载所有关联吗?这不是我想要的,也不是我的查询所做的。查询只为每个 MyObjectTime 对象加载一个关联,这是我想急切加载的。
  • 你能举个例子吗...可以帮助我进一步帮助你:)。谢谢
  • 您能提供您的 ActiveRecord 模型的代码吗?最有可能的是,创建关联和范围的正确组合将解决您的问题并使您的代码更具可读性。关联范围:guides.rubyonrails.org/… 范围:api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/…

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


【解决方案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)

【讨论】:

  • 这两种方法的问题是每次循环迭代都会生成一个新的 SQL 查询。因此,如果@results 有 10000 行,那么您正在执行的操作会导致对 db 进行 10001 次查询,我希望将其限制为一个并一次获取所有数据。
  • 这是有道理的,尽管我认为没有标准化的方法可以做到这一点。您当然可以使用从您已经编写的查询中获得的数据自己填充模型,但我不确定它是否会更有效。
  • 当您说“您已经编写的查询”时,您是指我在问题中包含的“@results =”行吗?
【解决方案2】:

你不能。至少不使用 ActiveRecord 或 Rails 的默认界面。 ActiveRecord 查询方法被设计成只返回调用模型的对象。

例如,如果你像这样查询

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

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

构建结果对的说明

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

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

您可以通过这种方式使用includes 获取带有:answer_sheet 的标记。 我在示例中获取 20 个。

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

这会获取可以通过标记检索的 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

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

【讨论】:

  • 它不是必须使用 ActiveRecord,我只是将它包含在我的代码中,因为我想不出任何其他方法来做到这一点。如果有另一种不涉及 ActiveRecord 的方式,我可以。
  • Rails 的查询接口是基于 activeRecord 构建的,不支持您的要求。但正如我所说,您可以使用结果来构建对
  • 关于“您可以使用结果来构建对”,我不知道如何在不生成与之前生成的相同数量的查询的情况下执行此操作。
  • @Dave 我尝试添加一个示例。迭代不会生成带有 includes 的查询
  • 第二个查询的问题在于它使用了IN 子句。大多数(如果不是全部)关系数据库对可以在 IN 子句中使用的值的数量有限制。
【解决方案3】:

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

@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]}

【讨论】:

  • 仅供参考,这一行无法编译——“.map|t| [t, t.user_my_object_time_matches]}”。为什么我需要“地图”行?
【解决方案4】:

您可以使用 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!

运行自定义查询,其中包括来自父模型和兄弟模型的列(名称和 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"

【讨论】:

  • 结果是否有两个模型?如果您可以包含一个示例,将会很有帮助
  • 用示例更新答案
  • 不错!但是为什么你认为用 AR 来做会更好呢?使用 AR 访问它们仍然更容易!
  • 我没有。这不是“Rails 方式”,而只是指出这是一种替代方法,以防您需要运行自定义查询,而 ActiveRecord 可能无法满足需求。
【解决方案5】:

您可以在此处使用 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

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

@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

【讨论】:

    【解决方案6】:

    在没有关于模型或数据库的完整信息的情况下,我在下面做一些假设。大多数情况下,通过 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])
    

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

    【讨论】:

      猜你喜欢
      • 2018-01-05
      • 2011-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-09
      • 1970-01-01
      相关资源
      最近更新 更多