activerecord_优化ActiveRecord

activerecord

Ruby on Rails编程会宠坏您。 不断发展的框架将使您免于其他框架泛滥的烦恼。 您将用习惯于编写的几行代码来表达您的想法。 然后您将使用ActiveRecord。

作为长期的Java™程序员,ActiveRecord对我有些陌生。 使用Java框架,我通常会在独立的模型和模式之间建立映射。 这样的框架就是映射框架 。 使用ActiveRecord,我只能在SQL或称为迁移的Ruby类中定义数据库模式。 将对象模型设计基于数据库结构的框架称为包装框架 。 但是与大多数包装框架不同,Rails可以通过查询数据库表来发现对象模型的功能。 除了构建复杂的查询,我还可以使用模型遍历Ruby而不是SQL中的关系。 我获得了包装框架的简单性以及映射框架的许多功能。 ActiveRecord易于使用且易于扩展。 有时候,这太容易了。

像任何数据库框架一样,ActiveRecord可以让我做很多使我陷入困境的事情。 我可以获取太多列,也可以放弃重要的结构数据库功能,例如索引或空约束。 我并不是说ActiveRecord是一个糟糕的框架。 如果您需要扩展应用程序,则只需要知道如何加强应用程序即可。 在本文中,我将带您逐步了解Rails非正统的持久性框架可能需要的一些重要优化。

管理基础

使用script/generate model model_name ,生成支持模式的模型就像生成少量代码一样容易。 如您所知,该命令将生成您的模型,迁移,单元测试,甚至是默认夹具。 尝试在迁移中填写一些数据列,输入一些测试数据,编写一些测试,添加一些验证,然后将其完成是很诱人的。 但小心点。 您还应该考虑整体数据库设计。 请记住以下几点:

  • Rails不会使您免受基本数据库性能问题的困扰。 您的数据库需要信息(通常以索引的形式)来使性能良好。
  • Rails不会使您免受数据完整性问题的困扰。 尽管大多数Rails开发人员不喜欢在数据库中保留约束,但是您应该考虑可空列之类的事情。
  • Rails为许多元素提供了方便的默认值。 有时,对于大多数实际应用而言,默认属性(如文本字段的长度)过大。
  • Rails不会强迫您创建有效的数据库设计。

在继续前进并深入研究ActiveRecord之前,应确保您具有牢固的基础。 确保索引结构适合您。 如果给定的表很大,如果您要搜索id以外的列,并且索引对您有帮助(请参阅数据库管理器文档以获取详细信息-不同的数据库以不同的方式使用索引),请确保创建您的索引。 您无需使用SQL即可创建索引-您只需使用迁移即可。 您可以使用create_table迁移轻松创建索引,或者创建其他创建索引的迁移。 这是一个迁移示例,该迁移创建了一个我们用于ChangeingThePresent.org的索引(请参阅参考资料 ):

清单1.在迁移中创建索引
class AddIndexesToUsers < ActiveRecord::Migration
  def self.up
    add_index :members, :login
    add_index :members, :email
    add_index :members, :first_name
    add_index :members, :last_name
  end

  def self.down
    remove_index :members, :login
    remove_index :members, :email
    remove_index :members, :first_name
    remove_index :members, :last_name
  end
end

ActiveRecord将处理id上的索引,因此我显式添加了在各种搜索中使用的索引,因为该表很大,不经常更新且经常搜索。 通常,我们会等到在给定查询中测量问题后再采取行动。 这种策略使我们不必再去猜测数据库引擎。 但是对于用户而言,我们知道该表将Swift增长为数百万个用户,并且如果不对频繁搜索的列建立索引,该表将无效。

另外两个常见的问题也与迁移有关。 如果您有不应该为null的字符串和列,请确保对迁移进行适当编码。 大多数DBA(数据库管理员)可能认为Rails的null列默认值错误:默认情况下,列可以为null。 如果要创建不能为null的列,则必须显式添加参数:null => false 。 并且如果您有一个字符串列,请确保您编写了适当的限制。 默认情况下,Rails迁移会将string列编码为varchar(255) 。 通常,这太大了。 您应该尽力维护一个反映您的应用程序的数据库结构。 如果您的应用程序将登录名限制为10个字符,而不是使用不受限制的登录名,则应该对数据库进行适当的编码,如清单2所示:

清单2.使用限制和不可为空的列进行代码迁移
t.column :login, :string, :limit => 10, :null => false

您还应该考虑默认值以及可以安全提供的任何其他信息。 通过一些前期工作,您可以节省大量时间,以后再追寻数据完整性问题。 在考虑数据库基础时,还要考虑哪些页面是静态的,因此易于缓存。 如果可以在优化查询和缓存页面之间进行选择,则缓存页面将为您带来更大的回报,如果您可以忍受复杂性的话。 有时,页面或片段是纯静态的,例如状态列表或常见问题解答。 在这些情况下,缓存是扣篮。 在其他时候,您可能决定限制您的复杂性,而改为攻击数据库性能。 对于ChangeThePresent,我们已经完成了这两项工作,具体取决于问题和情况。 如果您决定攻击查询性能,请继续阅读。

