Rails中的查询,第2部分

在第二篇文章中,我们将深入研究Rails中的Active Record查询。 如果您还是SQL的新手,我将添加一些简单的示例,以便您随便进行标记并掌握一些语法。

话虽如此,如果您在继续阅读之前先完成了快速的SQL教程,那绝对有帮助。 否则,请花一些时间来了解我们使用的SQL查询,我希望到本系列结束时,它不会再变得令人生畏。

它的大部分确实很简单,但是如果您刚开始编码,那么语法就有点奇怪了,尤其是在Ruby中。 坚持下去,这不是火箭科学!

主题

  • 包含和急切加载
  • 联接表
  • 渴望加载
  • 范围
  • 集合体
  • 动态发现者
  • 具体领域
  • 自订SQL

包含和急切加载

这些查询包含一个以上的数据库表,可能是最重要的,可以从本文中删除。 它归结为:而不是做对于分布在多个表的信息多次查询, includes尝试将这些保持在最低水平。 其背后的关键概念称为“快速加载”,这意味着我们进行查找时正在加载关联的对象。

如果我们通过遍历对象集合然后尝试从另一个表访问其关联记录来做到这一点,我们将遇到一个称为“ N + 1查询问题”的问题。 例如,对于代理集合中的每个agent.handler ,我们将对代理及其处理程序分别agent.handler查询。 这是我们需要避免的,因为这根本无法扩展。 相反,我们执行以下操作:

滑轨
agents = Agent.includes(:handlers)

如果我们现在遍历如此多的代理程序(考虑到我们现在还没有限制返回的记录数),我们将以两个查询结束,而不是可能是一个庞大的查询。

的SQL
SELECT "agents".* FROM "agents"
SELECT "handlers".* FROM "handlers" WHERE "handlers"."id" IN (1, 2)

列表中的这个代理有两个处理程序,当我们现在向代理对象询问其处理程序时,无需触发其他数据库查询。 当然,我们可以更进一步,并渴望加载多个关联的表记录。 如果无论出于何种原因我们不仅需要加载处理程序,还需要加载与代理相关的任务,则可以使用这样的includes

滑轨
agents = Agent.includes(:handlers, :mission)

简单! 请注意,对于包含使用单数和复数版本。 它们取决于您的模型关联。 has_many关联使用复数形式,而belongs_tohas_one当然需要单数形式。 如果需要,还可以添加where子句以指定其他条件,但是为急切加载的关联表指定条件的首选方法是使用joins

急切加载时要记住的一件事是,将添加的数据将全部发送回Active Record,Active Record进而构建包括这些属性的Ruby对象。 这与“简单”加入数据相反,在这种情况下,您将获得一个虚拟结果,例如可以用于计算的结果,并且消耗的内存少于包含的结果。

联接表

联接表是另一种工具,可以避免在管道中发送过多不必要的查询。 一种常见的情况是使用一个查询将两个表连接起来,该查询返回某种组合记录。 joins只是Active Record的另一种查找器方法,使用SQL术语,您可以使用JOIN表。 这些查询可以返回从多个表中合并的记录,并且您将获得一个虚拟表,其中包含这些表中的记录。 当您将其与激发每个表的所有类型的查询进行比较时,这很不错。 通过这种方法可以获得几种不同类型的数据重叠。

内部joins的默认方式。 这将匹配所有与某个ID及其作为另一个对象或表中的外键的表示形式匹配的结果。 在下面的示例中,简单地说:给我所有任务,其中任务id在座席表中显示为mission_id"agents"."mission_id" = "missions"."id" 内部联接排除了不存在的关系。

滑轨
Mission.joins(:agents)
的SQL
SELECT "missions".* FROM "missions" INNER JOIN "agents" ON "agents"."mission_id" = "mission"."id"

因此,我们在单个查询中匹配任务及其伴随的代理人! 当然,我们可以首先获得任务,一个接一个地进行迭代,然后寻求他们的代理。 但是,然后我们将回到可怕的“ N + 1查询问题”。 不,谢谢!

这种方法的优点还在于,我们不会通过内部联接获得零个案例。 我们只会获得返回的记录,这些记录将其ID与关联表中的外键匹配。 例如,如果我们需要找到缺少任何特工的任务,则需要外部联接。 由于当前涉及编写您自己的OUTER JOIN SQL,因此我们将在上一篇文章中对此进行研究。 回到标准联接,您当然也可以联接多个关联的表。

滑轨
Mission.joins(:agents, :expenses, :handlers)

