在最后一部分中,我们将对查询进行更深入的研究,并探讨一些更高级的方案。 我们将在本文中讨论Active Record模型之间的关系,但是我将远离那些可能会使新手编程感到困惑的示例。 在继续之前,类似以下示例的内容不应引起任何混淆:
Mission.last.agents.where(name: 'James Bond')
如果您不熟悉Active Record查询和SQL,建议您继续阅读之前的两篇文章。 如果没有我到目前为止积累的知识,可能很难吞下这一个人。 当然取决于你。 另一方面,如果您只是想看看这些稍微高级的用例,那么这篇文章的时间就不会比其他文章长。 让我们开始吧!
主题
- 范围和协会
- 微光连接
- 合并
- 有很多
- 定制联接
范围和协会
让我们重申一下。 我们可以立即查询Active Record模型,但是关联对于查询也很公平-并且我们可以链接所有这些东西。 到目前为止,一切都很好。 我们也可以将查找器打包到模型中的整洁,可重用范围中,我简要地提到了它们与类方法的相似性。
滑轨
class Agent < ActiveRecord::Base
belongs_to :mission
scope :find_bond, -> { where(name: 'James Bond') }
scope :licenced_to_kill, -> { where(licence_to_kill: true) }
scope :womanizer, -> { where(womanizer: true) }
scope :gambler, -> { where(gambler: true) }
end
# => Agent.find_bond
# => Agent.licenced_to_kill
# => Agent.womanizer
# => Agent.gambler
# => Mission.last.agents.find_bond
# => Mission.last.agents.licenced_to_kill
# => Mission.last.agents.womanizer
# => Mission.last.agents.gambler
# => Agent.licenced_to_kill.womanizer.gambler
# => Mission.last.agents.womanizer.gambler.licenced_to_kill
因此,您也可以将它们打包到您自己的类方法中并使用它来完成。 我认为,作用域并不是一帆风顺的东西,尽管有人说它们在这里和那里有点魔术,但是由于类方法实现了同样的事情,所以我选择这样做。
滑轨
class Agent < ActiveRecord::Base
belongs_to :mission
def self.find_bond
where(name: 'James Bond')
end
def self.licenced_to_kill
where(licence_to_kill: true)
end
def self.womanizer
where(womanizer: true)
end
def self.gambler
where(gambler: true)
end
end
# => Agent.find_bond
# => Agent.licenced_to_kill
# => Agent.womanizer
# => Agent.gambler
# => Mission.last.agents.find_bond
# => Mission.last.agents.licenced_to_kill
# => Mission.last.agents.womanizer
# => Mission.last.agents.gambler
# => Agent.licenced_to_kill.womanizer.gambler
# => Mission.last.agents.womanizer.gambler.licenced_to_kill
这些类方法的读法相同,您不需要用lambda刺穿任何人。 最适合您或您的团队的事物; 由您决定要使用哪个API。 只是不要混搭它们-坚持一次选择! 这两个版本都使您可以轻松地将这些方法链接到另一个类方法中,例如:
滑轨
class Agent < ActiveRecord::Base
belongs_to :mission
scope :licenced_to_kill, -> { where(licence_to_kill: true) }
scope :womanizer, -> { where(womanizer: true) }
def self.find_licenced_to_kill_womanizer
womanizer.licenced_to_kill
end
end
# => Agent.find_licenced_to_kill_womanizer
# => Mission.last.agents.find_licenced_to_kill_womanizer
滑轨
class Agent < ActiveRecord::Base
belongs_to :mission
def self.licenced_to_kill
where(licence_to_kill: true)
end
def self.womanizer
where(womanizer: true)
end
def self.find_licenced_to_kill_womanizer
womanizer.licenced_to_kill
end
end
# => Agent.find_licenced_to_kill_womanizer
# => Mission.last.agents.find_licenced_to_kill_womanizer
让我们再走一步-待在我身边。 我们可以在关联本身中使用lambda来定义特定范围。 起初看起来有点怪异,但它们可能非常方便。 这样就可以在您的关联上正确调用这些lambda。
这很酷,而且可读性很强,并且链式链接较短。 但是,请注意不要将这些模型耦合得太紧。
滑轨
class Mission < ActiveRecord::Base
has_many :double_o_agents,
-> { where(licence_to_kill: true) },
class_name: "Agent"
end
# => Mission.double_o_agents
告诉我这不酷! 它不是日常使用,但我猜想可以使用。 因此, Mission
在这里只能“请求”拥有杀人许可的特工。
关于语法的一句话,因为我们偏离了命名约定,并使用了更具表现力的东西,例如double_o_agents
。 为了避免混淆Rails,我们需要提及类名,否则可能会期望寻找一个DoubleOAgent
类。 当然,您可以同时拥有两个Agent
关联(通常的和您自定义的一个),并且Rails不会抱怨。
滑轨
class Mission < ActiveRecord::Base
has_many :agents
has_many :double_o__agents,
-> { where(licence_to_kill: true) },
class_name: "Agent"
end
# => Mission.agents
# => Mission.double_o_agents
微光连接
当查询数据库中的记录并且不需要所有数据时,可以选择指定要返回的确切内容。 为什么? 因为返回到Active Record的数据最终将内置到新的Ruby Objects中。 让我们看一下一种避免Rails应用程序中内存膨胀的简单策略:
滑轨
class Mission < ActiveRecord::Base
has_many :agents
end
class Agent < ActiveRecord::Base
belongs_to :mission
end
滑轨
Agent.all.joins(:mission)
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id"
因此,该查询返回了一个任务列表,该任务列表具有从数据库到Active Record的任务,然后依次从中开始构建Ruby对象。 mission
数据可用,因为这些行中的数据已合并到座席数据的行中。 这意味着,联接的数据在查询期间可用,但不会返回到Active Record。 因此,例如,您将拥有此数据来执行计算。
之所以特别酷,是因为您可以使用不会发送回应用程序的数据。 需要内置到Ruby对象中的属性较少(占用内存)可能是一个大胜利。 通常,考虑只发送绝对必要的行和列。 这样,您可以避免很多膨胀。
滑轨
Agent.all.joins(:mission).where(missions: { objective: "Saving the world" })
这里的语法仅作简要介绍:因为我们不是通过where
查询查询Agent
表,而是通过联接的:mission
表查询,所以我们需要指定我们在WHERE
子句中查找特定的missions
。
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."objective" = ? [["objective", "Saving the world"]]
在此处使用includes
还将使任务返回Active Record以进行急切的加载并占用构建Ruby对象的内存。
合并
例如,当我们要合并对具有您定义的特定范围的业务代表及其关联任务的查询时, merge
便会派上用场。 我们可以采用两个ActiveRecord::Relation
对象并合并它们的条件。 当然,这没什么大不了的,但是如果您要在使用关联时使用某个范围,则merge
很有用。
换句话说,我们可以通过merge
模型上的命名作用域对merge
进行过滤。 在前面的示例之一中,我们使用类方法自己定义了此类命名范围。
滑轨
class Mission < ActiveRecord::Base
has_many :agents
def self.dangerous
where(enemy: "Ernst Stavro Blofeld")
end
end
class Agent < ActiveRecord::Base
belongs_to :mission
end
滑轨
Agent.joins(:mission).merge(Mission.dangerous)
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = ? [["enemy", "Ernst Stavro Blofeld"]]
当我们封装了什么dangerous
任务是内Mission
模型,我们可以把它塞到一个join
通过merge
这种方式。 因此,将这种情况的逻辑转移到它所属的相关模型上,一方面是一种实现松散耦合的好方法-我们不希望Active Record模型彼此了解很多细节,而另一方面一方面,它在连接时为您提供了一个不错的API,而不会让您大吃一惊。 下面没有合并的示例将无法正常工作:
滑轨
Agent.all.merge(Mission.dangerous)
的SQL
SELECT "agents".* FROM "agents" WHERE "missions"."enemy" = ? [["enemy", "Ernst Stavro Blofeld"]]
现在,当我们将任务的ActiveRecord::Relation
对象合并到代理上时,数据库不知道我们正在谈论的任务。 我们需要弄清楚我们需要哪个关联并首先加入任务数据,否则SQL会感到困惑。 最后一颗樱桃。 我们还可以通过使代理程序参与进来,从而更好地封装它:
滑轨
class Mission < ActiveRecord::Base
has_many :agents
def self.dangerous
where(enemy: "Ernst Stavro Blofeld")
end
end
class Agent < ActiveRecord::Base
belongs_to :mission
def self.double_o_engagements
joins(:mission).merge(Mission.dangerous)
end
end
滑轨
Agent.double_o_engagements
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = ? [["enemy", "Ernst Stavro Blofeld"]]
那是我书中的甜樱桃。 封装,适当的OOP和出色的可读性。 大奖!
有很多
在上面,我们已经看到了很多的belongs_to
关联。 让我们从另一个角度看待这一点,并将秘密服务部分纳入其中:
滑轨
class Section < ActiveRecord::Base
has_many :agents
end
class Mission < ActiveRecord::Base
has_many :agents
end
class Agent < ActiveRecord::Base
belongs_to :mission
belongs_to :section
end
因此,在这种情况下,代理不仅将具有mission_id
而且还将具有section_id
。 到目前为止,一切都很好。 让我们查找具有特定任务的特工的所有部分,以便执行某些任务的部分。
滑轨
Section.joins(:agents)
的SQL
SELECT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections."id"
你有注意到吗? 一个小细节是不同的。 外键被翻转。 在这里,我们需要一个段列表,但是要使用如下外键: "agents"."section_id" = "sections."id"
换句话说,我们正在从要连接的表中查找外键。
滑轨
Agent.joins(:mission)
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id"
以前,我们通过belongs_to
关联进行的连接如下所示:外键已镜像( "missions"."id" = "agents"."mission_id"
),并从我们要启动的表中查找外键。
回到您的has_many
场景,我们现在将获得重复的部分列表,因为它们在每个部分中当然都有多个代理。 因此,对于加入的每个代理程序列,我们为该section或section_id获得一行-简而言之,我们基本上是在复制行。 为了使这一点更加令人眼花,乱,让我们也将任务组合在一起。
滑轨
Section.joins(agents: :mission)
的SQL
SELECT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections"."id" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id"
检查两个INNER JOIN
部分。 还在我这儿? 我们正在通过代理商从代理商部分“到达”他们的任务。 是的,我知道有趣的东西令人头痛。 我们得到的是与某个部分间接相关的任务。
结果,我们连接了新的列,但是该查询返回的行数仍然相同。 返回到Active Record的结果(导致构建新的Ruby对象)仍然是部分列表。 因此,当我们有多个执行多个代理的任务时,我们将再次获得本节的重复行。 让我们进一步过滤一下:
滑轨
Section.joins(agents: :mission).where(missions: { enemy: "Ernst Stavro Blofeld" })
的SQL
SELECT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections"."id" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = 'Ernst Stavro Blofeld'
现在,我们只返回涉及与恩斯特·斯塔夫罗·布洛费尔德为敌军的任务的部分。 大都会人可能会认为自己是超级坏人,他们可以在多个区域中运作,例如A和C区域,美国和加拿大。
如果给定部分中有多个业务代表正在执行相同的任务以停止Blofeld或其他事情,我们将再次向Active Record中返回重复的行。 让我们对此有所不同:
滑轨
Section.joins(agents: :mission).where(missions: { enemy: "Ernst Stavro Blofeld" }).distinct
的SQL
SELECT DISTINCT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections"."id" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = 'Ernst Stavro Blofeld'
这给我们带来的就是布洛费尔德行动的区域数量(众所周知),这些区域活跃着特工与他为敌的任务。 作为最后一步,让我们再次进行一些重构。 我们将其提取为class Section
上一个不错的“小”类方法:
滑轨
class Section < ActiveRecord::Base
has_many :agents
def self.critical
joins(agents: :mission).where(missions: { enemy: "Ernst Stavro Blofeld" }).distinct
end
end
class Mission < ActiveRecord::Base
has_many :agents
end
class Agent < ActiveRecord::Base
belongs_to :mission
belongs_to :section
end
您可以对此进行更多重构,并分担职责以实现更宽松的耦合,但现在让我们继续。
定制联接
大多数情况下,您可以依靠Active Record编写所需的SQL。 这意味着您将留在Ruby领域,无需过多担心数据库详细信息。 但是有时您需要戳入SQL领域并做自己的事。 例如,如果您需要使用LEFT
并且默认情况下打破Active Record进行INNER
联接的通常行为。 joins
是一个小窗口,可根据需要编写您自己的自定义SQL。 您打开它,插入自定义查询代码,关闭“窗口”,然后可以继续在Active Record查询方法中添加。
让我们用一个涉及小工具的示例来演示这一点。 假设典型的探员通常有has_many
小工具,我们想找到没有配备任何奇特工具的探员来帮助他们。 通常的联接不会产生良好的结果,因为我们实际上对这些间谍玩具的nil
值(或SQL语言中的null
感兴趣。
滑轨
class Mission < ActiveRecord::Base
has_many :agents
end
class Agent < ActiveRecord::Base
belongs_to :mission
has_many :gadgets
end
class Gadget < ActiveRecord::Base
belongs_to :agent
end
执行joins
操作时,我们将仅返回已配备小工具的代理,因为这些小工具上的agent_id
不为nil。 这是默认内部联接的预期行为。 内部联接建立在双方的匹配之上,并且仅返回与该条件匹配的数据行。 对于不携带小工具的业务代表,值nil
不存在小工具不符合该条件。
滑轨
Agent.joins(:gadgets)
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "gadgets" ON "gadgets"."agent_id" = "agents"."id"
另一方面,我们正在寻找急需军需人员爱戴的schmuck经纪人。 您的第一个猜测可能如下所示:
滑轨
Agent.joins(:gadgets).where(gadgets: {agent_id: nil})
的SQL
SELECT "agents".* FROM "agents" INNER JOIN "gadgets" ON "gadgets"."agent_id" = "agents"."id" WHERE "gadgets"."agent_id" IS NULL
不错,但是从SQL输出中可以看出,它并没有发挥作用,仍然坚持使用默认的INNER JOIN
。 这就是我们需要的一个场景OUTER
加入,因为我们的“方程式”的一侧缺失,可以这么说。 我们正在寻找不存在的小工具的结果,更确切地说,是为没有小工具的代理商寻找结果。
到目前为止,当我们在联接中将符号传递给Active Record时,它期望建立关联。 另一方面,使用传入的字符串,它希望它是SQL代码的实际片段,这是查询的一部分。
滑轨
Agent.joins("LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id").where(gadgets: {agent_id: nil})
的SQL
SELECT "agents".* FROM "agents" LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id WHERE "gadgets"."agent_id" IS NULL
或者,如果您对没有任务的懒惰代理感到好奇(可能挂在巴巴多斯或任何地方),我们的自定义联接将如下所示:
滑轨
Agent.joins("LEFT OUTER JOIN missions ON missions.id = agents.mission_id").where(missions: { id: nil })
的SQL
SELECT "agents".* FROM "agents" LEFT OUTER JOIN missions ON missions.id = agents.mission_id WHERE "missions"."id" IS NULL
外部联接是更具包容性的联接版本,因为它会匹配联接表中的所有记录,即使其中某些关系尚不存在。 因为这种方法不像内部联接那样排他性,所以您到处都会得到一堆nil。 当然,在某些情况下这可以提供很多信息,但是内连接通常是我们所需要的。 在这种情况下,Rails 5将让我们使用一种称为left_outer_joins
的专门方法。 最后!
道路上的一件事:尽可能将这些窥探到SQL领域的漏洞保持尽可能小。 您将为所有人(包括您将来的自我)做出巨大的帮助。
最后的想法
使Active Record为您编写高效的SQL是您对于初学者来说应该从此迷你系列中学到的主要技能之一。 这样,您还将获得与其所支持的任何数据库兼容的代码,这意味着查询将在各个数据库之间保持稳定。 您不仅需要了解如何使用Active Record,而且还必须了解基础SQL,这是同等重要的。
是的,SQL可能很无聊,读起来很乏味,外观也不雅致,但是不要忘了Rails将Active Record包裹在SQL周围,并且您不应该忽视对这项重要技术的理解-仅仅是因为Rails使得不关心大多数内容变得非常容易的时间。 效率对于数据库查询至关重要,尤其是当您为流量较大的更大的受众构建内容时。
现在访问Internet并找到更多有关SQL的资料,一次又一次地将其从您的系统中删除!
翻译自: https://code.tutsplus.com/articles/queries-in-rails-part-3--cms-26453