N + 1问题

默认情况下,ActiveRecord关系是惰性的。 这意味着框架将等待访问关系,直到您实际访问它为止。 以具有地址的成员为例。 您可以打开控制台并键入以下命令: member = Member.find 1 。 您将在日志中看到以下内容,如清单3所示:

清单3. Member.find(1)的日志
^[[4;35;1mMember Columns (0.006198)^[[0m   ^[[0mSHOW FIELDS FROM members^[[0m
^[[4;36;1mMember Load (0.002835)^[[0m   ^[[0;1mSELECT * FROM members WHERE
 (members.`id` = 1) ^[[0m

Member与使用宏has_one :address, :as => :addressable, :dependent => :destroy定义的地址有关系。 请注意,当ActiveRecord加载Member时,您不会在日志中看到地址字段。 但是,如果您在控制台中键入member.address ,您将在development.log看到清单4的内容:

清单4.访问一个关系将强制进行数据库访问
^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;35;1mAddress Load (0.252084)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

因此,ActiveRecord在您实际访问member.address之前不会对地址关系执行查询。 通常,这种惰性设计可以很好地工作,因为持久性框架不需要移动那么多数据即可加载成员。 但是,假设您想访问成员列表及其所有地址,如清单5所示:

清单5.使用地址检索多个成员
Member.find([1,2,3]).each {|member| puts member.address.city}

由于您应该看到每个地址的查询,因此就性能而言,结果将不理想。 清单6讲述了这个故事:

清单6.对N + 1问题的查询
^[[4;36;1mMember Load (0.004063)^[[0m   ^[[0;1mSELECT * FROM members WHERE
 (members.`id` IN (1,2,3)) ^[[0m
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;35;1mAddress Load (0.000989)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Columns (0.073840)^[[0m   ^[[0;1mSHOW FIELDS FROM addresses^[[0m
^[[4;35;1mAddress Load (0.002012)^[[0m   ^[[0mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 2 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m
^[[4;36;1mAddress Load (0.000792)^[[0m   ^[[0;1mSELECT * FROM addresses WHERE
 (addresses.addressable_id = 3 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m
  ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

结果像我承诺的那样丑陋。 您会得到一个查询,查询所有成员,然后查询每个地址。 我们检索了三个成员,您得到了四个查询。 N名成员; N + 1个查询。 这个问题就是可怕的N + 1问题。 大多数持久性框架通过渴望的关联来解决此问题。 Rails也不例外。 如果您知道需要访问某个关系,则可以选择将其包含在初始查询中。 ActiveRecord为此使用:include选项。 如果您将查询更改为Member.find([1,2,3], :include => :address).each {|member| puts member.address.city} Member.find([1,2,3], :include => :address).each {|member| puts member.address.city} ,您会得到更好的结果:

清单7.解决N + 1问题
^[[4;35;1mMember Load Including Associations (0.004458)^[[0m   ^[
   [0mSELECT members.`id` AS t0_r0, members.`type` AS t0_r1,
   members.`about_me` AS t0_r2, members.`about_philanthropy`

   ...

   addresses.`id` AS t1_r0, addresses.`address1` AS t1_r1,
   addresses.`address2` AS t1_r2, addresses.`city` AS t1_r3,

   ...

   addresses.`addressable_id` AS t1_r8 FROM members
   LEFT OUTER JOIN addresses ON addresses.addressable_id
   = members.id AND addresses.addressable_type =
   'Member' WHERE (members.`id` IN (1,2,3)) ^[
   [0m
 ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:
  98:in `find'^[[0m

该查询将更快。 您将看到一个查询,它检索所有成员和地址。 渴望的协会就是这样工作的。

使用ActiveRecord,您还可以嵌套:include选项,但只能嵌套一层。 例如,考虑具有多个contactsMember和具有一个addressContact 。 如果要显示成员联系人的所有城市,可以使用清单8中的代码:

清单8:获取成员联系人的城市
member = Member.find(1)
member.contacts.each {|contact| puts contact.address.city}

该代码可以工作,但是您必须查询成员,每个联系人以及每个联系人的地址。 急于将:contacts:include => :contacts一起使用,可以稍微提高性能。 通过同时包含两者,可以做得更好,如清单9所示:

清单9:获取成员联系人的城市
member = Member.find(1)
member.contacts.each {|contact| puts contact.address.city}

通过使用嵌套的include选项,您可以做得更好:

member = Member.find(1, :include => {:contacts => :address})
member.contacts.each {|contact| puts contact.address.city}

嵌套的include告诉Rails急切地包含contactsaddress关系。 只要知道要在给定查询中使用关系,就可以使用渴望加载技术。 该技术是我们最常用于ChangeThePresent.org的性能优化技术,但它确实有局限性。 当必须跨两个以上的表联接时,最好使用SQL。 如果您需要进行报告,则几乎总是比简单地抓住数据库连接和将ActiveRecord与ActiveRecord::Base.execute("SELECT * FROM...")绕开更好。 通常,渴望的协会将绰绰有余。 现在,我将换档,并为Rails开发人员研究另一个bugaboo:继承。

继承与铁轨

当大多数Rails开发人员第一次遇到Rails时,他们都会着迷。 就是这么简单。 您只需在数据库表上创建一个type列,并从父级继承任何子类。 Rails将负责其余的工作。 例如,您可能有一个名为Customer的表,该表继承自一个名为Person的类。 客户拥有“人员”的所有列,以及会员编号和订单历史记录。 清单10展示了该解决方案的简单性。 主表具有父级和所有子类的所有列。

清单10.实现继承
create_table "people" do |t|
  t.column "type", :string
  t.column "first_name", :string
  t.column "last_name", :string
  t.column "loyalty_number", :string
end

class Person < ActiveRecord::Base
end

class Customer < Person
  has_many :orders
end

这样的解决方案在大多数方面都行之有效。 代码很简单,并且没有重复性。 查询简单且性能良好,因为您无需进行任何联接即可访问多个子类,并且ActiveRecord可以使用type列来确定要返回的记录。

但是,在某些方面,ActiveRecord继承是相当有限的。 如果您的继承层次结构太宽泛,则继承会崩溃。 例如,在ChangeThePresent,我们有几种类型的内容,每种都有名称,简短描述和简短描述,一些常见的演示属性以及几个自定义属性。 我们希望原因,非营利组织,礼物,成员,驱动器,注册表和许多其他类型的对象都可以从通用基类继承,因此我们可以以相同的方式处理所有类型的内容。 我们不能,因为Rails模型将对象模型的内容放在一个表中,这不是一个现实的解决方案。

探索替代品

我们针对此问题尝试了三种解决方案。 第一,我们将每个适当的类放在自己的表中,并使用视图为内容建立一个公共表。 由于Rails不能很好地处理数据库视图,因此我们早日放弃了该解决方案。

我们的第二个解决方案是使用简单的多态性。 通过这种策略,每个适当的子类都有自己的表。 我们将公共列下推到每个表中。 例如,假设我需要一个名为Content的超类,该超类仅具有带有GiftCauseNonprofit子类的name属性。 GiftNonprofitCause都将具有name属性。 由于Ruby是动态类型的,因此它们不需要从公共基类继承。 他们只需要响应相同的方法即可。 ChangeThePresent在多个地方使用多态来提供常见的行为,尤其是当我们处理图像时。

第三种选择是提供一种通用功能,但是使用关联而不是继承。 ActiveRecord具有称为多态关联的功能,非常适合将常见行为附加到类而不进行继承。 您先前在Address看到了一个多态关联的示例。 我可以使用相同的技术为内容管理(而不是继承)附加我的通用属性。 考虑一个称为ContentBase的类。 通常,要将该类与另一个类关联,可以使用has_one关系和一个简单的外键。 但是您可能希望ContentBase与多个类一起工作。 您需要一个外键,以及一个定义目标类类型的列。 这正是ActiveRecord多态关联的工作方式。 查看清单11中的类。

清单11.网站内容的关系双方
class Cause < ActiveRecord::Base
  has_one :content_base, :as => :displayable, :dependent => :destroy
  ...
end

class Nonprofit < ActiveRecord::Base
  has_one :content_base, :as => :displayable, :dependent => :destroy
  ...
end


class ContentBase < ActiveRecord::Base
  belongs_to :displayable, :polymorphic => true
end

通常, belongs_to关系只有一个类,但是ContentBase的关系是多态的。 外键不仅具有识别记录的标识符,而且还具有识别表的类型。 使用这种技术,我可以获得继承的大部分好处。 通用功能都在一个类中。 但是我也得到一些附带好处。 我不必将CauseNonprofit所有列都放在一个表中。

一些数据库管理员不喜欢多态关联,因为它们不使用真正的外键,但是对于ChangeThePresent,我们可以自由使用它们。 实际上,数据模型在理论上并不像它可能那样美丽。 您不能使用引用完整性之类的数据库功能,也不能依靠工具来根据列名发现关系。 对我们而言,干净简单的对象模型的优点胜于该方法的问题。

create_table "content_bases", :force => true do |t|
  t.column "short_description",          :string

  ...

  t.column "displayable_type", :string
  t.column "displayable_id",   :integer
end

结语

ActiveRecord是一个功能完善的持久性框架。 您可以使用它构建可扩展的,可靠的系统,但是与任何数据库框架一样,您必须注意框架生成SQL。 偶尔遇到问题时,必须调整方法。 跟上索引,使用include进行急切的加载,在地方使用多态关联而不是继承是您改善代码库的三种方式。 下个月,我将引导您完成另一个编写真实世界Rails的示例。


翻译自: https://www.ibm.com/developerworks/web/library/wa-rails3/index.html

activerecord

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值