您可以在where添加一些where子句,以进一步指定您的发现者。 在下面的示例中,我们仅查找由詹姆斯·邦德执行的任务,并且仅查找属于任务“ Moonraker”的特工。

Mission.joins(:agents).where( agents: { name: 'James Bond' })
的SQL
SELECT "missions".* FROM "missions" INNER JOIN "agents" ON "agents"."mission_id" = "missions"."id" WHERE "agents"."name" = ?  [["name", "James Bond"]]
滑轨
Agent.joins(:mission).where( missions: { mission_name: 'Moonraker' })
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."mission_name" = ?  [["mission_name", "Moonraker"]]

使用joins ,您还必须注意模型关联的单数和复数用法。 由于我的Missionhas_many :agents ,我们可以使用复数形式。 另一方面,对于Agentbelongs_to :mission ,只有单数版本有效而​​不会崩溃。 重要的小细节: where部分更简单。 由于您要扫描表中满足特定条件的多行,因此复数形式总是有意义的。

范围

范围是将常见查询需求提取到您自己命名的方法中的便捷方法。 这样,如果其他人必须使用您的代码,或者将来您是否需要重新访问某些查询,它们将更容易传递,也可能更容易理解。 您可以为单个模型定义它们,但也可以将它们用于它们的关联。

天空是really-极限joinsincludeswhere都是公平的游戏! 由于作用域还返回ActiveRecord::Relations对象,因此您可以毫不犹豫地链接它们并在它们之上调用其他作用域。 像这样提取范围并将它们链接起来以进行更复杂的查询非常方便,并使更长的范围更加可读。 范围是通过“ stabby lambda”语法定义的:

滑轨
class Mission < ActiveRecord::Base
  has_many: agents

  scope :successful, -> { where(mission_complete: true) }
end

Mission.successful
class Agent < ActiveRecord::Base

  belongs_to :mission

  scope :licenced_to_kill, -> { where(licence_to_kill: true) }
  scope :womanizer,        -> { where(womanizer: true) }
  scope :gambler,          -> { where(gambler: true) } 
end

# Agent.gambler
# Agent.womanizer
# Agent.licenced_to_kill
# Agent.womanizer.gambler

