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
选项,但只能嵌套一层。 例如,考虑具有多个contacts
的Member
和具有一个address
的Contact
。 如果要显示成员联系人的所有城市,可以使用清单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急切地包含contacts
和address
关系。 只要知道要在给定查询中使用关系,就可以使用渴望加载技术。 该技术是我们最常用于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
的超类,该超类仅具有带有Gift
, Cause
和Nonprofit
子类的name
属性。 Gift
, Nonprofit
和Cause
都将具有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
的关系是多态的。 外键不仅具有识别记录的标识符,而且还具有识别表的类型。 使用这种技术,我可以获得继承的大部分好处。 通用功能都在一个类中。 但是我也得到一些附带好处。 我不必将Cause
和Nonprofit
所有列都放在一个表中。
一些数据库管理员不喜欢多态关联,因为它们不使用真正的外键,但是对于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