Agent.licenced_to_kill.womanizer.gambler
的SQL
SELECT "agents".* FROM "agents" WHERE "agents"."licence_to_kill" = ? AND "agents"."womanizer" = ? AND "agents"."gambler" = ?  [["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

从上面的示例中可以看到,仅将示波器链接在一起时,找到James Bond会更好。 这样,您可以混合和匹配各种查询并同时保持DRY。 如果您需要通过关联的作用域,则也可以使用它们:

Mission.last.agents.licenced_to_kill.womanizer.gambler
SELECT  "missions".* FROM "missions"  ORDER BY "missions"."id" DESC LIMIT 1
SELECT "agents".* FROM "agents" WHERE "agents"."mission_id" = ? AND "agents"."licence_to_kill" = ? AND "agents"."womanizer" = ? AND "agents"."gambler" = ?  [["mission_id", 33], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

当您查看Mission.all类的内容时,也可以重新定义default_scope

class Mission < ActiveRecord::Base
  default_scope { where status: "In progress" }
end

Mission.all
的SQL
SELECT "missions".* FROM "missions" WHERE "missions"."status" = ?  [["status", "In progress"]]

集合体

本节没有的理解方面涉及这么多先进,但你会更多的往往不是需要他们可以考虑更高级一点比一般的取景器般的场景.all.first.find_by_id或什么的。 例如,基于基本计算的过滤很可能是新手无法立即联系到的。 我们到底在这里看什么?

  • sum
  • count
  • minimum
  • maximum
  • average

轻轻松松吧? 很棒的事情是,我们可以让Active Record为我们完成所有这些工作,并通过查询返回这些结果,而不是遍历返回的对象集合进行这些计算,最好是在一个查询中。 很好,是吗?

  • count
滑轨
Mission.count

# => 24
的SQL
SELECT COUNT(*) FROM "missions"
  • average
滑轨
Agent.average(:number_of_gadgets).to_f

# => 3.5
的SQL
SELECT AVG("agents"."number_of_gadgets") FROM "agents"

由于我们现在知道了如何利用joins ,因此我们可以更进一步,仅要求代理商执行特定任务所需的小工具平均值。

滑轨
Agent.joins(:mission).where(missions: {name: 'Moonraker'}).average(:number_of_gadgets).to_f

# => 3.4
的SQL
SELECT AVG("agents"."number_of_gadgets") FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."name" = ?  [["name", "Moonraker"]]

在那时,将这些平均小工具的数量按任务名称分组变得微不足道了。 请参阅以下有关分组的更多信息:

滑轨
Agent.joins(:mission).group('missions.name').average(:number_of_gadgets)
的SQL
SELECT AVG("agents"."number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" GROUP BY missions.name
  • sum
滑轨
Agent.sum(:number_of_gadgets) 

Agent.where(licence_to_kill: true).sum(:number_of_gadgets) 

Agent.where.not(licence_to_kill: true).sum(:number_of_gadgets)
的SQL
SELECT SUM("agents"."number_of_gadgets") FROM "agents"

SELECT SUM("agents"."number_of_gadgets") FROM "agents" WHERE "agents"."licence_to_kill" = ?  [["licence_to_kill", "t"]]

SELECT SUM("agents"."number_of_gadgets") FROM "agents" WHERE ("agents"."licence_to_kill" != ?)  [["licence_to_kill", "t"]]
  • maximum
滑轨
Agent.maximum(:number_of_gadgets)

Agent.where(licence_to_kill: true).maximum(:number_of_gadgets)
的SQL
SELECT MAX("agents"."number_of_gadgets") FROM "agents"

SELECT MAX("agents"."number_of_gadgets") FROM "agents" WHERE "agents"."licence_to_kill" = ?  [["licence_to_kill", "t"]]
  • minimum
滑轨
Agent.minimum(:iq)

Agent.where(licence_to_kill: true).minimum(:iq)
的SQL
SELECT MIN("agents"."iq") FROM "agents"

SELECT MIN("agents"."iq") FROM "agents" WHERE "agents"."licence_to_kill" = ?  [["licence_to_kill", "t"]]

注意!

所有这些聚合方法都不允许您链接其他内容,它们是终端。 顺序对于进行计算很重要。 我们从这些操作中没有得到ActiveRecord::Relation对象,这会使音乐停止在这一点上,而是获得一个哈希或数字。 以下示例不起作用:

滑轨
Agent.maximum(:number_of_gadgets).where(licence_to_kill: true)

Agent.sum(:number_of_gadgets).where.not(licence_to_kill: true)

Agent.joins(:mission).average(:number_of_gadgets).group('missions.name')

分组

如果您希望将计算分解并分类为逻辑组,则应使用GROUP子句,而不是在Ruby中执行。 我的意思是,您应该避免对可能产生大量查询的组进行迭代。

滑轨
Agent.joins(:mission).group('missions.name').average(:number_of_gadgets)

# => { "Moonraker"=> 4.4, "Octopussy"=> 4.9 }
的SQL
SELECT AVG("agents"."number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" GROUP BY missions.name

本示例查找在单个查询中分组到特定任务的所有代理,并返回以计算出的小工具平均数量为其值的哈希值! 对! 当然,其他计算也是如此。 在这种情况下,让SQL完成工作确实更有意义。 我们为这些聚合触发的查询数量太重要了。

动态发现者

对于模型上的每个属性,例如说nameemail_addressfavorite_gadget等,Active Record都可以让您使用为您动态创建的非常易读的查找器方法。 我知道这听起来很神秘,但是除了find_by_idfind_by_favorite_gadget之外,它并不代表其他任何含义。 find_by部分是标准的,Active Record只是为您查找属性的名称。 您甚至可以添加一个! 如果您什么也找不到,则希望该查找程序引发错误。 最麻烦的是,您甚至可以将这些动态查找器方法链接在一起。 像这样:

滑轨
Agent.find_by_name('James Bond')

Agent.find_by_name_and_licence_to_kill('James Bond', true)
的SQL
SELECT  "agents".* FROM "agents" WHERE "agents"."name" = ? LIMIT 1  [["name", "James Bond"]]

SELECT  "agents".* FROM "agents" WHERE "agents"."name" = ? AND "agents"."licence_to_kill" = ? LIMIT 1  [["name", "James Bond"], ["licence_to_kill", "t"]]

当然,您可以对此有所了解,但我认为,如果超出两个属性,它就会失去其魅力和实用性:

滑轨
Agent.find_by_name_and_licence_to_kill_and_womanizer_and_gambler_and_number_of_gadgets('James Bond', true, true, true, 3)
的SQL
SELECT  "agents".* FROM "agents" WHERE "agents"."name" = ? AND "agents"."licence_to_kill" = ? AND "agents"."womanizer" = ? AND "agents"."gambler" = ? AND "agents"."number_of_gadgets" = ? LIMIT 1  [["name", "James Bond"], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"], ["number_of_gadgets", 3]]

在这个例子中,很高兴看到它是如何工作的。 每个新的_and_添加一个SQL AND运算符,以将属性逻辑上联系在一起。 总体而言,动态查找器的主要优点是可读性-但是,过多的动态属性会很快失去该优势。 我很少使用它,也许主要是当我在控制台中玩时使用,但是知道Rails提供了这种巧妙的小技巧绝对是一件很好的事。

具体领域

通过Active Record,您可以选择返回更关注对象所携带属性的对象。 通常,如果没有另外指定,查询将通过*SELECT "agents".* )连续询问所有字段,然后Active Record用完整的属性集构建Ruby对象。 但是,您只能select查询应返回的特定字段,并限制Ruby对象需要“携带”的属性数量。

滑轨
Agent.select("name") 

=> #<ActiveRecord::Relation [#<Agent 7: nil, name: "James Bond">, #<Agent id: 8, name: "Q">, ...]>
的SQL
SELECT "agents"."name" FROM "agents"
滑轨
Agent.select("number, favorite_gadget") 

=> #<ActiveRecord::Relation [#<Agent id: 7, number: '007', favorite_gadget: 'Walther PPK'>, #<Agent id: 8, name: "Q", favorite_gadget: 'Broom Radio'>, ... ]>
的SQL
SELECT "agents"."number", "agents"."favorite_gadget" FROM "agents"

如您所见,返回的对象将仅具有选定的属性以及它们的ID(当然,这是任何对象所给定的)。 如果您使用上述字符串或符号,则没有任何区别,查询将是相同的。

滑轨
Agent.select(:number_of_kills)

Agent.select(:name, :licence_to_kill)

注意:如果您尝试访问查询中未选择的对象的属性,则会收到MissingAttributeError 。 由于该id将自动为您提供无论如何,你可以要求该ID不选择它,虽然。

自订SQL

最后但并非最不重要的一点是,您可以通过find_by_sql编写自己的自定义SQL。 如果您对自己的SQL-Fu有足够的信心,并且需要对数据库进行一些自定义调用,则此方法有时可能会派上用场。 但这是另一个故事。 只是不要忘记先检查Active Record包装器方法,并避免在Rails尝试与您会面超过一半的时候重新发明轮子。

滑轨
Agent.find_by_sql("SELECT * FROM agents")

Agent.find_by_sql("SELECT name, licence_to_kill FROM agents")

毫不奇怪,这导致:

的SQL
SELECT * FROM agents

SELECT name, licence_to_kill FROM agents

由于作用域和您自己的类方法可以互换使用以满足您的自定义查找器需求,因此对于更复杂的SQL查询,我们可以更进一步。

滑轨
class Agent < ActiveRecord::Base

  ...

  def self.find_agent_names
    query = <<-SQL
      SELECT name
      FROM agents
    SQL
    self.find_by_sql(query)
  end
end

我们可以编写将SQL封装在Here文档中的类方法。 这使我们能够以一种非常易读的方式编写多行字符串,然后将该SQL字符串存储在一个变量中,我们可以重复使用该变量并将其传递给find_by_sql 。 这样,我们就不会在方法调用中添加大量查询代码。 如果您有多个地方可以使用此查询,那么它也是DRY。

由于这本来应该是新手友好的,而不是SQL教程本身,所以出于某种原因,我将示例保持为非常简单。 但是,用于处理更复杂查询的技术完全相同。 很难想象在那里有一个自定义SQL查询,该查询可以扩展到超过十行代码。

合理地选择所需的坚果! 它可以节省生命。 这里有一个关于语法的词。 SQL部分只是一个标识符,用于标记字符串的开头和结尾。 我敢打赌,您将不再需要这种方法了-希望如此! 它肯定有它的位置,没有它,Rails的土地就不会一样了—在极少数情况下,您绝对希望使用它来微调自己的SQL。

最后的想法

我希望您能更轻松地编写查询和阅读可怕的原始SQL。 我们在本文中讨论的大多数主题对于编写处理更复杂的业务逻辑的查询都是必不可少的。 花些时间了解这些内容,并在控制台中进行一些查询。

我很确定,如果您从事第一个实际项目,并且需要制定自己的自定义查询,那么当您离开教程领域时,您的Rails信誉迟早会大大提高。 如果您仍然对这个话题不满意,我想说的只是简单地玩一玩-这真的不是火箭科学!

翻译自: https://code.tutsplus.com/articles/queries-in-rails-part-2--cms-26450

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值